📖

re:Invent 2024: AWSが解説 ElastiCacheの高度なデータモデリング

2024/01/01に公開

はじめに

海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!

📖 AWS re:Invent 2024 - Advanced data modeling for Amazon ElastiCache (DAT422)

この動画では、Amazon ElastiCacheとValkeyを活用したアプリケーションパフォーマンスの向上とスケーリングコストの削減について解説しています。マイクロ秒単位のレスポンスタイムを実現するElastiCacheの特徴や、Lazy LoadingやWrite-Throughなどの具体的なCachingストラテジー、Thundering Herd Problemへの対処方法が詳しく説明されます。また、Hyperloglogを使用したユニークユーザー数のカウント方法や、Token Bucketアルゴリズムを用いたRate Limitingの実装など、高度なユースケースも紹介されています。さらに、TTLやEvictionなどのキャッシュ管理手法、レプリカ活用によるスループット改善など、実践的なベストプラクティスについても言及されています。
https://www.youtube.com/watch?v=Ej6TRC_uYzA
※ 動画から自動生成した記事になります。誤字脱字や誤った内容が記載される可能性がありますので、正確な情報は動画本編をご覧ください。
※ 画像をクリックすると、動画中の該当シーンに遷移します。

re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!

本編

ElastiCacheとValkeyの概要:高性能データストアの紹介

Thumbnail 0

みなさん、こんにちは。本日のセッションにご参加いただき、ありがとうございます。とても興味深いトピックについてお話しさせていただきます。私はSenior Engineering ManagerのYaronです。そして、一緒にお話しさせていただくのは、Principal Software EngineerのKevin McGeheeです。私たち二人ともElastiCacheとAWSのチームに所属しています。

本日のアジェンダに入る前に、皆様が適切な会場にいらっしゃることを確認させていただきます。アプリケーションのパフォーマンスを向上させ、スケーリングコストを削減することにご関心のある方、そして、マイクロ秒単位のレスポンスタイムで毎秒数百万のリクエストを処理しながら、ElastiCacheが提供する高度なデータ構造をどのように活用するかを理解したいと考えているソフトウェアエンジニア、DevOps担当者、経営層の方々にとって、このセッションは最適です。

Thumbnail 60

Thumbnail 80

本日は、ElastiCacheとValkeyについてご紹介させていただき、その後、高度なユースケースについて一緒に見ていきます。そして最後に、ベストプラクティスと運用の概要についてお話しします。 Amazon ElastiCacheは、クラウド上でのin-memoryデータストアの導入、運用、スケーリングを容易にする、フルマネージドサービスです。マイクロ秒単位のレスポンスタイムを実現することで、アプリケーションのパフォーマンスを大幅に向上させることができます。現在、Redis open source、Memcached、そして今年発表されたValkey open sourceという3つのオープンソースエンジンをサポートしています。Multi-AZ、包括的なセキュリティとコンプライアンス対応、そして他のサービスとの優れた連携など、クラウドでの作業を容易にする多くの重要な機能をサポートしています。

Valkeyの特徴と性能:Redisの代替として

Thumbnail 130

Thumbnail 150

お客様はElastiCacheを様々なユースケースで活用されています。一見すると、これらのユースケース間にはあまり共通点がないように見えるかもしれません。これは実際にその通りなのですが、 ValkeyとRedis open sourceが提供するAPIとデータ構造のおかげで、これらすべてのユースケースを非常に効果的かつ高速にサポートすることができます。

Thumbnail 170

ここでValkeyについてお話ししましょう。今年、Amazon ElastiCacheとMemoryDBは、Valkeyという3番目のオープンソースエンジンのサポートを発表しました。Valkeyは、高性能なKey-Valueデータストアで、Redisのコミュニティ代替として位置づけられています。既存のRedis open sourceのコントリビューターによって開発され、Redis open source 7.0.2のドロップイン置き換えとして機能します。Linux Foundationが管理しており、オープンソースとして維持されることが保証されています。

Thumbnail 190

Valkeyは、パフォーマンス、信頼性、効率性の面でトップを走っています。AWSチームはオープンソースコミュニティに2つの重要な貢献をしました。1つ目はパフォーマンスの面で、毎秒100万リクエストを実現する新しいスレッディングモデルをオープンソース版に提供しました。また、メモリ使用量を最大20%削減できる効率的なアーキテクチャにも貢献しました。

キャッシングの基本:パフォーマンス向上とコスト削減

Thumbnail 220

Thumbnail 240

さて、イントロダクションが終わったところで、いよいよ本題に入っていきましょう。ここからは、皆さんと一緒にサンプルアプリケーションを作りながら、すべてのユースケースを結びつけていきたいと思います。 あなたがシステムアーキテクト、ソフトウェアエンジニア、あるいはDevOpsエンジニアとして、新しいクールなスタートアップに入社したと想像してみてください。どのスタートアップでもそうですが、初期投資は限られていますが、ワークロードの増加や新規顧客の参加に柔軟に対応できる、スケーラブルなインフラを構築したいと考えています。

Thumbnail 280

Thumbnail 300

まずはキャッシングの概念から始めて、インフラコストを削減しながらパフォーマンスを向上させる方法をご紹介します。その後、より高度なキャッシング技術についても見ていきます。 アプリケーションはEC2で実行されており、データの永続的な保存にはAmazon RDSを選択したと仮定しましょう。Amazon RDSはディスクにデータを保存するため、考慮すべき点がいくつかあります。

Thumbnail 310

Thumbnail 320

Thumbnail 330

Thumbnail 340

その1つが、ディスクからデータを取得する際の追加レイテンシーです。 ただし、ある程度のワークロードまでは、これで問題なく動作します。ワークロードが増えてきた場合、 より大きなインスタンスサイズを選択してRDSをスケールアップすることができます。また、レプリカを追加してスケールアウトすることで、 RDSの読み取りスループットを向上させることもできます。しかし、ある時点で上限に達してしまい、 ワークロードの増加に対してインフラをさらに強化する方法を考える必要が出てくるでしょう。

Thumbnail 410

ここで、Amazon ElastiCacheの利用を検討してみましょう。ElastiCacheはデータを明示的にメモリに保存するため、ディスクと比べて20倍高速です。確かにRDSにもキャッシュがあり、データブロックやページをキャッシュに保存します。しかし、複数のテーブルを結合する複雑なSQLクエリの場合、これらのページ全体を走査してディスクからデータを取得する必要があり、追加のレイテンシーが発生します。Amazon ElastiCacheを使えば、結果のみを保存し、キャッシュから直接データを取得することで、マイクロ秒単位のレスポンスタイムを実現できます。キャッシュにデータを投入すれば、すぐにそこからデータの読み取りを開始でき、クエリのレスポンスタイムをサブミリ秒まで改善し、バックエンドの負荷を軽減できます。また、 バックエンドのスケールダウンを可能にすることで、コストの最適化も図れます。

高度なキャッシング戦略:Lazy LoadingとWrite-Through

Thumbnail 420

Thumbnail 430

Thumbnail 440

Thumbnail 450

Thumbnail 460

では、いくつかのCachingストラテジーについて見ていき、私たちのマーケットプレイスのスタートアップにどれが最も適しているか、一緒に考えていきましょう。まず最初に、Lazy Loadingと呼ばれる方法から見ていきます。コンセプトはとてもシンプルです。最初にCacheからデータを読み込みます。もしアイテムがCacheにあれば、それで完了で、マイクロ秒単位でクエリが終わります。もしアイテムがCacheにない場合は、この例ではRDSにあたるソースから読み込む必要があります。クエリの複雑さによって、1ミリ秒から10ミリ秒以上かかる可能性があります。アイテムを取得したら、次回のためにCacheを更新して、保存しておくことができます。

Thumbnail 470

Thumbnail 480

Thumbnail 490

2つ目のアプローチはWrite-Throughと呼ばれるものです。まず、ソースデータベースでアイテムを更新し、その直後にCacheのデータも更新します。実質的に、ソースとCacheの両方で同一のアイテムを複製することになります。それぞれのアプローチにはトレードオフがあります。私たちのマーケットプレイスでは、これら2つを組み合わせて異なるストラテジーを作ることに焦点を当てたいと思います。Cacheからデータを読み込むためにLazy Loadingを使用し、必要に応じてCacheのアイテムを無効化するためにWrite-Throughアプローチを使用します。

Thumbnail 520

Thumbnail 530

Thumbnail 540

Thumbnail 550

どのように機能するか見てみましょう。まず、Cacheからデータを読み込み、次にデータベース自体からデータを読み込みます。この目的のためにAmazon DynamoDBを選択し、その後、Cacheのアイテムを更新します。ここで、ソース自体でこのアイテムまたは別のアイテムを変更し、Cacheのアイテムが古くなっていないことを確認したい場合を考えてみましょう。そのために、DynamoDBのTrigger機能を使用して、AWS Lambda関数を呼び出し、Cacheを無効としてマークし、そのアイテムをCacheから削除します。このように、2つのアプローチを組み合わせて、私たちのニーズに合った独自のものを作成しています。

Thumbnail 570

Thumbnail 580

場合によっては、よく使用されるアイテムを有効期限が切れる前に再キャッシュしたいことがあります。これを解決するために、有効期限が切れる前にCacheを更新するバックグラウンドタスクを作成できます。この例では、Multi-Commandsを使用しています。これにより、複数の異なるコマンドをキューに入れ、アトミック性を保ちながら、サーバーサイドで1つのトランザクションとしてすべてを一度に実行することができます。この例では、まずCacheからアイテムを取得し、TTLコマンドを使用して有効期限をチェックし、Cacheで実行すると値を受け取ります。

Thumbnail 620

Thumbnail 630

Thumbnail 640

有効期限までの秒数も受け取ります。この例では、クライアントが徐々に有効期限をリッスンし、最後のクライアントが5秒のしきい値を超えるまでの様子がわかります。その直後に、データベースからデータを事前に取得し、ソースに再投入します。ここで、Thundering Herd Problemと呼ばれる問題を避けたいと思います。Thundering Herd Problemは、複数のプロセスやスレッドが同時に処理を試みることで発生し、多くの場合、高い競合やストレインを引き起こし、最終的にボトルネックの原因となります。

リアルタイム分析とセッション管理:ElastiCacheの活用

Thumbnail 660

Thumbnail 670

Thumbnail 680

Thumbnail 690

Cacheに保存されている非常にホットなアイテムが間もなく期限切れになるというシナリオを考えてみましょう。 どのようなことが起こるでしょうか?すべてのクライアントが、すでに期限切れとなったこのホットアイテムを取得するためにCacheに接続しようとします。 そして、同時にCache missが発生します。その結果として、すべてのクライアントがRDSにアクセスすることになり、RDSに大きな負荷がかかり、 最終的にレイテンシーとパフォーマンスに影響を及ぼすことになります。そこで、バリアのような同期メカニズムを構築することで、 1つのクライアントだけがリクエストを行い、アイテムをCacheに再配置できるようにすることができます。

Thumbnail 700

Thumbnail 710

Thumbnail 720

Thumbnail 730

Thumbnail 740

この特定のクライアントがソースからアイテムを取得している間、他のすべての クライアントは完了するまで待機します。データの取得とCacheへの更新が完了すると、すべてのクライアント がCacheからデータを読み取り、マイクロ秒単位のレスポンスタイムを享受できるようになります。 この実装方法についてさらに詳しく見ていきましょう。この例では、2つのクライアントだけを使って説明します。一つずつ、実際の動作を見ていきましょう。 最初のクライアントの方が少し早く、Cacheからアイテムを取得しようとしましたが、アイテムはすでにCacheにありません。nilが返されます。

Thumbnail 750

Thumbnail 760

Thumbnail 780

次に2番目のクライアントが同じアイテムを取得しようとしますが、アイテムがCacheにないため、同様にnilが返されます。 先ほど述べたように、1番目のクライアントの方が少し早かったため、Cache上で定義されたロックを取得することに成功します。 その直後、データベースからアイテムを更新するためのクエリを実行します。2番目のクライアントは同じロックの取得を試みますが、すでに1番目のクライアントによってロックが取得されているため失敗します。そのため、ロックが解放されるまで定期的にチェックしながら待機することになります。

Thumbnail 790

Thumbnail 810

Thumbnail 820

1番目のクライアントがデータベースからアイテムの取得を完了すると、そのアイテムをCacheに更新してロックを解放し、最終的に 他のクライアントがCacheからアイテムを取得できるようになります。これが、Amazon ElastiCacheをバリアとして使用して同期を作成し、Thundering Herd問題を回避する方法です。ここまでAmazon RDSとAmazon ElastiCacheを使用した具体的なCachingの例を紹介してきました。 しかし、重要なのは、私たちはRDSだけに限定されているわけではないということです。Amazon S3のオブジェクトもCacheすることで、 S3へのI/Oを行う必要がなくなり、直接ElastiCacheにアクセスできるため、パフォーマンスの向上とコスト削減を実現できます。

Thumbnail 840

Thumbnail 850

Thumbnail 870

Thumbnail 880

ElastiCacheはRedisを使用しており、Redisはバイナリセーフです。そのため、Redisオープンソースではほぼすべてのものをシリアライズできます。シリアライズ可能な すべてのものをElastiCacheにCacheとして保存し、高いパフォーマンスとコスト削減を実現できます。 さらに高度なCachingのユースケースである、Client-side Cachingについて見ていきましょう。通常、お客様はさらに低いレイテンシーを実現するため、あるいはアプリケーション自体でローカルCacheを使用する利便性のために、これを使用します。考え方はシンプルで、 Cacheからアイテムを取得し、データを受け取ったらローカルCacheに保存します。その時点から、アイテムが 古くなるまで、リモートCacheと通信する必要がなくなります。

Thumbnail 890

Thumbnail 900

アイテムが古くなったかどうかを判断する方法には2つのアプローチがあります。1つ目はTime to Live(TTL)による有効期限です。有効期限が切れると、そのアイテムは無効となります。2つ目のアプローチは、チャンネルをサブスクライブして、キーの更新を受け取ると、キーの通知を受け取り、そのアイテムを無効とマークできます。Redisにはクライアントサイドキャッシングの実装方法、特に無効化に関して2つの異なる方法があります。

Thumbnail 920

Thumbnail 940

Thumbnail 950

デフォルトモードでは、サーバーがクライアントとそれらが更新したキーとのマッピングをすべて記憶します。これにより、サーバーは特定のキーに関心のある特定のクライアントのみに更新を通知できます。ただし、ここでの課題は、サーバー側でメモリを消費することです。2つ目のアプローチはブロードキャストモードです。このモードでは、クライアントがプレフィックスをサブスクライブし、そのプレフィックスに一致するキーが更新されると、クライアントは通知メッセージを受け取ります。

Thumbnail 990

Thumbnail 1010

これが実際にどのように実装されているか見てみましょう。クライアントアプリケーションとConnection Poolがあります。私たちは常にConnection Poolの使用を推奨しています。これは、TCP接続が一般的なRedisコマンドと比較して比較的コストの高い操作だからです。例えば、SETコマンドやGETコマンドの処理は、既存の接続を使用する場合、処理速度が桁違いに速くなります。Connection Poolを2つのセグメントに分割します。Connection 0は無効化用の接続として使用され、他のすべての接続の無効化を統合します。Connection 1から9はデータ接続として機能します。すべてのデータはこれらの接続を通じて流れます。

機械学習とRate Limiting:ElastiCacheの高度な使用例

Thumbnail 1020

Thumbnail 1030

Thumbnail 1050

Thumbnail 1070

Connection 0は無効化チャンネルをサブスクライブし、他のすべての接続はRedirect Trackingを使用してConnection 0にリダイレクトします。キーを更新するたびに、無効化メッセージで受信し、ローカルキャッシュを更新できます。キャッシングを使用してパフォーマンスを向上させ、コストを削減する方法を理解したところで、マーケットプレイスでSession Storeが必要な理由を見てみましょう。マーケットプレイスでは、接続されているすべてのクライアントのセッションを保存する必要があります。ショッピングカート、ユーザー設定、認証、その他の重要なセッションデータを含める必要があります。私たちの課題は、高速で動的なリアルタイムセッションデータを処理するための低レイテンシーで高並行性のデータストアを持つことです。

Thumbnail 1090

より具体的には、パーソナライゼーションを作成するためにSession Storeを使用します。つまり、各クライアントがウェブサイトに接続したときに、異なるルック&フィールを体験できるようにします。クライアントはLoad Balancerを通じてバックエンドに接続し、バックエンドサーバー間で接続が分散されます。各Microserviceにはユーザーセッションのリストが含まれていますが、最終的にはすべてのセッションを分散メモリまたは分散キャッシングとしてAmazon ElastiCacheに保存します。サーバーが削除または追加された場合でも、すべてのセッションはElastiCache上で有効なままとなり、お客様にシームレスな体験を提供し、Microservice環境をステートレスモデルで実行することができます。

Thumbnail 1140

Thumbnail 1150

Thumbnail 1170

Thumbnail 1190

セッションデータを保存するために、Hashデータ構造を使用することは非常に一般的です。Hashデータを使用する際、まずのようにHashを識別するためのHashキーがあり、IDが実際の値にマッピングされるキーと値のペアを持ちます。これを構築するには、HashのIDに続いてHash自体のキーと値のペアのリストを指定して、HSETコマンドを使用します。Hashは非常に柔軟で、ランダムアクセスを使用して単一の値を取得する際に定数時間で実行できます。この例では、HGETコマンドを使用してオブジェクトの名前だけを取得できます。コードで実際に見てみましょう。まず、ElastiCacheサーバーに接続し、その後、Pythonクライアントを使用してセッションを作成します。キーと値のペアを使用してHashデータを生成するディクショナリマップを使用し、セッションIDを生成して、そのセッションIDをRedisに保存します。セッションIDはValkeyに保存します。

Thumbnail 1210

Thumbnail 1220

そのために、HSETコマンドを使用して、セッションIDと先ほど作成したディクショナリを指定します。データを取得するには、HGETALLコマンドを使用して、先ほど生成したセッションを指定します。これで、セッションIDをアンマーシャルして、アプリケーション内部のセッションオブジェクトにデコードできます。

Thumbnail 1240

Thumbnail 1250

Thumbnail 1260

では、マーケットプレイスに機械学習の機能を追加する方法を見てみましょう。ユーザーに対して機械学習ベースのレコメンデーションを構築し、おすすめの画像を表示できるようにします。具体的には、モデル自体については説明しませんが、Amazon ElastiCacheを機械学習インフラストラクチャの一部としてどのように統合できるかをお見せします。まず、Feature Storeについて理解する必要があります。Feature Storeは、機械学習インフラストラクチャの最も重要な要素の1つです。トレーニングと推論のユースケースで利用可能な特徴量の単一のソースを提供します。Feature Storeには2種類あります:モデルトレーニングとバッチスコアリング用のオフラインと、超高速な特徴量計算用の2つ目です。

Thumbnail 1290

このダイアグラムでは、機械学習インフラストラクチャを構築するために選択したアーキテクチャを確認できます。トレーニングとオンライン推論に使用できる特徴量定義を登録するための中央処理システムとしてFEASTを使用します。オフラインFeature StoreにはAmazon Redshiftを使用し、オンラインFeature StoreにはElastiCacheを使用して、オンライン推論用の特徴量を超高速なレスポンスタイムで提供し、信用スコアリングモデルを使用してリアルタイムの予測を行います。

Thumbnail 1330

Thumbnail 1350

定義するために、feature_store.yamlを使用します。ご覧のように、オンラインFeature StoreのエンジンとしてValkeyを定義し、このFeature StoreにアクセスするためのElastiCacheエンドポイントを提供しています。オフラインFeature Storeについては、Redshiftを使用し、管理者認証情報を提供します。セッションストアと同様に、ランダムアクセスと柔軟性のためにHashデータ構造を使用します。Hashキーは特徴量名となり、Hash値は特徴量の値となります。

Thumbnail 1400

それでは、リアルタイム分析に移りましょう。続いてKevinに話を進めてもらいたいと思います。セッションの前半をお楽しみいただけたことと思います。ありがとうございました。ここからは、ElastiCacheとValkeyのAPIを活用して、サンプルアプリケーションのリアルタイム分析のユースケースを実現する方法について、いくつか例を挙げながら説明していきたいと思います。私たちのNight Sky Marketplaceについて最初に解決したい課題は、「どの写真が最も閲覧されているか」という点です。そのために、リーダーボードのような仕組みを使用します。

Thumbnail 1420

この例では、リーダーボードに4つの異なる画像が表示されており、それぞれに閲覧数というスコアが関連付けられています。Valkeyでは、これをSorted Set(ソート済みセット)というデータ構造を使って表現できます。Sorted Setは、Valkeyエンジン内部で2つの異なるデータ構造を組み合わせたものです。これらは、皆さんが情報科学の入門授業で学んだものです。1つ目はハッシュマッピングで、ユーザーをスコアに紐付けます。この場合、ユーザーは閲覧数を記録している画像で、スコアは閲覧回数となります。

Thumbnail 1440

Thumbnail 1470

ソート順を維持するために、もう1つのデータ構造が必要です。この場合、スキップリストを使用してスコア順にソートを維持します。ご覧のように、画像と閲覧数のペアが最低スコアから最高スコアまで並んでいます。興味深いのは、このスキップリストが双方向にリンクされているため、最高スコアから最低スコア、あるいは最低スコアから最高スコアの順に反復処理できる点です。これら2つのデータ構造を組み合わせることで、データ構造の読み書き両方において対数的な処理が可能になります。

Thumbnail 1500

実際の使用方法を見てみましょう。ValkeyのAPIでは、多くのコマンドの先頭に、操作対象のデータ構造を示す文字が付いています。リーダーボードとSorted Setの場合、「Z」というプレフィックスがSorted Setを操作することを示します。Sorted Setにアイテムを追加するには、ZADDコマンドを使用します。ここでは、「leaderboard_views」というキーに対して、image_oneを31というスコアで追加しています。これを実行すると、Sorted Setが作成されます。

Thumbnail 1510

Thumbnail 1520

下に示すスキップリストには、最初は1つのエントリだけが含まれています。先ほどの4つのサンプルエントリに対して、この操作を繰り返すことができます。新しいエントリを追加するたびに、データ構造がリアルタイムで更新され、クエリを実行できる状態が維持されます。ただし、これは例示用であり、実際のアプリケーションでは、すでに画像が登録されている状態で、新しい閲覧があるたびにカウントをインクリメントするような実装が望ましいでしょう。

Thumbnail 1540

Thumbnail 1570

同様の操作を行うコマンドとして、ZINCRBYがあります。これはSorted Set内の既存の値 または新しい値をインクリメントするものです。例えば、ZINCRBY image oneでcount 10 viewsを実行すると、31から41に変更されます。新しく更新された値が返され、Sorted Set内での位置が調整されます。このデータ構造に対してはいくつかの異なるクエリ方法があります。ここでは、ZRANGEコマンド を使用します。これにはさまざまなオプションがありますが、今回はスコアの高い順から低い順にLeaderboard全体を表示してみましょう。

Thumbnail 1600

Thumbnail 1620

ここではバウンドの設定ができます。今回は無限大からゼロまでを指定しているので、開始位置も終了位置も制限がありません。実際のユーザーやimage IDではなく、スコアによってこのSorted Setをクエリしています。また、先ほど説明した双方向リンクの特性を利用して、末尾から先頭への逆順で実行しています。 これを実行すると、Sorted Set内のすべてのキーと値のペアが返されます。Image fourが最も閲覧数の多い写真で56ビュー、Image threeが最も少なく18ビューとなっています。これにより、Valeエンジン内でマイクロ秒 レベルのレイテンシでリアルタイム分析の更新と読み取りが可能になります。

Thumbnail 1640

Thumbnail 1650

Vale APIを使用して実装できるリアルタイム分析の別の例を見てみましょう。それは、各写真のユニークユーザー数です。単なる総閲覧数だけでなく、ユーザーベースからのユニークな閲覧数や、さらにはユニークなLikeの数でセンチメント分析を行いたい場合があります。 この例では、各ユーザーが特定のイメージにマッピングされています。これを実現する最も単純な方法、つまり最も明白な方法は、これらの異なるユーザーIDをSetに格納することです。 この例では、4つのアイテムを持つSetになります。このアプローチの欠点は、閲覧したユーザー数に比例してスペース複雑度が線形的に増加することで、O(N)のスペースが必要になります。

Thumbnail 1680

このデータを圧縮するさまざまな方法はありますが、結局のところスペース複雑度はO(N)のままで、インメモリデータストアとしては理想的ではありません。ValeエンジンはこれをHyperloglogと呼ばれるデータ構造で解決しています。 これは、セットの要素数を近似値で求める確率的データ構造です。精度をサイズと引き換えにすることで、実装上の総サイズを12キロバイトに制限しています。何人のユーザーを追加しても、1%未満のエラー率を維持しながら、メモリ使用量は最大12キロバイトに抑えられます。

Thumbnail 1750

Thumbnail 1760

Thumbnail 1770

Sorted SetコマンドにZプレフィックスが使用されたように、HyperloglogコマンドにはPFプレフィックスが使用されます。PFADDコマンドを使用して、特定のHyperloglogにユーザーのセットを追加できます。ここでは、viewers image oneと呼び、データ構造が変更されたかどうかに基づいて1か0が返されます。空の状態から3つを追加したので、 追加されたことが通知されます。次にPFCOUNTコマンドを使用すると、ユニークな閲覧者数の推定値を取得できます。 この場合、まだエラーがないため3が返されます。重複を追加しようとすると、 データ構造が更新されなかったため0が返され、PFCOUNTは引き続き3を返します。

Thumbnail 1780

Thumbnail 1790

Thumbnail 1800

それでは、スケールした場合にどのように動作するか見てみましょう。 Vale engineで、Setと Hyperloglogの両方に10,000個の数値を追加して、精度とサイズを比較する実験を行いました。 Setの場合、追加した要素の正確な数である10,000が返されます。 しかし、欠点として、メモリ使用量が約420キロバイトにもなり、Setに項目を追加するたびにさらに増加し続けます。

Thumbnail 1810

Thumbnail 1830

Hyperloglogに対してPFCOUNTを実行すると、ユーザー数を約13人少なく見積もりましたが、これはこのデータ構造の1%のエラー率の範囲内です。 メモリ使用量を見ると、先ほど説明した約12キロバイトの境界線付近であり、実際のSetのメモリ使用量の30分の1以下です。これは、大量のユーザーがいる場合にSetが必要とする大きな容量を使用せずに、ユニークビューワーを推定する効果的な方法です。

地理空間機能とToken Bucketアルゴリズム:ElastiCacheの多様な機能

Thumbnail 1860

Thumbnail 1870

Thumbnail 1880

リアルタイム分析から話題を変えて、 Valkey engineの地理空間機能と、このサンプルアプリケーションでの活用方法について説明しましょう。 マーケットプレイスのユーザーに対して、彼らの現在地から近い場所で撮影された夜空の写真を表示したい場合を考えてみましょう。ここでは、Las Vegasの地図があり、Las Vegas Strip付近にいる状況を想定します。

Thumbnail 1890

Thumbnail 1900

Thumbnail 1920

Valkeyでは、緯度と経度の座標を52ビットの整数にエンコードするGeohashアルゴリズムを使用できます。 この整数は、先ほど見たSorted Setデータ構造とスコアとして組み合わせることで、Sorted Set上でバウンディングボックスや半径による検索を可能にします。 これは独自のデータ構造であるGEO Setとなり、Sorted Setと同様に、セットの追加やクエリ操作がO(log(N))の時間複雑度で実行できます。

Thumbnail 1930

Thumbnail 1950

具体例を見てみましょう。Stripの近くで撮影された画像1枚と、Las Vegas Airport近くで撮影された画像1枚があるとします。GEOADDコマンドを使用して、これらの画像の緯度経度のペアをGEO Setに追加すると、保存されたことを示す1が返されます。 Treasure Islandからこのウェブサイトにアクセスして、近くにあるものを検索する場合、GEORADIUSコマンドを使用できます。コマンドに自分の緯度経度を入力し、5キロメートルの半径などの範囲を指定すると、その範囲内にある両方の画像が返されます。

Thumbnail 1970

Thumbnail 1980

Thumbnail 2010

オブジェクトが自分からどれだけ離れているかを正確に知るためにさらにパラメータを追加することもできますが、ここではシンプルなバージョンをお見せしています。 より狭い範囲、例えば1キロメートル半径で絞り込みたい場合は、image3だけが返されます。また、GEOセット内のオブジェクト間の距離を求めることもできます。image3とimage2の間の距離を確認するには、GEODISTを使用して、 image2とimage3のオブジェクトIDを渡すと、メートル単位で結果が返され、約4.5キロメートル離れていることがわかります。

Thumbnail 2030

Rate Limitingの活用方法について、もう一つの例を見ていきましょう。Rate Limiterの概念はシンプルです:分散アプリケーションにおいて、任意のリソースへのダウンストリームコールを制限したい場合に使用します。例えば、私たちのナイトスカイマーケットプレイスに天気予報機能があり、ユーザーごとにダウンストリームコールを制限したいとします。これを各アプリケーションで実装することもできますが、分散システムでは、アプリケーション間でRate Limiting機能を同期させる方法がありません。そこで、先ほどのセッションストアのユースケースと同様に、Amazon ElastiCacheを使用して、バックエンドですべてを同期させることができます。

Thumbnail 2080

Thumbnail 2090

Thumbnail 2100

Rate Limitingについて、シンプルな例と、より高度なユースケースを見ていきましょう。シンプルな方は、先ほど見た文字列データ型を使用しますが、数値演算に活用します。 バイナリセーフなデータのSetやGetを行うだけでなく、実際には文字列を数値としてO(1)の時間で操作することができます。 例えば、"number"というキーに対してINCRコマンドを使用すると、キーが存在しない場合は1増加させ、値が1のこのキーを作成します。その後、インクリメントを続けることができます。

Thumbnail 2110

Thumbnail 2120

このコマンドを実行するたびに、値が増加し、新しい値が返されます。この機能を使って、かなりシンプルなRate Limiterを構築できます。

Thumbnail 2150

また、Redisがサポートする別の機能であるLua Scriptingも活用します。Lua Scriptingは、Amazon ElastiCache上のRedisサーバーで実行できるスクリプト言語で、先ほど見たMulti Execケースと同様のアトミックな操作を可能にしますが、サーバーサイドでより複雑なロジックを実行できます。一般的なRate Limiting アルゴリズムでは、カウンターを0から開始します。この場合は3という事前に定義された制限までカウントアップし、TTL値を使用してアイテムを削除し、TTLが期限切れになるまで追加のリクエストを拒否します。その時点でカウンターがリセットされ、再び呼び出しができるようになります。一般的なアニメーションの例で見ると、リクエストが行われ、最初のリクエスト時にTTLが設定されます。3回のリクエストを行うと、リセットされるまでそれ以上のリクエストは許可されず、その後さらにリクエストを行うとTTLが再度リセットされます。

Thumbnail 2190

スクリプトの内容を説明させていただきます。1画面に収まる比較的シンプルなものです。まず、ハードコードされた制限値を設定します。この例では10秒間に4回のコールが可能となります。スクリプトには引数を渡すことができ、ユーザーごとのキーを受け取ります。そしてそのキーを取得するか、存在しない場合はデフォルト値の0から始めます。0の場合は、有効期限の開始を可能にするために設定する必要があります。現在の値を0に設定し、TTLが機能し始めるようにします。制限値を下回っている場合は、incrementを呼び出して1を返し、コールが許可されたことを示します。制限値に達しているか超えている場合は0を返し、Redisのバックグラウンドプロセスがそのキーを削除し、プロセスが空の状態からリセットされるまで、その後のコールは0を返し続けることになります。

Thumbnail 2260

Thumbnail 2280

Thumbnail 2290

Redis APIでこのスクリプトを実際に呼び出す方法についてお話しましょう。 これには2つのパートがあります。まずスクリプトをロードし、それから実行します。スクリプトをロードするには、Script Load機能を使用し、スクリプト全体を文字列引数として渡します。すると、スクリプト自体の一意の識別子であるSHA IDが返されます。 このIDを使って、クライアントからスクリプトを評価することができます。 EVALSHAでスクリプトIDを指定し、先ほど説明した引数を渡します。この場合は、操作対象のキーという1つの引数だけです。様々なフレームワークやクライアントでは、SHAを直接操作する必要がないように抽象化されています。しかし、これがスクリプトを呼び出す一般的なプロセスであり、この例では最初のコールが許可されたことを示す1が返されます。

Thumbnail 2320

Thumbnail 2330

Thumbnail 2340

Thumbnail 2350

次は、Token Bucket アルゴリズムを使用したより高度なRate Limitingの例を見てみましょう。この場合、固定容量のバケットがあり、特定のレートでトークンが追加または補充され、 リクエストがそれらのトークンを消費してバケットから取り除きます。依然としてユーザー固有のバケットを持ちますが、 より多くの変数があります。容量、補充レート、現在のトークン数があります。単純なカウンターの代わりに、Hash型のデータ構造を使用して、 特定のToken Bucketに関連するすべての情報を保存します。この場合、現在のトークン数とエポックからのミリ秒単位での最終更新時間を保持する必要があり、これを使用してコールが行われるたびにバケットに補充するトークン数を決定します。

Thumbnail 2370

Thumbnail 2400

そのスクリプトが実際にどのようになっているか、いくつかのパートに分けて見ていきましょう。前回のように変数をハードコードする代わりに、スクリプトの引数として受け取ります。これらはキーと共にクライアントから渡すことができます。そして、HMGETを使用してデータストアからそれらのメタデータを取得し、Redisの関数である現在時刻も取得します。 次に、バケットを補充する関数を実行します。既に存在する場合は、最後の補充時刻と現在時刻の差に基づいてバケットに追加するトークン数を計算し、バケットサイズを超える場合はそれに制限します。

Thumbnail 2430

一方、初めてコールが行われた場合は、バケットサイズのトークンから開始します。その後、コールが許可されるかどうかを判断します。この場合、 カウントアップではなくカウントダウンを行います。少なくとも1つのトークンがある場合、コールを許可してそれを減算します。その後、HSETを使用して最終更新時刻と現在のトークン数の両方を更新して状態を更新します。最後に、ここでも有効期限を使用していますが、これは単純なアルゴリズムのように継続的なコールを維持するための正確性のためではなく、メモリ最適化のためです。しばらくコールがない場合、その状態を削除して最初からやり直せるようにするためです。

ElastiCacheとValkeyの運用ベストプラクティス

Thumbnail 2470

Thumbnail 2490

Thumbnail 2500

Thumbnail 2510

Thumbnail 2520

ここからは、ElastiCacheとValeを使用する際のベストプラクティスと運用の概要について、これらの高度なデータ構造を実装する際のヒントとコツをお話ししたいと思います。 まず覚えておくべき重要なポイントは、キャッシュは永続的ではなく、データベースの代替にはならないということです。ElastiCacheを使用する場合は、一時的なデータに使用するのがベストです。 マルチAZキャッシュでは99.99%の可用性SLAを提供していますが、システムはデータの一貫性よりも可用性、つまり読み取りと書き込みを継続できることを優先します。 これが実装にどう影響するかというと、プライマリとレプリカ間のレプリケーションは非同期で行われるため、プライマリノードの単一障害によってシステムでデータの末尾が失われる可能性があるということです。

Thumbnail 2540

Thumbnail 2550

Thumbnail 2560

Thumbnail 2570

プライマリとレプリカがある複製セットアップでユーザーが書き込みを行う場合、プライマリにキーを設定してOKが返ってきます。しかし、そのプライマリは非同期でレプリカに結果を送信します。その送信前にプライマリが障害を起こすと、クライアント側で確認済みであってもそのデータは失われてしまいます。 これに対処するには、いくつかの方法があります。ベストプラクティスは、このデータ損失を想定して対応できるように計画することです。単純なキャッシュの場合は、アイテムが見つからない場合にデータベースから再取得することで対応できます。Rate Limiterのような場合は、障害発生時に1回余分に呼び出しが発生しても問題ないかもしれません。しかし、より複雑なケースでは、損失を検出して再構築や修復を行う方法を実装する必要があるかもしれません。

Thumbnail 2590

Thumbnail 2620

Thumbnail 2630

Thumbnail 2640

これらのユースケースで、対応が負担になりすぎる場合は、Amazon MemoryDBの使用を検討することをお勧めします。これは私たちのチームがサポートしている別のAWSサービスで、Vale互換のAPIを提供しますが、違いは永続性と一貫性があることです。バックグラウンドでマルチAZトランザクションログを使用して、永続性を確保しています。 キャッシュからアイテムを削除し、安定したサイズを維持する方法については、データサイズを特定のしきい値以下に保つためのさまざまな方法があります。 最初の方法は、明示的な削除や上書きによってデータセットのサイズを管理することです。次は先ほど簡単に触れたTime to Live(TTL)、そして最後はEvictionです。

Thumbnail 2650

Thumbnail 2660

Thumbnail 2670

TTLは、アイテムに有効期限を設定して自動的に期限切れにすることで、キャッシュサイズを管理する最も一般的な方法です。ValeのTTLは各アイテムに明示的に割り当てる必要があり、グローバルやデフォルトのTTLは存在しません。個々のアイテムごとに設定する必要があります。 TTLは、EXPIREコマンドを使用して相対的に設定するか、EXPIREATコマンドを使用して固定時間で設定できます。TTLを使用する際のベストプラクティスの1つは、ランダムなジッターを追加することです。複数のアイテムが同時に期限切れになることは避けたいところです。なぜなら、先ほど話したThundering Herd問題を引き起こす可能性があるからです。ただし、この場合は単一のキーに対してではなく、様々なキーにまたがって発生するため、先ほど示した戦略で対処するのが難しくなります。これを避けるために、大量の期限切れが時間をかけて発生するようにジッターを追加することで、問題を軽減できます。次はEvictionについて話しましょう。

Thumbnail 2720

Evictionは、メモリが一杯になるか、ストレージサイズに達した時にキャッシュからアイテムを自動的に削除する方法です。Evictionポリシーに基づいて、2つの異なるEvictionプールを設定できます:サーバー内のすべてのアイテムから削除するか、TTLが設定された揮発性アイテムから削除するかです。ほとんどの場合、TTLが設定されたアイテムはTTLが発動する前に削除されても許容できるため、Eviction時の最初の削除候補となります。

Thumbnail 2750

Thumbnail 2760

これらのプールの中で、使用できるアルゴリズムが3つあります。 最も最近使用されていないものを削除するLeast Recently Used(LRU)、特定のアイテムの呼び出し頻度と回数を追跡するLeast Frequently Used(LFU)、 そして有効期限に基づく削除の3つです。

Thumbnail 2780

お客様からよく寄せられる質問の1つが、データベースの前段にキャッシュを配置する際のサイジングとその考慮点についてです。 これに関しては、ユースケースによって大きく異なるため、万能な解決策は存在しません。実際には、各アプリケーションに最適なキャッシュサイズを見つけるには、試行錯誤のプロセスが必要です。コストとキャッシュヒット率の間にはトレードオフが存在します。純粋なキャッシング用途では、ある一定のしきい値を超えてスケールアウトを続けると収穫逓減の法則が働き、むしろ小さめのサイズで若干低めのヒット率を維持する方が良い場合もあります。

Thumbnail 2840

先ほど見たようなリーダーボードの例のような、より永続的な使用ケースでは、データベース内のアイテム数に大きく依存するため、最適化がより難しくなります。実際の活用方法や、単一のデータストアに対してどれだけ多くの異なるワークロードの組み合わせを置くかによって変わってきます。 ここでのベストプラクティスの1つは、これらの多くを自動的に判断できるAuto Scalingを活用することです。Amazon ElastiCacheには2つのモードがあります:介入不要で自動的にスケールするElastiCache Serverlessと、CPUの使用率やデータストレージサイズなどの指標に基づいてApplication Auto Scalingを活用できるノードベースのElastiCacheです。

Thumbnail 2910

最後に、いくつかの使用上のベストプラクティスをご紹介します。先ほど述べたように、マイクロ秒レベルのレスポンスタイムが得られるものの、 よくある落とし穴の1つは、キャッシュへの接続を繰り返し行うことです。接続のハンドシェイク自体は、TCPハンドシェイク、場合によってはTLSや認証を含むため、比較的コストがかかります。コネクションプールを使用するか、アプリケーションの呼び出し間で接続を共有することで、長期的な接続を維持することが、全体的に最高のパフォーマンスを得られます。

Thumbnail 2940

レプリカからの読み取りは、スループットとレイテンシーの両方を改善するもう1つの方法です。 ElastiCacheでは、プライマリに対して最大5つのレプリカを持つことができます。すべてのノードにわたって線形にスループットをスケールでき、特定のアベイラビリティーゾーンにあるノードにアクセスできるため、より良いレイテンシーを得ることができます。プライマリは異なるアベイラビリティーゾーンにある可能性がありますが、レプリカは同じゾーン内に配置できるため、レイテンシーをミリ秒単位で削減できる可能性があります。

Thumbnail 2970

Thumbnail 3000

Thumbnail 3020

もう1つの重要な考慮事項は、実行するコマンドの計算複雑性を理解することです。KEYS * のようなコマンドは、データストレージ内のキーの数に対して線形的な関係にあり、非常に大きくなる可能性があります。先ほど説明した操作でも、特定のSet、List、またはHashの中のアイテム数に応じてスケールする可能性があります。大規模なマルチギガバイトオブジェクトを制限することも、もう1つのベストプラクティスです。例えば、Sorted Setのリーダーボードに何百万ものアイテムがある場合、それを複数のSorted Setに分割することで、複数のサーバーをより効率的に活用し、それらのアイテム間の同期オーバーヘッドを最小限に抑えることができます。以上で発表を終わらせていただきます。ご清聴ありがとうございました。


※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。

Discussion