【AWS Database Blog】DynamoDB におけるパーティションキー設計の手引き
こんにちは。ソリューションアーキテクトの江川(@daiti0804)です。本日は、AWS のソリューションアーキテクトであるGowri Balasubramanian が、AWS Database Blogに投稿したChoosing the Right DynamoDB Partition Key をご紹介します。
このブログ投稿では、リレーショナルデータベースから DynamoDB へ移行するにあたって、適切なパーティションキーを選択するための重要な考慮事項と戦略を説明します。これはDynamoDB を利用するスケーラブルで信頼性の高いアプリケーションの設計と構築において重要なステップです。
パーティションキーとは
DynamoDB では二種類のプライマリキーをサポートします:
- パーティションキー(Partition key): ハッシュキー(以前の名称)としても知られていますが、パーティションキーは、単一のattribute(属性)で構成されます。DynamoDB 内のattributeは、多くの点で他のデータベースシステムのフィールドや列に似ています。
- パーティションとソートキー(Partition key and sort key): 複合プライマリキーもしくはハッシュ-レンジキーと呼ばれるこのキーのタイプは、2 つのattributeで構成されます。最初のattributeはパーティションキーであり、2 番目のattributeはソートキーです。以下に例を示します。
図1 複合プライマリキーをもつDynamoDBテーブル
なぜパーティションキーが必要なのか
DynamoDB は、Item(項目)と呼ばれる attribute のグループとしてデータを格納します。Item は、他のデータベースシステムの行、レコードと似ています。DynamoDB は、ユニークであるプライマリキーの値に基づいて各 Item を格納、取得します。 Item はパーティション(DynamoDB内部の物理ストレージ)と呼ばれる10 GBのストレージユニットに分散されます。図2に示すように、各テーブルには1つ以上のパーティションがあります。詳細については、DynamoDB開発者ガイドの「パーティションの動作について」を参照してください。
DynamoDB は、パーティションキーの値を内部でハッシュ関数の入力として使用します。 ハッシュ関数の出力によって、Item が格納されるパーティションが決定されます。各 Item の格納先はパーティションキーのハッシュの値によって決まります。
同じパーティションキーを持つすべての Item は同じパーティションに格納され、さらに複合プライマリキーの場合、 Item はソートキーの値でソートされます。もし 10 GB を超える場合、DynamoDB は、ソートキーによりパーティションを分割します。
図2 DynamoDB テーブルとパーティション
パーティションキーとリクエストスロットリング
DynamoDB はプロビジョニングされたスループット(読み込みキャパシティと書き込みキャパシティ)を各パーティション間で均等に割り当てます。 したがって、DynamoDB 全体に指定したプロビジョンドキャパシティは、パーティションごとにスループットが制御されます。もし、読み込みもしくは書き込みのスループットが、単一のパーティションのキャパシティを超えた場合は、そのリクエストは ProvisionedThroughputExceededexception の例外でスロットリングされるでしょう。
読み込み/書き込みの超過は、以下の原因によって発生する可能性があります。
- 不適切なパーティションキーを選択したことによる不均等なデータ分散
- パーティション内の同じキー(ホットキーと呼ばれる最も呼び出されるItem)への頻繁なアクセス
- プロビジョニングされたスループットよりも高いリクエストレート
リクエストスロットリングを避けるには、アクセス要件を満たし、均等にデータを分散させるためにDynamoDB テーブル設計において適切なパーティションキーを選択する必要があります。
パーティショキー設計の推奨アプローチ
高いカーディナリティを持つ attribute を使用する。つまり、e-mail id, employee_no, customerid, sessionid, ordered などの各 Item ごとに異なる値を持つ attribute を利用することです。
複合プライマリキーを利用する。アクセスパターンに合致する場合、複数のattributeを組み合わせてユニークキーを構成するようにしましょう。たとえば、customerid + productid + countrycode がパーティション・キーで、order_dateがソート・キーである orders テーブルです。
大量の読み取りトラフィックがある場合は、頻繁にアクセスされる Item をキャッシュします。キャッシュはローパスフィルタとして機能し、非常にアクセスが多い Item を埋もれたパーティションから読み取らないようにします。たとえば、商品の取引に関する情報を持つテーブルを考えてみましょう。ブラック・フライデーやサイバー・マンデーのような大規模なセールス・イベントでは、セール対象の商品は、他の商品よりも多くのアクセスが予想されます。
書き込みの多いユースケースの場合は、あらかじめ決められた範囲の乱数/数字を追加します。パーティションキーに大量の書き込みが必要な場合は、プレフィックスやサフィックス(あらかじめ指定された範囲の固定数、たとえば1〜10)を使用して、パーティションキーに追加します。たとえば、請求書トランザクションのテーブルを考えてみましょう。単一の請求書には、クライアントごとに数千のトランザクションが含まれることがあります。大規模なクライアントの請求書の詳細を照会/更新する場合の一意性と能力をどのように担保すれば良いでしょうか。
このシナリオで推奨されるテーブルレイアウトは以下のとおりです。
- パーティションキー: 請求書番号(InvoiceNumber)ごとの Item 数に応じて、ランダムなサフィックス (1-10 or 1-100) をつけた請求書番号
- ソートキー: クライアントのトランザクションID(ClientTransactionid)
Partition Key Sort Key Attribute1 InvoiceNumber+Randomsuffix ClientTransactionid Invoice_Date 121212-1 Client1_trans1 2016-05-17 01.36.45 121212-1 Client1-trans2 2016-05-18 01.36.30 121212-2 Client2_trans1 2016-06-15 01.36.20 121212-2 Client2_trans2 2016-07-1 01.36.15 - この組み合わせにより、各パーティションを有効利用できます。ソートキーは、特定のクライアントをフィルタするために利用できます(例えば、InvoiceNumber=121212-1 かつ ClientTransactionid が Client1 で始まるもの)。
- パーティションキーに付加された乱数(1-10)のため、InvoiceNumber に対して 10回のクエリを行う必要があります。パーティションキーは、121212-[1-10]となるので、パーティションキーが 121212-1 かつ ClientTransactionid が Client1 で始まるという条件でクエリをかける必要があります。このアプローチを、121212-2 から 121212-10 まで繰り返し、結果をマージする必要があります。
乱数(1-10)を利用する代わりの方法もあります: 常に Invoice_Date を使用してクエリ結果をフィルタしている場合、InvoiceNumberにInvoice_Date の一部(曜日、月、年(DDMMYYYY)など)を追加して書き込みに際してのパーティションの有効活用ができます。もし、2016年9月(SEP)のClient1 に対する請求書のリストが必要な場合、InvoiceNumber = 121212-01SEP2016〜121212-30SEP2016 かつ ClientTransactionidがClient1で始まるという条件で、30回クエリを発行する必要があります。一方、アクセスパターンとして、特定の日付の請求書を確認する場合は、パーティションキーを使用して直接照会することができます。
Note:
プレフィックスの範囲を決めると、 その後、その範囲の拡張することは容易ではありません。というのも、プレフィックスの変更はアプリケーションレベルでの変更が必要になるからです。それゆえ、予期される将来の成長に対応するために、各パーティションキーがどの程度ホットになり、(バッファも含めて)十分なランダムプレフィックスをつけるよう検討しましょう。
この方法をとると、クエリごとに X回の読み込みリクエストが発生するので、読み込みに追加のレイテンシーがかかります。
大小のユースケースごとにテーブルを分割する。パーティションキーに対して、多数の Item がある場合は、パーティションキーに乱数を使用するのが効果的です。パーティションキーに小さい Item と大きい Itemが混在している場合はどうなるでしょうか?たとえば、パーティションキーごとに数万のアイテムを持つ顧客/製品が1つあり、パーティションキーごとにわずか数のアイテムを持つ顧客がある場合はどうなるでしょう?このシナリオでは、小さなプレフィックスを使用することは、小規模な顧客にとって効果的ではありません。
また先ほどと同様に InvoiceTransactions テーブルを考えてみましょう。クライアントの情報を事前に知っていて、その規模感を分類できるものとします。小規模なお客様の請求には、10の取引があり、大規模なお客様は 10,000件の取引があります。
その場合、以下のようにテーブルを分割するアプローチが推奨されます。
Table Name | Partition Key | Sort Key |
---|---|---|
InvoiceTransactions_big | InvoiceNumber+Randomsuffix | ClientTransactionid |
InvoiceTransactions_small | InvoiceNumber | ClientTransactionid |
この場合、ランダムプレフィックスはクライアントごとに数万の請求取引が予想される大きいテーブルにだけ適用すればよいでしょう。小さなテーブルに対してのクエリは、複数の API コールを必要ありません。
パーティションキー設計のアンチパターン
DB エンジンで生成されるシーケンスやユニークID をパーティションキーとして使用する。Oracle DB のテーブルのようにプライマリキーとしてシーケンス(schema.sequence.NEXTVAL) を利用して、一意性を確保しようとするのはよくあることです。これらはデータアクセスには利用されません。
下記は、Oracle DB からDynamoDBに移行された注文テーブルのスキーマレイアウトの例です。メインテーブルのパーティションキー(TransactionID)にはUIDが設定されます。クエリを実行するため、OrderIDとOrder_Dateにグローバルセカンダリインデックス(GSI)が作成しています。
Partition Key | Attribute1 | Attribute2 |
---|---|---|
TransactionID | OrderID | Order_Date |
1111111 | Customer1-1 | 2016-05-17 01.36.45 |
1111112 | Customer1-2 | 2016-05-18 01.36.30 |
1111113 | Customer2-1 | 2016-05-18 01.36.30 |
このアプローチには潜在的な問題があります:
- TransactionID をクエリの目的で利用することができません。したがって、データの高速な読み取りのためにパーティションキーを使う能力を失っています。
- GSI は、結果整合性のみをサポートし、読み込みと書き込みに追加のコストが発生します。
Note: シーケンスの代わりに、一意性を担保し、Item の上書きを防止するために条件付き書き込みを利用できます。
低いカーディナリティの attribute をパーティションキー(例.product_code)やソートキー(例.order_date)として利用する。
- この設計により、ホットパーティションとなる可能性が大幅に高まります。たとえば、ある製品が非常に人気がある場合、そのキーの読み込みと書き込みが高くなり、スロットリングの問題が発生します。
- Scan を除いて、DynamoDB API操作では、テーブル、GSIのパーティションキーに等価演算子(EQ)が必要です。結果として、パーティションキーは、簡単なルックアップを使用してアプリケーションによって簡単に照会されるものでなければなりません(たとえば、key = valueを使用して、一意の Item または複合プライマリキーの場合は少ない Item を返却)。一回の Query 操作でフェッチできる Item は1 MBという制限があります。つまり、最適な選択肢とは言えない LastEvaluatedKeyを使用したイテレートアクセスが必要となってしまいます。
DynamoDBに格納する前にcustomer_idのようなキーに対して独自にハッシュ化しないでください。 DynamoDBはキーをハッシュし、パーティション間で配布します。ダブルハッシングは、単にお客様のアプリケーション設計を複雑にするだけです。
DynamoDBテーブルのデータモデルとアクセスパターンを分析せずに、移行元のデータベースからプライマリキーをそのまま持ってくることは避けましょう。
まとめ
DynamoDB のパーティションキー戦略を考えるにあたって、すべてのユースケースにあてはまる唯一のソリューションはありません。格納したデータとアクセスパターンに基づいて、様々なアプローチを検証すべきです。そして、スロットリングを起こす可能性が最も小さい適切なキーを選択してください。
参考
テーブル内のすべての項目に対して均一なデータアクセスを実現する設計 - DynamoDB 開発者ガイド
コメント