Tablestore は、パーティションキーの範囲に基づいてデータを自動的に複数のパーティションに分割します。パーティションキーの選定が不適切だと、ホットスポット、データスキュー、またはスケーリングのボトルネックにつながります。プライマリキー設計、属性列設計、テーブル分割のベストプラクティスに従って、高性能でスケーラブルなデータテーブルを設計してください。
プライマリキー設計
Tablestore は、パーティションキー (最初のプライマリキー列) の範囲に基づいてデータを自動的に複数のパーティションに分割し、サービスノードに分散します。データとアクセス負荷を均等に分散し、ホットスポットを回避できるようにプライマリキーを設計してください。
パーティションキー設計の原則
パーティションキーは、データテーブルにおける最初のプライマリキー列です。Tablestore は、パーティションキーの範囲に基づいてデータを自動的に複数のパーティションに分割し、各パーティションを別々のサービスノードに割り当てます。パーティションキーを設計する際は、次の原則に従ってください:
単一のパーティションキー値配下のデータ量を 10 GB 未満に抑えます。
異なるパーティションキー値配下のデータは、論理的に独立させます。
連続するパーティションキー値の狭い範囲にアクセス負荷が集中しないようにします。
ワークロードが低い場合 (TPS/QPS が 1,000 未満、データ量が 10 GB 以内、かつ大幅に増加する見込みがない場合)、ホットスポットの影響は最小限となり、単一パーティションで負荷を処理できます。ただし、単一パーティションの処理能力に依存するアーキテクチャは設計しないでください。
データ分散
分散データシステムでは、データ分散が不均一だと次の問題が発生します:
読み取り/書き込みスループットが単一パーティションに制約され、明確なボトルネックが生じます。
ホットスポットの分布が不均一だとロングテールが発生し、全体の処理が遅くなります。
ホットスポットのパーティションがビジネスパイプライン全体のボトルネックとなり、上流/下流システムに影響を及ぼします。
データがパーティション全体に均等に分散されると、読み取り/書き込み負荷はすべてのパーティションに分散されます。各リクエストはローカルデータのみを対象とし、リソースを追加することで水平スケールできます。
代表的なホットスポット問題と解決策
監視シナリオとして、各マシンの各時点のメトリック値を格納するデータテーブルを考えます。プライマリキー設計は次のとおりです:
|
Timestamp (パーティションキー) |
MachineIp |
問題 |
|
|
|
すべての書き込みがテーブル末尾 (最後のパーティション) に追記され、テールホットスポットが発生します。 |
|
|
|
|
|
|
|
データはパーティションキーの範囲で分割されるため、すべての書き込みが最後のパーティションに集中し、パーティション分割では解消できないテールホットスポットが発生します。
アプローチ 1:パーティションキーの並べ替え
MachineIp を最初のプライマリキー列に移動し、Timestamp を 2 番目にします。調整後のプライマリキーは MachineIp = "10.10.0.1", Timestamp = 1718000001 になります。これにより、書き込み負荷はマシン単位でパーティションに分散されます。UserId、DeviceId、OrderId など、自然に分散される他のフィールドもパーティションキーとして同様に有効です。
特定の IP 範囲からの書き込みが単一パーティションに集中する場合、Tablestore はそのパーティションを自動的に分割し、複数のパーティションに負荷を分散します。
アプローチ 2:MD5 プレフィックスの付与
MachineIp の MD5 ハッシュを計算し、先頭 4 文字の 16 進文字列を IP の前に付与してパーティションキーにします。たとえば、10.10.0.1 の MD5 の先頭 4 文字が a1b2 の場合、パーティションキーは a1b2,10.10.0.1 になります。この方法により IP 範囲の連続性が崩れます。また、16 進プレフィックスはシステムの事前分割にも役立ちます。
アプローチ 3:グローバルな順序付けが必要な場合の代替案
ビジネス要件として時間でグローバルに順序付けされたクエリが必要で、かつテールホットスポットを回避したい場合は、次の代替案を検討してください:
ローカル順序:分散キーとなるフィールドを 1 列目に配置します。同じ分散フィールド値を持つデータは、時間順のまま保持されます。たとえば、特定の MachineIp 配下のデータは Timestamp により順序付けられたままです。
バケットベースの書き込み:時刻値の N による剰余を最初のプライマリキー列 (0~N-1) とし、時刻を 2 列目にします。たとえば 16 バケットの場合、書き込みのパーティションキーは
timestamp % 16 = 3になります。読み取り時は 16 バケットすべてを並列にクエリし、結果をマージします。バケット数を増やすほど負荷は均等に分散されますが、読み取り時の並列クエリ数が増えます。検索インデックス:データが均等に分散されるようにデータテーブルに書き込み、検索インデックス を使用して、時間や他のフィールドで順序付けされたクエリを実行します。検索インデックスはデータを複数のシャードに自動的に分散し、クエリ時に結果をマージします。
プライマリキー列長とデータ量の制約
単一のパーティションキー値配下のデータ量を 10 GB 以内に抑えます (ハード制限はありません)。同一のパーティションキー値を持つ行は、それ以上分割できません。
プライマリキー列長の上限は 1 KB です。プライマリキーを短くすると、クエリパフォーマンスが向上します。
列の連結によるパーティションキーの作成
単一のパーティションキー値配下にあるすべての行の総データ量が 10 GB を超える可能性がある場合は、複数のプライマリキー列を連結して新しいパーティションキーを作成し、データを分散します。連結する際は、次のルールに従ってください:
連結後のパーティションキーは、元々同じパーティションキー値を共有していたレコードを、異なるパーティションキー値に効果的に分割できる必要があります。
整数のプライマリキー列は、先頭に 0 を付与してソートの整合性を維持します。たとえば、
OrderNumber = 123は 0 埋め後に00000123となり、辞書順が数値順と一致します。-
新しいパーティションキーの辞書順を維持するために、データ内のすべての文字より ASCII 値が小さいデリミタ文字を選択します。具体的な選択は、ビジネスデータの文字セットに依存します。たとえば SellerID の値に数字と英字が含まれる場合 (
a100やa1001など)、デリミタの違いによってソートは次のように変わります:デリミタ
連結後のソート順
理由
結果
:000054:a1001は000054:a100:より前にソートされます:の ASCII 値は数字より大きいため、共通プレフィックス後の比較で、a1001の1が:よりも小さいと判断されてしまいます。誤ったソート
,000054,a100,は000054,a1001より前にソートされます,の ASCII 値は数字と英字のいずれよりも小さいため、辞書順が元データの順序と一致します正しいソート
パーティションキーへのハッシュプレフィックスの追加
順次増加する列をパーティションキーとして使用する必要がある場合は、パーティションキーにハッシュプレフィックスを付与し、テーブル内で隣接データの分布をランダム化してアクセス負荷を均等に分散します。
ハッシュプレフィックスを追加すると、これまで連続していたデータは分散されます。論理的に連続するデータを範囲読み取り操作で読み取ることはできなくなります。範囲クエリの代替として検索インデックスを使用してください。
属性列設計
行幅の制御:Tablestore はワイド行 (最大で数十万の属性列) をサポートしますが、極端にワイドな行を 1 回のリクエストで読み取るとタイムアウトする可能性があります。1 行あたりの属性列数は 1 万未満に抑えてください。列名指定またはページネーションを使用してワイド行を読み取ってください。
属性列サイズの上限:単一の属性列の値は 2 MB を超えられません。この上限を超えるデータは複数の列に分割するか、Object Storage Service (OSS) に保存してください。
アクセス頻度によるテーブル分割:1 行に多数の属性列があり、アクセス頻度が大きく異なる場合は、高頻度列と低頻度列を別テーブルに保存してください。たとえば商品管理システムでは、商品数量や価格は頻繁にアクセスされる一方、商品説明 (大きなテキスト) はアクセス頻度が低いことがあります。2 つのテーブルに分割してください。
大きなテキストの圧縮:大きな属性列テキストを圧縮してバイナリとして保存することで、ストレージ使用量を削減するとともに読み取り/書き込みのオーバーヘッドを低減できます。
金額には Long 型を使用:Tablestore は BigDecimal 型をサポートしていません。金額など正確な計算が必要なフィールドでは、Long 型で最小単位の値を保存してください (例:5.32 USD を 532 セントとして保存)。Double 型による精度損失を回避できます。
テーブル分割と容量計画
テーブル分割
単一の検索インデックスのデータ量は 200 億行以内に抑えてください。データ量が 200 億行を超える場合は、Tablestore テクニカルサポートに連絡し、テーブル分割の評価と設計を行ってください。
たとえば、現時点で最大のログテーブルが 61 億行で、年間 21 億行増加する場合、3~5 年以内に 200 億行を超えないため分割は不要です。既存データが大きく、かつ急速に増加している場合は、テーブル分割戦略を事前に計画してください。
ホット/コールドデータ分離
データアクセスパターンは通常、時間の影響を受けます。直近のデータは頻繁にアクセスされ、古いデータは次第にコールドデータとなります。ホットデータとコールドデータを同じテーブルに保存すると、アクセス負荷が不均一になり、予約済み読み取り/書き込みスループットが十分に活用されません。
ホットデータとコールドデータは、予約済み読み取り/書き込みスループットが異なる別テーブルに保存してください。ホットデータテーブルには多条件クエリ用に検索インデックスを作成し、コールドデータテーブルには固定的なクエリパターン向けにセカンダリインデックスを使用してコストを削減します。検索インデックスはデータ TTL (time-to-live) をサポートしており、ホットデータとコールドデータを自動的に分離できます。
一括データインポート
Tablestore にデータを一括インポートする際、プライマリキー順に書き込むと、書き込み負荷が単一パーティションに集中してインポートが遅くなります。次の対策を実施してください:
データセットを小さなサブセットに分割し、複数のワーカースレッドでサブセットをランダムに選択して並列にインポートしてください。
データを書き込む前に事前分割を実施し、開始時点からデータが複数のパーティションに分散されるようにするには、Tablestore テクニカルサポートにご連絡ください。
高並列の非同期書き込みには TableStoreWriter を使用してください。バケット分散とバッチ送信が自動的に処理されます。
設計事例:学生証の取引記録
次の例では、大学の学生証取引システムを用いて、テーブル設計の意思決定プロセスを示します。データテーブルのプライマリキー列には、CardID (学生証 ID)、SellerID (加盟店 ID)、DeviceID (端末 ID)、OrderNumber が含まれます。ビジネスルールは次のとおりです:
各学生証は 1 つの CardID にマッピングされ、各加盟店は 1 つの SellerID にマッピングされます。
各端末はグローバルに一意な DeviceID にマッピングされます。
OrderNumber は単一端末内で一意で、時間とともに増加しますが、グローバルに一意ではありません。
取引記録はリアルタイムで書き込まれます。
プライマリキーとパーティションキーの選定
最初にパーティションキーを決定します。各プライマリキー列の特性に基づき、異なるパーティション分割アプローチの影響を比較します:
|
パーティション分割アプローチ |
分析 |
|
CardID をパーティションキーにする |
学生 1 人あたりの 1 日の取引回数は限定的です。書き込み負荷は数万枚のカードに分散され、データ分散は良好です。推奨します。 |
|
SellerID をパーティションキーにする |
加盟店数が少なく、多くの取引が一部の加盟店に集中するため、ホットスポットが発生しやすくなります。推奨しません。 |
|
DeviceID をパーティションキーにする |
端末はグローバルに一意であり、分散は良好です。ただし、トラフィックの多い一部の端末 (人気の食堂にあるカードリーダーなど) では、局所的なホットスポットが発生する可能性があります。 |
|
OrderNumber をパーティションキーにする |
OrderNumber は端末内で時間とともに増加します。これをそのままパーティションキーにするとテールホットスポットが発生します。ハッシュプレフィックスが必要です。 |
設計の結論
分析の結果、CardID は最も良好なデータ分散 (数万枚のカード、学生 1 人あたり 1 日数回の取引) を提供するため、推奨されるパーティションキーです。プライマリキーの順序は CardID, DeviceID, SellerID, OrderNumber です。多くのビジネスシナリオでは、プライマリキーとパーティションキーを選定するだけで十分です。
高度な最適化
基本的なパーティションキー選定だけでは、パフォーマンス要件を満たせない場合があります。次に、代表的な高度最適化アプローチを 2 つ示します。
シナリオ 1:単一パーティションキーのデータ量が過大な場合に、複数列を連結してパーティションキーを作成する
ビジネス要件として DeviceID によるクエリが必要で、DeviceID をパーティションキーとして選定した場合、単一の DeviceID 配下のデータ量が 10 GB を超える可能性があります。この場合、DeviceID、SellerID、CardID を連結して新しいパーティションキーを作成し、同一端末からのレコードを加盟店と学生証単位で分割します。
連結前
|
DeviceID (パーティションキー) |
SellerID |
CardID |
OrderNumber |
問題 |
|
|
|
|
|
DeviceID=54 のすべてのレコードが同一パーティションに格納されます。時間の経過とともに、蓄積データ量が 10 GB を超える可能性があります。 |
|
|
|
|
|
|
|
|
|
|
|
連結後
|
DeviceID,SellerID,CardID (新しいパーティションキー) |
OrderNumber |
効果 |
|
|
|
同一端末からの取引記録が、加盟店と学生証単位で異なるパーティションキー値に分割されます。 |
|
|
|
|
|
|
|
シナリオ 2:パーティションキー値が順次増加する場合に、ハッシュプレフィックスを追加する
ビジネス要件として OrderNumber によるクエリが必要で、OrderNumber をパーティションキーとして選定する場合、端末内での OrderNumber の順次増加によりテールホットスポットが発生します。OrderNumber に対して MD5 のハッシュプレフィックスを計算し、データを均等に分散します:
|
元の OrderNumber |
MD5 の先頭 4 文字 |
HashOrderNumber (パーティションキー) |
|
|
|
|
|
|
|
|
|
|
|
|
ハッシュプレフィックスを追加した後は、同じアルゴリズムで任意の OrderNumber のハッシュプレフィックスを計算し、対応する HashOrderNumber を取得できます。ただし、ハッシュプレフィックスによって元の順序は分散されるため、論理的に連続するレコードを範囲読み取り操作で読み取ることはできなくなります。
ホット/コールドデータ分離
学生証の取引記録には明確な時間的特性があります。直近の記録は頻繁にクエリされる一方、卒業した学生の記録はほとんどアクセスされません。ホット/コールドデータ分離は、次のとおり計画してください:
直近のアクティブデータ (例:過去 1 年) と履歴データを、予約済み読み取り/書き込みスループットが異なる別テーブルに保存します。
アクティブテーブルには高い予約済みスループットを、履歴テーブルには低いスループットを設定し、ストレージコストを削減します。
アクティブテーブルには検索インデックスを作成し、多次元クエリ (加盟店や時間範囲など) を実行します。履歴テーブルではプライマリキーによるクエリのみで十分です。
保持期間を超えたデータを、定期的にアクティブテーブルから履歴テーブルへ移行します。