![]() |
| Amazon DynamoDB |
はじめに
前回、DynamoDB のホットパーティションを意図的に起こそうとして、見事に失敗しました。
パーティションキー設計の良し悪しによってスロットリングの発生に違いがあるかを確認しようとしましたが、いくつかの要因でなかなか発生せず。
今回はそのリベンジ編です。前回の要因を排除して再挑戦してみました。
まずは結論から
- ホットパーティションを起こせました。 Bad 設計のテーブルでスロットル率 54.8%
- WCU設定値を大きくしても1パーティションあたりの上限1000WCU/秒で頭打ち
- オンデマンドでも発生しました。 ただし同じ負荷を繰り返したら消えました
想定される読者
- DynamoDB のパーティションキー設計を検討している方
- ホットパーティションの話は知っているが、実際の数字を見たことがない方
前回のおさらい
架空のコーヒー焙煎メーカー「Kiro Roasters」の在庫管理システムを題材にしています。倉庫は3拠点(東京 / 大阪 / 福岡)、商品は約5,000 SKU。朝の出荷ラッシュで東京倉庫に出庫が集中する、という設定です。
検証で使うパーティション設計は以下2種類です。
Bad 設計(カーディナリティ = 3)
PK: warehouseId → "WH-TOKYO" / "WH-OSAKA" / "WH-FUKUOKA"
SK: itemId → "ITEM#ETH-YIRG-G1-MEDIUM-200G" など
Good 設計(カーディナリティ = 約5,000)
PK: itemId → "ITEM#ETH-YIRG-G1-MEDIUM-200G" など
SK: warehouseId → "WH-TOKYO" / "WH-OSAKA" / "WH-FUKUOKA"
DynamoDB の物理パーティションには、1つのパーティションにつき書き込み 1,000 WCU/秒、読み取り 3,000 RCU/秒という上限があります。全体のキャパシティに余裕があっても、1つのパーティションキーにアクセスが集中すればその上限で頭打ちになります。これがホットパーティションです。
Bad 設計の場合は1つの物理パーティション(東京倉庫)に書き込みが集中するため、この上限に到達する想定でした。
ホットパーティション発生のメカニズム
検証を進めるうちに行き先を見失ってきたので、ホットパーティションが発生する流れを改めて整理しておきます。
DynamoDB はパーティションキーの値をハッシュ化して、書き込み先の物理パーティションを決めます。ここが全ての起点です。
Bad 設計のパーティションキーは warehouseId で、今回取り得る値は3種類しかありません。ハッシュの結果も3通りにしかならないため、書き込み先は最大3パーティションに固定されます。十分なテーブル容量があったとしても、特定のパーティションに書き込みが偏るとこうなります。
インプットの 4,000 req/s が東京80%と偏っていた場合、東京パーティションに 3,200 WCU/秒 のリクエストが届きます。ただし物理パーティション上限の 1,000 WCU/秒 までしか受け取れないので、あふれた 2,200 WCU/秒 が ThrottlingException になります。
テーブル全体に十分な容量があったとしても、空いているキャパシティは他のパーティションのものなので、東京の書き込みには回せません。容量を追加しても東京パーティションの上限は 1,000 WCU/秒 のままです。
一方 Good 設計はパーティションキーが itemId で約5,000種類あります。ハッシュの行き先が5,000通りに散るため、東京80%という偏りが残ったままでもパーティションの偏りにはならず、書き込みは成功します。
同じ倉庫の偏り、同じ負荷、同じテーブル容量。違いはパーティションキーの選び方だけです。
検証環境
前回からの変更点
前回は WCU の設定が小さすぎた(最大でも WCU 100)ため、物理パーティションが1つしか存在せず、Bad と Good でキー設計の差が出ませんでした。パーティションが1つなら、どんなキーを選んでも書き込み先は全部同じです。加えて、負荷投入時の実効 RPS が十分に上がらなかったため、想定通りのインプットが出来ていませんでした。
今回は WCU の設定値を十分に上げて、十分な負荷を投入した結果を確認していきます。
検証対象テーブル
当初から取り扱っていた Good/Bad テーブルだけでなく、GSI 設定付きテーブルとオンデマンドテーブルも比較対象に加えてみます。
| 論理名 | PK / SK | 課金モード | 容量 | GSI |
|---|---|---|---|---|
| bad | warehouseId / itemId | プロビジョンド | WCU 10,000 / RCU 1,000 | なし |
| good | itemId / warehouseId | プロビジョンド | WCU 10,000 / RCU 1,000 | なし |
| goodGsi | itemId / warehouseId | プロビジョンド | WCU 10,000 / RCU 1,000 | PK=warehouseId |
| badOnDemand | warehouseId / itemId | オンデマンド | PAY_PER_REQUEST | なし |
goodGsi は「ベーステーブルは正しく設計したけれど、倉庫別に照会したいので GSI を足した」という、実際にありそうな構成です。badOnDemand は「オンデマンドにしておけば大丈夫じゃないの?」という根拠なき期待を確かめるためのものです。
負荷テストの条件
4パターンとも同じ条件で回します。
| パラメータ | 値 |
|---|---|
| 宣言 RPS | 4,000 req/s(27ワーカー × 149 req/s) |
| 継続時間 | 120秒 |
| 総リクエスト数 | 480,000(4,000 x 120 = 480,000) |
| 倉庫分布 | 東京80% / 大阪10% / 福岡10% |
| データ量 | 各テーブル15,000レコード(5,000 SKU × 3倉庫) |
| リージョン | us-west-2 |
| SDK リトライ | 無効(maxAttempts: 1) |
東京への集中率を前回の 70% から 80% に上げました。4,000 × 0.8 = 3,200 WCU/秒 が1つのパーティションに向かう計算で、上限 1,000 WCU/秒 を大きく超えさせる狙いです。
アーキテクチャ図
基本的には前回検証時と同様です。変更点は以下3点。
- DynamoDB を 4テーブル(bad / good / goodGsi / badOnDemand)に追加
- 負荷投入 Lambda のメモリが 1024MB → 2048MB
- ワーカー数も増加(4 → 27)
検証結果
まず全体の結果です。総リクエスト数は 480,000。しっかりと負荷を叩き込んでます。
| # | テーブル | 成功 | スロットル | スロットル率 | 実効 RPS | 消費 WCU ピーク |
|---|---|---|---|---|---|---|
| ① | bad | 216,860 | 263,140 | 54.8% | 4,034 | 1,805/s |
| ② | good | 480,000 | 0 | 0% | 4,034 | 3,995/s |
| ③ | goodGsi | 352,340 | 127,660 | 26.6% | 4,034 | 3,238/s |
| ④ | badOnDemand | 215,851 | 264,149 | 55.0% | 4,034 | 1,800/s |
個々の結果については以降で説明しますが、スロットリングもしっかりと発生しておりひと安心。
検証① Bad Table vs ② Good Table
ここが本来やりたかった比較です。同じ負荷・同じ容量・同じデータ量で、パーティションキーの設計だけが違う2つのテーブルを並べます。
結果
| 指標 | ① Bad(PK=warehouseId) | ② Good(PK=itemId) |
|---|---|---|
| 総リクエスト | 480,000 | 480,000 |
| 成功 | 216,860 | 480,000 |
| スロットル | 263,140 | 0 |
| スロットル率 | 54.8% | 0% |
| 消費 WCU ピーク | 1,805/s | 3,995/s |
Bad Table でスロットリングが発生する一方で、Good Table であれば負荷が集中しても全てのリクエストが成功していることが分かります。キー設計が異なるだけで、スロットル率 54.8% → 0% です。
CloudWatch で両テーブルの消費 WCU とスロットリング発生数を並べたグラフです。
Bad(青)はスロットル(緑)が大量に発生し、処理できたリクエストが半分以下に抑えられています。Good(赤)はスロットルゼロで、投げた分がそのまま処理されています。
スロットル率は理論値どおり
前半説明したホットパーティションのメカニズムがそのまま数字に出ています。東京パーティションに 3,227 req/s が集中し、上限 1,000 WCU/秒 を超えた 2,227 req/s が弾かれた結果、スロットル率は理論値 55.2% に対して実測 54.8% でした。
一方 Good は、パーティションキーが約5,000種類に分散しているため、東京80%の偏りがあってもどのパーティションも上限に到達しません。スロットルゼロで全リクエストが成功しています。
容量をいくら積んでも、キー設計が悪ければ1パーティション 1,000 WCU/秒 の壁で止まる。キー設計が正しければ、同じ偏りでも 4,000 RPS を素通しできる。 これが本記事の一番見せたかった数字です。
検証③ Good Table + GSI
ベーステーブルは Good 設計、そこに GSI を3本足した構成です。3本とも PK は warehouseId にしてあります。「倉庫別に一覧したい」「倉庫別に棚番号で引きたい」「倉庫別に単価で並べたい」という、普通に出てきそうな要件を想定しました。
結果
書き込みの 26.6% が失敗
検証②(GSIなし)では 0% だったスロットルが、GSI を足しただけで 26.6% に跳ね上がりました。ベーステーブルのキー設計は同じ Good 設計のままです。
原因
GSI の PK が warehouseId なので、GSI 側でホットパーティションが発生
ベーステーブルは PK=itemId で分散していますが、GSI は PK=warehouseId なので結局3つのパーティションに集中します。GSI のパーティションが詰まると、ベーステーブルへの書き込みリクエスト自体が拒否されます。アプリケーションから見ると、ベーステーブルのキー設計を正しくしたのにスロットルが返ってくる、という状態です。
CloudWatch で Good(青)と Good + GSI(オレンジ)の消費 WCU を並べたグラフです。
同じ Good 設計なのに、GSI を足しただけで消費 WCU が頭打ちになり、約19%落ちていることが見て取れます。
GSI への書き込みは非同期なので「ベーステーブルには影響しない」と考えていましたが、GSI のキャパシティが詰まるとベーステーブルの書き込み自体がブロックされます。ベーステーブルのキー設計を正しくしても、GSI のキー設計を間違えるとホットパーティションは戻ってきます。
検証④ オンデマンド
「オンデマンドなら DynamoDB が勝手にスケールしてくれるのでは?」を確かめます。
結果
初回は Bad Table と同じく 55.0% でスロットリング
パーティション単位の 1,000 WCU/秒 という制限は、課金モードに関係なく適用されます。オンデマンドはホットパーティションの対策にはなりません。
ただ、同じ負荷を繰り返したところ挙動が変わりました。
| 実行 | スロットル率 | 消費 WCU ピーク |
|---|---|---|
| 1回目 | 55.0% | 1,800/s |
| 2回目 | 30.0% | 2,797/s |
| 3回目 | 0.0% | 4,005/s |
CloudWatch で結果を見ると、回を追うごとにスロットル率(オレンジ)が下がり、3回目でゼロになりました。その分、消費 WCU が伸びています。
原因
split-for-heat が働いたと思われます
DynamoDB には、ホットなパーティションを検知するとそのパーティションを内部的に2つに分割する仕組みがあります。これが split-for-heat です。
通常、1パーティションの上限は 1,000 WCU/秒 です。しかし分割されると、同じパーティションキーの範囲を2つの物理パーティションが受け持つようになるため、実質的な上限が 2,000 WCU/秒 に上がります。さらに負荷が続けばもう一度分割されて 4,000 WCU/秒 に、という具合です。
今回の実測はこの動きと一致しています。
1回目: 上限 1,000 → 消費WCU 1,800/s で頭打ち、スロットル 55%
2回目: 上限が上がった? → 消費WCU 2,797/s、スロットル 30%
3回目: さらに上がった? → 消費WCU 4,005/s、スロットル 0%
ただしパーティション数を直接確認できる API はないので、これは間接証拠からの推測です。「パーティションが分割されたから上限が上がった」と断定はできません。
まとめ
無事、DynamoDB のホットパーティションを故意に発生させる事ができました。
当初想定していたパターンの実証だけでなく、GSIやオンデマンドなど追加パターンの検証もでき、有意義な内容になりました。
ちなみにこのために使った DynamoDB のコストは、176ドル になりました。追加検証を行った1日だけの金額です。プロビジョンドが大半を占めており、オンデマンドはごくわずか(1ドル未満)です。
今回の検証でオンデマンドモードでもホットパーティションが発生することは確認できたので、もし似た検証を検討されている方はオンデマンドでの検証をオススメします。
さらに実運用を考える上でもオンデマンドの適応力(split-for-heat?)は魅力的に感じました。
今回はいきなり極端に高い負荷を叩き込みましたが、本来徐々に負荷が上がるような状況が想定されるため、仮にパーティションキーの設計が甘くても負荷状況に応じて良い感じにパーティション分割が働いてくれるのではないかと期待できるのではないかと。もちろん、キー設計をしっかりやるに越したことはないですけどね。




