Amazon Web Services ブログ
大規模な Amazon DynamoDB テーブルに適切なシャード数の選択
一般的な設計のベストプラクティスとして、アプリケーションをテーブル内のすべての論理パーティションキーとインデックス全体での均一な読み込みと書き込みアクティビティのために設計することによって、Amazon DynamoDB のスループットキャパシティーを最大限に活用することができます。このような設計により、テーブルのキャパシティーを過剰に消費する可能性があるホットパーティションの発生を防ぐことができます。
書き込みアクセスをパーティション全体に均等に分散させる 1 つの方法は、パーティションキースペースを拡大させることです。この戦略は、パーティションキーに追加のサフィックスを付加して、基盤となるパーティション全体での配分を向上させるもので、書き込みシャーディングと呼ばれます。
しかし、このアプローチは、いくつかの興味深い設計面での疑問を生じます。いつシャーディングを検討すればよいのか? パーティションキーごとに何個のシャードを作成するべきなのか? シャードをいつ作成するのか? シャード数をスケールするにはどうすればよいのか? シャーディングは読み込みおよび書き込みパターンにどのように影響するのか?
この記事では、複合プライマリキー (パーティションキーとソートキー) が存在する DynamoDB テーブルのための動的な書き込みシャーディングのメカニズムを説明します。このメカニズムは、書き込みスループットの需要の増加に基づいてパーティションキーに新しいシャードを臨機応変に追加することによって、DynamoDB テーブルの書き込みキャパシティーを最適化できるようにしてくれます。
パーティション、キー、および書き込みシャーディング
DynamoDB は水平分散されたワークロードで能力を発揮します。DynamoDB はテーブルを作成するときに、プロビジョンドスループット要件に対応するために十分なパーティションをテーブルに割り当てます。作成時、DynamoDB はテーブルのキャパシティーを基盤となるパーティション全体に均等に分散します。DynamoDB は、テーブルが作成された後で追加のパーティションをテーブルに割り当てることもあります。詳細については、「パーティションキーを効率的に設計し、使用するためのベストプラクティス」と「パーティションとデータ分散」を参照してください。
DynamoDB は、パーティションに項目を分散するために一貫した内部ハッシュ関数を使用し、DynamoDB が項目を保存するパーティションは項目のパーティションキーによって決定されます。同一のパーティションキーを共有する項目のグループ (コレクションと呼ばれます) は、コレクションがパーティションのストレージ容量を超える場合を除き、同じパーティションにマップされます。
さらに、単一のパーティションが複数のコレクションに関連付けられた項目を保持する場合があります。同じパーティションにマップされた 1 つ、または複数のコレクションに対する過剰な書き込みアクティビティは、ホットパーティションの原因になります。ホットパーティションとは、集中的な読み込みおよび書き込みアクティビティが、パーティションのプロビジョンドキャパシティー、またはパーティションの最大キャパシティーを超え、キャパシティーエラーを生じる場合のことです。
これらのキャパシティーエラーは、プロビジョンドキャパシティーモデルにおける ProvisionedThroughputExceededException
、およびオンデマンドキャパシティーモデルにおける InternalServerError
によって識別されます。同じコレクションの項目が同じ基盤となるパーティションにマップされるため、これらのキャパシティーエラーは大規模なコレクションで発生する可能性が高くなります。詳細については、「エラー処理」および「読み込み/書き込みキャパシティーモード」を参照してください。
書き込みシャーディングとは、コレクションを効率的に DynamoDB テーブルのパーティション全体に分散させるメカニズムです。これは、パーティションキーに対する書き込み操作を複数のパーティションに分散させることによって、パーティションキーあたりの書き込みスループットを向上させます。このため、個々のパーティションキーの書き込みスループットが基盤となるパーティションのキャパシティーを超過してもよくなり、DynamoDB のパーティションレベルでのキャパシティーエラーが最小限に抑えられます。
さらに、書き込み操作を DynamoDB のパーティション全体に分散させることによって、パーティションのキャパシティーがより均等に使用されます。これはテーブルのキャパシティーのより効率的な使用につながり、コストが削減されます。
書き込みシャーディングは、パーティションキーにシンプルな値のサフィックスを付加することによって、クライアント側から実装することができます。サフィックスの付加による書き込みシャーディングは、パーティションキーに対する 1 バイトの変更でさえも内部ハッシュ関数での異なる出力の生成につながり、項目を異なるパーティションに置くために効率的です。詳細については、「書き込みシャーディングを使用してワークロードを均等に分散させる」を参照してください。
DynamoDB は、一時的な需要のバーストを緩和するバーストキャパシティー、および不均等なアクセスパターンに合わせてパーティション間のキャパシティーを再利用するアダプティブキャパシティーの機能をネイティブに提供します。書き込みシャーディングは、DynamoDB のパーティション全体にトラフィックを均等に分散させるための補完的なメカニズムです。
事前に設定された数のシャードを使った書き込みシャーディングの例
シャーディングすると良いテーブルの例は、ファイルシステムの監査ログです。ここでのパーティションキーはファイルパス、ソートキーはアクセスタイムスタンプになります。このファイルの人気が極度に上がり、頻繁に共有されるようになった場合、単一の DynamoDB パーティションが過剰な数の書き込みリクエストを受けます。
以下の表は、3 つの異なるシャーディングスキームに出現する同一の項目を示しています。
- 最初のメソッドでは、データが 1 つのシャード内に置かれています。
- 2 番目のメソッドでは、ライターが 10 個のシャードについて 1 から 10 のランダムな数値を選択し、項目を更新する前にそれをサフィックスとしてパーティションキーに付加します。10 個のシャードそれぞれに対して、パーティションキーの後続読み込みを行う必要があります。
- 3 番目のメソッドでは、シャード数を決定するために、ライターがソートキーと連結されたパーティションキーをハッシュし、事前に設定されたシャード数 (10 個) に対してハッシュのモジュロを計算します。
このメソッドは、ファイルパスとアクセス時間に基づいて監査項目を検索するためのクエリを最適化するときに便利です。例えば、特定の時間におけるファイルへのアクセスに関する情報を保存している監査項目を検索するためにクエリするシャードを特定するには、項目を検索するために 10 個のシャードすべてをクエリするのではなく、ファイルパスとタイムスタンプャードのハッシュを計算することができます。
メソッド | メソッドの説明 |
パーティションキー | ソートキー | シャードの数 | シャードアルゴリズム | サンプルパーティションキー | サンプルソートキー (アクセスタイムスタンプ) |
1 | 元の項目 | <ファイルパス> | <タイムスタンプ> | 1 | なし | /shared/firetvGen2.txt | 123456789101 |
2 | ランダムな書き込みシャード | <ファイルパス>_<シャード> | <タイムスタンプ> | 10 | RANDOM(1、10) | /shared/firetvGen2.txt_3 | 123456789101 |
3 | 計算された書き込みシャード | <ファイルパス>_<シャード> | <タイムスタンプ> | 10 | md5(<File Path> + timestamp) % 10 + 1 | /shared/firetvGen2.txt_6 | 123456789101 |
パーティションキーのためのシャード数の判断
前述の例は、パーティションキーに事前に設定された数のシャードを使用します。この戦略は、各パーティションキーの読み込みおよび書き込みキャパシティーのニーズと、将来的な増加がよく理解されている場合に効果的ですが、必ずしもそうとは限りません。ユースケースには、各パーティションキーに可変の非決定的な読み込みおよび書き込みパターンがあるものもあります。
DynamoDB からのフィードバックに基づいてパーティションキーの新しいシャードを自動的に追加できる動的シャーディングのメカニズムでは、シャードのアンダーサイジングを防ぎ、実質的に制限なくスケールすることが可能になります。詳細については、Amazon DynamoDB Auto Scaling: 規模を問わないパフォーマンスとコストの最適化を参照してください。
動的シャーディングのメカニズム
動的シャーディングのメカニズムには、各パーティションキーに関連するシャードメタデータを追跡するための個別の DynamoDB テーブルを作成します (作成後はパーティションキーメタデータテーブルと呼ばれます)。アプリケーション内の DynamoDB リーダーとライターが、メタデータテーブルをクエリすることによって、所定のパーティションキーの現在のシャード数を判断します。
メタデータテーブルには、各項目に以下のスキーマがあります。
タイプ | 説明 | データタイプ |
パーティションキー: file_path |
値はメインテーブルのパーティションキーと同じ | タイプはメインテーブルのパーティションキーと同じ |
属性 1: number_of_shards |
シャードの数 | 数値 |
属性 2: last_updated |
最後に更新されたシャード数 | 数値 |
属性 3: shard_history |
シャード履歴 | StringSet |
メイン DynamoDB テーブルは複合プライマリキーを使用しますが、パーティションキーメタデータテーブルはメインテーブルからのパーティションキーのみをプライマリキーとして使用します。これは、パーティションキーメタデータテーブルにある各項目がメインテーブルのコレクションを表すため、メインテーブルよりも小さいメタデータテーブルにもつながります。
以下の表は、前のセクションの監査ログの例を使用した、パーティションキーメタデータテーブル内のサンプル項目を表しています。
file_path | number_of_shards | last_updated | shard_history |
/shared/firetvGen2.txt | 2 | 1562858912 | { “1561758912:1”, “1562858912:2” } |
ここでは、number_of_shards
属性に記載されているように、パーティションキー /shared/firetvGen2.txt
に 2 個のシャードがあります。
Last_updated
属性には、シャード数が最後に更新されたときのタイムスタンプ (エポックタイムとして表示) があります。last_updated
は、パーティションキーのシャード数の更新にクールダウン期間を施行するために役立ちます。シャード数を増やす前に、ライターは最後にシャード数が更新された時間をチェックする必要があります。最後の更新からの経過時間 (現在の時間とメタデータテーブルからの時間の差として計算) がクールダウン期間 (事前に設定) よりも短い場合、シャードの数が最近更新されたことから、メタデータテーブルを更新する代わりに、メインテーブルで書き込み操作が再試行されます。
Shard_history
属性は、合理的なシャード更新の回数のためにタイムスタンプとシャード数を保持します。この属性のデータタイプは、文字列のセットを保持するための DynamoDB StringSet です。エントリの値は { “1561758912:1
”、“1562858912:2
” } に似た値になり、セット内の各文字列はエポックタイムの値と、コロンで区切られたシャード数を表しています。
シャード履歴を保持するメリットは 2 つあります。まず、シャードの最近の履歴をクエリしてシャード数の増加が速すぎるか遅すぎるかを見極めることによって、シャードを追加するロジックをより高度にすることができます。次に、パーティションキーに対するスループット需要が時間と共に減少し、読み込みのコストを最適化する必要がある場合は特に、将来のシャード数を削減するとよいかもしれません。
ライターの動作
メインテーブルに書き込む前に、ライターはまずパーティションキーメタデータテーブルをクエリしてパーティションキーのシャード数 (number_of_shards
属性) を取得します。パーティションキーメタデータテーブルが項目を返さない場合 (ライターが新しいパーティションキーでメインテーブルに項目を挿入しようとしていることを示す)、ライターは、number_of_shards
を 1 とし、他の属性は前の表で説明したスキームに従って、パーティションキーメタデータテーブルに新しい項目を挿入します。
パーティションキーメタデータテーブルに項目が存在する場合、ライターがシャード数を判断し、書き込むシャードをシャードスペース (例えば、5 個のシャードのシャードスペースは 1~5) からランダムに選択して、書き込む前にシャード数をパーティションキーに付加します。
ライターが、メインテーブルへの書き込み中にパーティションキャパシティーエラーを受け取った場合、ライターは、前のセクションで説明したように、シャード数 (number_of_shards
) を増分する、現在のタイムスタンプ (last_updated
) を挿入する、および shard_history
を更新することによってメタデータテーブルの項目を更新します。ライターはその後、パーティションキーに新しいシャード数を付加することによって、メインテーブルでの項目の書き込みを試行します。これに続いて、ライターによる新しい書き込みのすべてが、パーティションキーのより大きなシャードされたスペース全体に分散されます。パーティションキャパシティーエラーの詳細については、以下の「パーティション vs. テーブルキャパシティー制約の検知」セクションを参照してください。
プロビジョンドキャパシティーのテーブルでは、エラーがテーブルキャパシティーに関連する場合、スロットリングへの対応として、UpdateTable
オペレーションを使用してテーブルのプロビジョンドキャパシティーを増加させることができます。詳細については、DynamoDB 開発者ガイドの「UpdateTable」を参照してください。DynamoDB Auto Scaling が設定されている場合は、これがテーブルの書き込みキャパシティーを自動的に更新します。詳細については、「DynamoDB Auto Scaling によるスループットキャパシティーの自動管理」を参照してください。
リーダーの動作
リーダーからメイン DynamoDB テーブルに対するクエリは、いずれも所定のパーティションキーのすべてのシャードをクエリする必要があります。リーダーはまず、パーティションキーのパーティションキーメタデータテーブルからシャード数 (number_of_shards
) を取得し、クエリを各シャードのメインテーブルに対して行います。
メインテーブルでの読み込み操作で発生する 1 つ、または複数のキャパシティーエラーは、制約付きテーブルまたはパーティションの読み込みキャパシティーに関連している場合があります。プロビジョンドキャパシティーのテーブルについては、テーブルキャパシティーが制約されている場合、スロットリングへの対応として、UpdateTable
オペレーションを使用してテーブルの読み込みキャパシティーユニットを更新できます。DynamoDB Auto Scaling が有効化されている場合は、これがテーブルの読み込みキャパシティーを自動的に更新します。
しかし、1 つ、または複数のパーティションキャパシティーが制約されており、テーブルに十分な読み込みキャパシティーがある場合は、テーブルの読み込みキャパシティーの増加は、パーティションの最大読み込みキャパシティーまでしか有効になりません。この場合は、グローバルセカンダリインデックス (GSI) テーブルの使用とキャッシングが、メインテーブルの読み込みをスケールするための効果的な戦略です。詳細については、「グローバルセカンダリインデックス」を参照してください。
読み込みスループットをスケールするには、メインテーブルと同じスキーマを持つ GSI を効果的に使用することができます。1 つ、または複数の GCI を作成し、メインテーブルと GSI における読み込み操作のバランスを取ることによって、利用可能な読み込みスループットを増やすことができます。さらに、Amazon DynamoDB Accelerator、Amazon Elasticache、またはパーティションキーメタデータテーブルのコピーを保持するための第 1 階層のローカルキャッシュの使用を検討してください。
パーティション vs. テーブルキャパシティー制約の検知
パーティションキャパシティーエラーは、パーティション上のキャパシティーリクエストがそのプロビジョンドキャパシティー、またはパーティションの最大キャパシティーを超える場合に発生します。エラーが発生するときのパーティションキャパシティーエラーの有力な指標は、テーブルで消費されたキャパシティーのための Amazon CloudWatch メトリクス (ConsumedReadCapacityUnits
または ConsumedWriteCapacityUnits
) がテーブルのプロビジョンドキャパシティー (ProvisionedReadCapacityUnits
または ProvisionedWriteCapacityUnits
) よりも低いことです。書き込みまたは読み込みエラーが生じているアプリケーションは、CloudWatch をクエリすることでこれらのメトリクスを比較できます。十分なテーブルキャパシティーがある場合は、ホットパーティションが同じパーティションにマップされた 1 つ、または複数のパーティションキーに対するリクエストを増加させている可能性が高くなります。詳細については、「DynamoDB メトリクスとディメンション」を参照してください。
パーティションキーメタデータテーブルの読み込みのスケーリング
リクエストフローに個別のパーティションキーメタデータテーブルを導入すると、追加の依存性がアプリケーションに追加されるので、これを考慮する必要があります。特に、メインテーブルに対するリーダーとライターの両方がパーティションキーメタデータテーブルをクエリして、パーティションキーのシャード数を判断する必要があります。このため、パーティションキーメタデータテーブルでの読み込みのスケーリングを検討することが重要になります。
パーティションキーメタデータテーブルは、独自にキャパシティーを管理する必要をなくし、運用上の負担を減らすためにも、オンデマンドキャパシティーを使用することが理想的です。さらに、GSI およびキャッシングメカニズムの使用といった、先ほど説明したメインテーブルの読み込みキャパシティーのスケーリングのためのテクニックも検討する必要があります。
パーティションキーメタデータテーブルの更新
パーティションキーメタデータテーブルの更新を試行する可能性がある複数のライターが存在する競合システムがある場合、クールダウンロジックの実装をさらに検討する必要があります。複数のライターがクールダウン期間の終了後すぐにメタデータテーブルの更新を試行するという状況を避けるために、ライターがメタデータテーブルの更新を試行する前のランダムバックオフを実装することができます。
例えば、各ライターが 1 秒から 10 秒のランダムな数値を選択し、メタデータテーブルの更新を試行する前に、クールダウンタイマーが切れてから 0 までカウントダウンします。これにより、シャード数の更新試行の頻度が低減されます。
さらに、DynamoDB は、複数のライターが同じ項目を同時に更新することを防ぐための楽観的ロックの組み込みメカニズムを提供します。パーティションキーのシャード数を更新する前に、ライターがそのパーティションキーを持つメタデータテーブルをクエリし、last_updated
値を取得します。シャード数を増加させるには、ライターは条件付きの UpdateItem
API コールを使用する必要があります。詳細については、DynamoDB 開発者ガイドの「条件付き更新」を参照してください。
条件が、以前取得したタイムスタンプの値に対する等価性について last_updated
属性をチェックします。これにより、複数のライターがメタデータテーブルの項目を無節操に更新しないことが確実になります。
例えば、項目例の表で説明されている項目を更新するには (last_updated
と shard_history
属性の更新を含む)、ライターは 1562858912
のタイムスタンプ値に対する last_updated
の等価性をチェックする条件式と共に UpdateItem
API を使用することができます。
まとめ
この記事では、キースペース全体における非決定的、または偏った書き込み分散があるアプリケーションのために DynamoDB テーブルの書き込みキャパシティーを最適化するシャーディングソリューションと設計上の考慮事項について説明しました。動的シャーディングのメカニズムは、項目を DynamoDB パーティション全体に分散させることを確実にしながら、パーティションキーの書き込みスループットが経時的に増加し、実質的に制限なくスケールすることを可能にします。
スケールのために設計された他の分散型システムと同様に、動的シャーディングの実装に選択するパラメータは、アプリケーションのアクセスパターンに応じて異なります。これらを念頭に置いて、より大きな DynamoDB コミュニティの利益となるように、コメントセクションで実装から学んだ事柄を共有してください。
著者について
Anuj Dewangan は、アマゾン ウェブ サービスのシニアソリューションアーキテクトです。 Anuj は、AWS における最大規模の戦略的なお客様が、AWS において分散型システムとアプリケーションをウェブスケールで設計、デプロイ、および運用する支援をしています。
Sean Shriver は、アマゾン ウェブ サービスのシニア NoSQL ソリューションズアーキテクトです。 Sean は、Amazon DynamoDB の移行、設計レビュー、AWS SDK 最適化、および概念実証において、知名度の高い戦略的なお客様を支援しています。