Amazon Web Services ブログ

DynamoDB のスケーリング: パーティション、ホットキー、Split for heat がパフォーマンスに与える影響(第 2 部: クエリの実行)

このシリーズの第 1 部では、Amazon DynamoDB のデータローディング戦略と短時間実行時の DynamoDB の動作について学びました。この記事では、クエリのパフォーマンスと継続した負荷に対してDynamoDBはどのように対応するかについて学びます。

クエリの実行

任意に大規模なトラフィックを発生させ、現実の動作をシミュレートするために、複数のマルチスレッドクライアントが必要です。これらのクライアントは、各クエリはランダムに生成したIPアドレスを可能な限り速くリクエスト送信します。これらはテーブルが提供できるクエリ容量を消費し、残りはスロットリングされます。これを実現するために、同じ単純なクエリクライアントを実行する Amazon Elastic Compute Cloud (Amazon EC2) インスタンスの自動スケーリンググループを作成しました。

クエリテスト: オンデマンドテーブル、単一のパーティションキー値

最初のテストでは、単一の値を持つパーティションキーデザインを使用してデータをロードしたオンデマンドテーブルに対してクエリを実行します。前述のように、この単一パーティションキー値の使用はロード速度を遅くしました。それでは、このパーティションキーがクエリ速度にどのような影響を与えるか見てみましょう。

毎秒 6,000 回の読み取りが一定の速度で行われることが期待されます。データは 1 つのパーティションにあり、各パーティションは 毎秒最大 3,000 の読み取りユニットを持ち、結果整合性のあるクエリはそれぞれ 0.5 の読み取りユニットを消費します。したがって、数学的には毎秒 6,000 回のクエリルックアップを 1 つの非常にホットなスロットリングパーティションに対して達成するはずです。しかし、実際の結果はまったく異なります。

次の図 1 には、テスト開始時と最初の 10 分間の状況が表示されています。

図 1: 1 つのオンデマンドテーブル、1つのパーティションキー値のクエリテスト

図 1: 1 つのオンデマンドテーブル、1つのパーティションキー値のクエリテスト

図 1 では、このクエリが 4,500 の読み取りユニットを消費しており、これは秒間 9,000 回の結果整合性のあるクエリに相当します。これは期待を上回っています。発生していることは次のとおりです。各パーティションには、冗長性のためにデータが 3 つのノードに広く分散されています。リーダーノードはすべての書き込みを受け取り、2 つのフォロワーノードは迅速にそれに続きます。強力な整合性のある読み取りは常に最新のデータを取得するためにリーダーノードに送信されます。リーダーノードは毎秒 3,000 回の読み取りを処理できるため、パーティションは毎秒 3,000 回の強力な整合性のある読み取りを処理できます。結果整合性のある読み取りは 3 つのノードのいずれかに送られます。3 つのノードがすべてアクティブな場合、パーティションは理論的には毎秒 9,000 の読み取りを処理できます。これがここで見られる結果です。通常の運用中には、 3 つのノードのうち1つが短時間ダウンする場合があります。これは内部メンテナンスのためであったり、クエリに応答できないログレプリカに置き換えられたりします。そのような場合、パーティションは毎秒 6,000 回の結果整合性のある読み取りしか処理できず、これはまだ正常な動作と見なされます。そのため、時折毎秒 9,000 回の結果整合性のある読み取りで実行されることがあるとしても、パーティションが毎秒 6,000 回の結果整合性のある読み取りを維持できると仮定して設計するべきです。

連続してクエリを行うと、10 分後にスループットが急上昇する様子が図 2 に示されています。

図 2: テストを継続するとスループットが 2 倍に増加

図 2: テストを継続するとスループットが 2 倍に増加

スループットは正確に 2 倍に増加しました。DynamoDB は、単一のホットパーティションを検知し、それを 2 つの新しいパーティションに分割することを決定し、スループットキャパシティを 2 倍にしました(そして追加費用は発生しませんでした)。

DynamoDB にはアダプティブキャパシティと呼ばれる機能があり、その中で頻繁にアクセスされるアイテムを分離する機能をSplit for heat と呼ばれます。 DynamoDB は、あるパーティションが継続的に高い読み取りまたは書き込みスループットを受け取っているのを観測すると、そのパーティションを 2 つの新しいパーティションに分割することがあります。これにより、これらのアイテムが利用できる読み取りおよび書き込みキャパシティが 2 倍になります。

Split for heat のロジックは、最近のトラフィックパターンに基づいてソートキーの分割ポイントを選択し、その結果として得られる 2 つの新しいパーティションに均等に熱を分散させるように調整されます。分割ポイントはほとんど中央になることはめったにありません。IP アドレスのユースケースでは、分割は最もクエリを受けている IP 範囲を分離することを目的とします。

分割ポイントを選択する際、DynamoDB はテーブルがローカルセカンダリインデックス(LSI)を持っているかどうかを考慮する必要があります。もし LSI が存在する場合、分割ポイントはアイテムコレクション(同じパーティションキーを共有するアイテム)の間のみになります。LSI が存在しない場合、DynamoDB はアイテムコレクション内での分割が可能で、その際には分割ポイントの位置にソートキーの値が使用されます。これにより、同じパーティションキーを持つアイテムが、ソートキーの値に基づいて異なるパーティションに割り当てられる可能性があります。パーティションキーのハッシュはパーティションの配置の最初の要素を提供し、ソートキーの値はその配置をさらに微調整します。IP テーブルが LSI で構築されていた場合、単一のアイテムコレクションはさらなる分割ができないため、ここでの Split for heat はクエリのパフォーマンス向上に寄与しません。

Split for heat は読み取りと書き込みに適用され、高いトラフィックが継続するときはいつでも適用されます。実際、第 1 部で単一のパーティションキーを使用してランダムなCSVファイルをロードする際、ロードの終盤でパーティションの分割が観察されました。DynamoDB のこの機能は適応力がありますが、それによってクエリテストがより多くのパーティションで開始され、クエリテストに不当な影響を与える可能性があったため、ベンチマーク時にはそれを望まなかったのです。分割が発生しないようにするため、私はシーケンシャルな CSV ファイルからロードすることを選択しました。なぜなら、DynamoDB の Split for heat のロジックは(常に増加するソートキーの値を持つ単一のパーティションキーなど)、一部のソートキーの値に集中した負荷を検出することが難しく、分割が開始しないようになっているからです。これは timestamp をソートキーなどにした場合が顕著です。単一のパーティションキーに負荷は集中しているが、ソートキーは常に最も値が大きいものだけが書きこみされ一度の負荷しか掛かっていない為です。このような場合もし分割を行っても負荷が分散されません。

テストに戻りましょう。しばらくすると、次の図 3 で示されるように、スループットが再び2倍になります。

図 3: テストを続けると、さらに 2 倍になっている

図 3: テストを続けると、さらに 2 倍になっている

この時点で、データを持つ 2 つのパーティションは両方とも分割され、4 つのパーティションがクエリを処理するようになり、最大レートは 2 倍になりました。

安定した負荷のもとで合計 90 分間実行した後、グラフは次の図 4 のパターンを示しました。

図 4: 90 分間のクエリパフォーマンス

図 4: 90 分間のクエリパフォーマンス

図 4 には注目すべき点がたくさんあります。初期の読み取り速度はパーティションキャパシティの制限によって制約され、Split for heat することでキャパシティを増やすために繰り返しパーティションが追加されました。約 1 時間後、パーティションは限界近くまで分割されました。

分割プロセスの一環として、クエリレートに一時的な低下が生じることがあり、負荷が集中しても毎秒 6,000 回の結果整合性のある読み込みに抑えておき、時々 9,000 回付近まで上がることがあるということを織り込んだ設計にすべきというリマインダーです。また、旧パーティションから新パーティションへの切り替えの時点で、そのパーティションへの書き込み(または強力な整合性のある読み取り)に対して約1秒間の Internal Server Error 応答が発生するのは予想されるものです。これは、パーティションの旧リーダーノードが新しいリーダーノードに移るためです。

テストの最後の30分間には、スループットは最終的にテーブルレベルの読み取りスループット制限で 120,000 RCU に制約されました。すべてのアカウントには、テーブルに付与できるプロビジョニングされた読み取りキャパシティの制限があります(書き込みには別の制限があります)。これは、オンデマンドテーブルの最大スループットを制御するために暗黙的に使用されます。ほとんどの AWS リージョンでは、デフォルトで 40,000 読み取りユニットです。テストの前に、テストアカウントのテーブルレベルの読み取りスループット制限を 80,000 読み取りユニットに増やしました。これにより、テーブルの制限が発動する前に分割と倍増を観察する時間が増えました。80,000 読み取りユニットのテーブルで結果整合性のあるクエリを実行すると、上記で説明した 50 パーセントのブーストのおかげで、最大 120,000 読み取りユニットの定常状態を達成できます。

では、真ん中の異常なピークについて話しましょう。バーストキャパシティのおかげで、一時的にスループットがテーブルの制限を超えることがあります。これは、アダプティブキャパシティのもう一つの機能であるバーストキャパシティによって、テーブルがキャパシティを借りることができるからです(ベストエフォートベースで)。これが、220,000 読み取りユニット(毎秒 440,000 クエリ)までの大きなピークを作り出す原因です。パーティションはもはやボトルネックではなく、バーストキャパシティが提供および消費され始めました。

許容されるバーストキャパシティは有限であり(このテーブルの場合、80,000 * 300 = 24,000,000 読み取りユニット相当)、その後はテーブルレベルの読み取りスループット制限に基づいてトラフィックが制限されます。

もしクエリの負荷を制限以下に軽減していれば、バーストキャパシティは後で再び蓄積されるでしょうが、クエリレートは常にテーブルがサポートできる最大まで保たれていたため、ラインはテーブルレベルの読み取りスループット制限で平坦に続いています。

もし、強力な整合性のある読み取りを行っていた場合、毎秒 80,000 読み取りが限界です。結果整合性のある読み取りを行っているので、上のセクションで説明した仕組みで追加の 50 パーセントがあり、チャートが示すように、毎秒 120,000 読み取りユニットで実行することができます。

ここで興味深いのは、特別な計画なしに、パーティションキーの値が 1 つだけであり(ベストプラクティスのアドバイスに反して)、1 時間後にはインフラが毎秒 44 万クエリを処理でき、制限はアカウント関連のクォータによるものだけだったということです。アイテム単体のスループットと混同しないでください。ソートキーはランダムの値でクエリを実行しています。パーティションという粒度で負荷は分散されていることを注意してください。

クエリテスト: オンデマンドテーブル、複数のパーティションキー値

2 つ目のテストでは、ベストプラクティスのアドバイスに従い、複数のパーティションキー値を使用します。次に、新しいオンデマンドテーブルを使用してプロセスを開始します。図 5 がそれに続きます。期待される結果は何でしょうか?

図 5: 200 以上のパーティションキー値を使用したクエリテスト

図 5: 200 以上のパーティションキー値を使用したクエリテスト

直ちに、毎秒 15,000 読み取りユニット(30,000クエリ)という高いスループットを達成しました。これは以前の開始時点の 4 倍です。なぜなら、新しく作成されたオンデマンドテーブルに存在する 4 つのパーティションをすべて使用しているからです。トラフィックを高く保ち続けると、パーティションは分割され、その後もさらに分割されます。次に続くのが図 6 です。

図 6: パーティション分割でスループットが向上

図 6: パーティション分割でスループットが向上

パーティションキーが 1 つの場合と同じパターンですが、今回は分割可能なパーティションが 1 つではなく 4 つから始まっています。

最終的なグラフは最初のものと非常に似ていますが、ピークは1時間後ではなく 45 分後に来ています。次に続くのが図 7 です。

図 7:クエリを 1 時間実行した場合のスループット

図 7:クエリを 1 時間実行した場合のスループット

ここからの要点は、パーティションキーを良く分散させる方が良いということです。200 以上のパーティションキーを持つ方が、1 つだけの場合よりも高速にスケールアウトします。

また、アダプティブキャパシティのため、DynamoDB をベンチマークする際には、最初の 5 分で見えるものが 1 時間後にも同じとは限らないということです!

クエリテスト: 毎秒 100 万リクエスト

最後に、毎秒 100 万リクエストを目指したテストで締めくくります。

このために、テーブルに 500,000 の読み取りユニットをプロビジョニングします(これにはデフォルトのアカウントクォータの引き上げが必要です)。これは毎秒 100 万のクエリに十分以上のキャパシティを提供するはずです。テーブルをプロビジョニングしたままにするか、作成後にオンデマンドに切り替えるかは選択できます。ただし、オンデマンドテーブルはプロビジョニングされたテーブルよりもパーティションを積極的に分割する傾向があります。しかし、今回のテストではプロビジョニングされたままにしておきます。その理由の 1 つは、プロビジョニングされたスループットが 50 万読み取りユニットである赤いラインを示すことです。これは基本的には毎秒 100 万のリクエストを実現する目標となります。

次の図は、最初の 15 分間で観測された結果を示しています。

図 8: 500,000 RCU でプロビジョニングされたテーブルに対する、200 以上のパーティションキー値を持つ初期読み取りトラフィック

図 8: 500,000 RCU でプロビジョニングされたテーブルに対する、200 以上のパーティションキー値を持つ初期読み取りトラフィック

前述の図 8 に示されているように、最初はテーブルが約 225,000 読み取りユニットまたは毎秒 450,000 クエリを達成しました。テーブルレベルでスループットが制限されていなかったため、私たちの読み取りスループットを制限しているホットパーティションがいくつかあったと推測できます。データをより多くのパーティションキー値に広く分散させ、テーブルのパーティションにより均等にデータを配置するためのより良いメカニズムを見つけるべきでしょうか?理想的にはそうですが、試してみないとわかりません。

図 9: 15 分後の読み取りトラフィックが倍増

図 9: 15 分後の読み取りトラフィックが倍増

前述の図 9 は、Split for heat が 15 分後にスループットを倍増させ、消費された RCU が 470,000 個であることから、最終的には毎秒 940,000 の結果整合性のあるクエリを達成していることを示しています。毎秒 100 万クエリにほぼ達しています。さらに 1 時間経過すると、次に続く図 10 に示されている全体的なトラフィックパターンが生成されます。

図 10: 90 分間の読み取りトラフィック、毎秒 1,440,000 クエリの安定を達成

図 10: 90 分間の読み取りトラフィック、毎秒 1,440,000 クエリの安定を達成

これは、ほぼ毎秒 150 万クエリの定常状態です。テーブルレベルの読み取りスループット制限の結果、そこで止まったに過ぎないです。

レイテンシはどうでしょう?次の図 11 に示す Amazon CloudWatch のメトリクスで見ることができます。

図 11: CloudWatch のクエリレイテンシのメトリクス

図 11: CloudWatch のクエリレイテンシのメトリクス

CloudWatch は、毎秒 100 万以上のクエリを実行した場合でも、クエリあたり平均 1.72~1.88 ミリ秒と安定していることを報告しています。これはスケール時の一貫したパフォーマンスです。

クエリテスト:まとめ

DynamoDB は、パーティションが継続的な読み書きトラフィックを受ける場合、そのパーティションを分割することがあります。これにより、そのパーティション内のアイテムの利用可能なスループットが 2 倍になります。分割ポイントは、最近のトラフィックパターンに基づいて理想的な位置が計算されます。テーブルに LSI が存在する場合、分割ポイントはアイテムコレクション間でのみ設定できます。

プロビジョニングされたテーブルへのトラフィックは、読み取りまたは書き込みキャパシティの設定によって制限される可能性があります。オンデマンドテーブルは、テーブルレベルの読み取りおよび書き込みスループット制限に基づく暗黙の最大プロビジョニングキャパシティがあります。これらの制限は引き上げることができます。

バーストキャパシティにより、一時的にテーブルの制限を超えるトラフィックが可能となります。

高いカーディナリティのパーティションキーを使用すると、アイテムをパーティションにスムーズに割り当てることができ、すべてのパーティションがテーブルのスループットに寄与できます。低いカーディナリティのパーティションキーを使用すると、パーティション間での作業負荷が不均一になる可能性があります。このような状況では、Split for heat が特に有用です。ただし、LSI が存在する場合や、ソートキーが増加し続ける場合など、分割が有益でないと判断された場合は、Split for heat はアイテムコレクション内で行うことはできません。

この記事と第 1 部で説明した DynamoDB のスケーリング動作とベストプラクティスのヒントについては、ブックマーク可能なリファレンスガイド第 3 部をご覧ください。


作者情報

Jason HunterJason Hunter はカリフォルニアを拠点とする DynamoDB 専門のプリンシパルソリューションアーキテクト。2003 年より NoSQL データベースの開発に従事。Java、オープンソース、XML への貢献で知られる。

Vivek NatarajanVivek Natarajan は Purdue 大学で CS を専攻しており、AWS のソリューションアーキテクトインターン生である。

(本記事は 2023/01/30 に投稿された Scaling DynamoDB: How partitions, hot keys, and split for heat impact performance (Part 2: Querying) を翻訳した記事です。翻訳は SA 鈴木が担当しました。)