![]() |
| Amazon DynamoDB Streams |
はじめに
連載の最終回です。前回は、DynamoDB Streams + Lambda の消費能力が S × P ÷ D × 0.84 で決まり、限界を握っているのがオープンシャード数 S(テーブルのパーティション数)だと分かりました。オンデマンドモードで普通に DynamoDB テーブルを作った場合、この制約(S=4)により Lambda の同時実行数が 40 で頭打ちになってしまいました。
では、この S は動かせるのか。動かせるなら、Lambda の同時実行数も増えるはずです。今回はテーブルの事前ウォーム(warm throughput) でシャードを増やせるかを確かめてみます。
まずは結論から
- シャード数は warm throughput で決まる
- warm を上限の 40,000 まで上げると S が 4 → 64。 前回詰まった負荷が滞留ゼロで通過
- シャード数の限界は S=64。 その先はクォータ制限に引っ掛かる
では、今回の内容に入っていきます。
シャード拡張による効果
まずシャードの仕組みと数え方を押さえてから、warm throughput で S を引き上げ、前回詰まった負荷がどう変わるかを実測します。そのうえで、S を増やせる限界までを見ていきます。
オープンシャードと親子関係
本題に入る前に、今回のキーワードであるシャードをおさらいします。前回も触れましたが、今回は「シャードが増える/数える」が主役で、しかも親子関係が効いてくるので、丁寧に押さえておきます。
DynamoDB Streams のシャードは、テーブルへの変更レコードが流れる管を分割したものです。テーブルのパーティションと1対1で対応し、1シャードを1つの Lambda が順番に処理します(順序保証のため)。だから後続処理の同時実行の上限は シャード数(S) × 並列化係数(P) になります。前回の S × P の S が、このシャード数です。
ここで大事なのが、シャードには 「オープン」と「クローズ」の2状態があることです。
パーティションが分割されると、元のシャード(親)がクローズし、新しいシャード(子)がオープンします。親子関係はこうなります。
ここに今回の肝が2つあります。
-
同時実行(
S × P)に効くのは「オープンシャード」の数だけ。 クローズ済みシャードは時間とともに累積するので、全シャード数を数えると大幅な過大評価になります。「S を数える」とは「オープンシャードを数える」ことです。 - Streams は親シャードを読み切ってから子シャードを読みます。 順序保証のためです。だから分割直後に滞留を抱えていると、処理はまず親の消化に張り付き、増えた子シャードはすぐにはフル活用されません。
オープンシャード数の数え方
概念を押さえたところで、実際に自分のテーブルで S を数える方法を先に見ておきます。同時実行(S × P)に効くのはオープンシャードの数だけです。注意点は、総シャード数ではなくオープンシャードだけを数えること。クローズ済みシャードが累積しているので、総数を数えると過大評価になります。オープンシャードは DescribeStream の各シャードで EndingSequenceNumber を持たないものとして見分けます。
次のコマンドは、その条件で絞り込んだシャードの件数、つまりオープンシャード数(S) を返します。シャードが少なければ、これ一発で済みます。
aws dynamodbstreams describe-stream --stream-arn "$STREAM_ARN" \
| jq '[.StreamDescription.Shards[]
| select(.SequenceNumberRange.EndingSequenceNumber == null)] | length'
ただし DescribeStream は1回で最大100シャードしか返しません。レスポンスに LastEvaluatedShardId が入っていたら、まだ続きがある合図です。その値を次の呼び出しの --exclusive-start-shard-id に渡し、LastEvaluatedShardId が返らなくなるまで繰り返して、各ページのオープンシャードを合算します。
S さえ正しく数えられれば、同時実行の上限 S × P が分かります。
では、その S をどう増やすのか、本題に入ります。
シャード数を決めるもの
DynamoDB の warm throughput(事前ウォーム) は、テーブルが「これくらいのスループットにすぐ耐えられる」状態を先に用意しておく機能です。read と write の2つの値を持ち、ここで効くのは write 側(以降 warm write と呼びます)です。パーティションあたりの書き込み上限は 1,000 units/s で、オープンシャードとパーティションは1対1。だとすれば、warm write を上げるとパーティションが増え、それにつれてシャードも増えるのではないか。これが今回の仮説です。
warm throughput は WCU/RCU そのものではありません。 WCU/RCU(プロビジョンドテーブルのキャパシティ設定)が「いくら課金してキャパシティを割り当てるか」なのに対し、warm throughput は「テーブルが即座に支えられるリクエスト数のベースライン」です(AWS ドキュメント)。だからオンデマンドテーブルにも warm throughput の値があります(既定値は read 12,000 / write 4,000)。
さっそく確かめます。「設定」タブで warm write を既定の 4,000 から、クォータ上限いっぱいの 40,000 に引き上げ、DescribeStream でオープンシャード数(S)を数えました。
| warm write | オープンシャード S |
|---|---|
| 4,000(既定) | 4 |
| 40,000(最大) | 64 |
増えました。 warm write を 10 倍(4,000 → 40,000)にすると、S は 16 倍(4 → 64)になりました。きれいな比例ではなく、S は warm write ÷ 1,000 の約 1.6 倍です。ぴったりは当たりませんが、見積もりの目安には使えます。
warm を上げたことで、オープンシャード S が 4 → 64、同時実行の上限 S × P が 40 → 640 まで広がりました。
今回は手動で warm を引き上げましたが、実は warm throughput は負荷に応じて DynamoDB が自動でも引き上げてくれます。ただし自動調整には2つのクセがあります。
- 上がるだけで、下がらない。 一度増えたパーティションはマージされず、負荷が減っても元に戻りません(AWS ブログ)。warm throughput は一方向にしか動きません。
- 急な立ち上がりには間に合わない。 自動調整は過去の使用実績に基づくので、じわじわ増える負荷には追従できても、バズのような急なスパイクには分割が間に合いません。
だから今回は、自動で上がるのを待たず先回りして手動で引き上げました。この「自動か手動か」の判断は、後半の考察でもう一度効いてきます。
warm 引き上げの反映中は Status が UPDATING になるので、ACTIVE に戻ってから S を確定してください。
S=64 での再試験
いよいよ負荷を投入してみます。S = 64 にシャード数を拡張したテーブルに対して、前回ぶつけたのと同じ高負荷「2,000件/分 × 15分」を流します。投入も並列化係数も処理時間も揃え、S だけを変えた比較です。
| 項目 | S=4(前回) | S=64(今回) |
|---|---|---|
| 投入実レート | 1,996.1/分 | 1,994.9/分 |
| 滞留増加率 | +1,427 件/分 | 0 件/分 |
| 停止時の滞留 | 42,814 件 | 0 件 |
| 回復時間 | 約 76 分 | 約 1.7 秒 |
| Lambda 同時実行数 | 最大 43 | 最大 142 |
同じコード・同じ負荷で、42,814 件あった滞留が 0 件になりました。 処理の限界が固定値ではなく、テーブルの成熟度で動くことが実証できました。同時実行数も最大 43 から最大 142 まで広がっており、同じ投入でも S が増えたぶんだけ Lambda が並列に走れるようになっています。
同じ 2,000 件/分の投入が、S の違いだけでこう変わります。
投入レートは同じでも、消費能力(出口の広さ)が 570 → 8,800 件/分に広がったので、入口が出口を上回らなくなった、という図です。
最大同時実行の実測
ここまでは「前回詰まった負荷が解消したか」の確認でした。もう一つ、別で確かめたいことがあります。同時実行が本当に理論値どおり S × P = 640 まで出るのかです。
ただ、さきほどの再試験(2,000 件/分)では 640 は測れません。消費能力 8,800 件/分に対して投入 2,000 件/分は余裕運転で、同時実行は必要な分しか立ち上がらないからです。640 枠を埋めきるには、投入で飽和させる必要があります。
そこで、後続処理を軽く(1 件 50 ms)して、滞留ゼロの S=64 テーブルに一気にバーストを入れ、ConcurrentExecutions(同時実行数)を実測しました。処理を軽くしても S × P の上限は変わりません(D は消費能力=捌く速さには効きますが、同時に走れる本数には効かないため)。
| 投入レート | 同時実行数の最大値 | 理論値 S × P(640)に対して |
|---|---|---|
| 15 万件を約 7 秒間 | 595 | 93.0% |
| 32 万件を約 13 秒間 | 603 | 94.2% |
約 600 多重まで出ました。「S=64 → 640 多重」は計算だけでなく実測で裏づけられたことになります。理論値ちょうどに届かない数%は、前回も触れた P のスケール損失(シャードが増えるほど 1 本あたりの並列度がわずかに目減りする)によるものだと思われます。
負荷・データ量との関係性
ところで、warm を 40,000 にすると S が 64 になりましたが、この値はどこまで信用できるのでしょうか。実測は warm ÷ 1,000 の約 1.6 倍で、公式の記述(パーティションあたり write 1,000 units/s、オープンシャードとパーティションは1対1)から素直に考えた 1 対 1 の対応より上振れしています。この上振れ分が warm 以外の要因(負荷履歴やデータ量など)で生まれているなら、それを足し込めば 1.6 倍以上に増やせる余地があるのでは、と考えました。
そこで、負荷ゼロ・データゼロの新品テーブルを新たに作り、warm write を 40,000 に設定してみました。結果は投入ゼロのまま S=64。上振れは warm 以外の要因ではなく、warm だけで決まっていたというのが実測の答えでした。
念のため、warm 以外の要因でも動かないことを一通り確かめました。
| 試したこと | 投入・設定 | オープンシャード S |
|---|---|---|
| 素の状態 | 負荷ゼロ・データゼロ | 64 |
| 過負荷をかける | 48 MB/s(warm 超え、スロットル発生)まで投入 | 64 のまま |
| データを大量に積む | 355 万件・約 3.6 GB を投入 | 64 のまま |
| 項目を大きくする | 1 件 64 KB(項目サイズの上限) | 64 のまま |
いずれも S は安定して 64 のまま動きませんでした。オープンシャード数を決めるレバーは warm write ただ一つです。負荷も、データ総量も、項目サイズも寄与しません。
拡張を止めるクォータ上限
限界は広げられました。では、どこまで広げられるのか。前回の宿題は「S をさらに増やして S × P を 1,000 まで持っていき、Lambda の同時実行枠(1,000)に触れる様子を見る」ことでした。S=100・P=10 で 1,000。S=100 を得るには warm を 100,000 まで上げる必要があります。
まず、オンデマンドのまま warm を 100,000 に設定しようとしました。これはテーブル単位の write クォータ 40,000 に引っかかって設定できません。関係するクォータは次の3つです。
| クォータ(リージョン単位) | 上限値 | 何を縛るか |
|---|---|---|
| テーブル単位の write throughput | 40,000 | 1つのテーブルに設定できる write の上限 |
| アカウント単位の write throughput | 80,000 | 全テーブルの write 合計の上限 |
| Streams の write throughput | 40,000 | Streams 有効テーブルの write の上限 |
「ならプロビジョンドに替えて WCU を直接張ればいい」と考え、課金モードを変えて 64,000 を設定しようとしたところ、今度はこう弾かれました。
Requested write capacity units exceed current limit allowed for stream-enabled tables.
つまり、どの経路でも 40,000 が上限です。しかも真犯人はテーブルクォータではなく、Streams の書き込みクォータでした。この2つは既定値が同じ 40,000 なので紛らわしいのですが、Streams を有効にしている以上、効いているのは Streams 側です。Streams を有効にしたテーブルは、オンデマンドでもプロビジョンドでも write が 40,000 で頭打ちになります。
ちなみに、同じ 40,000 でもプロビジョンドだと逆に S が減りました(オンデマンド warm で 64、プロビジョンドで 41)。プロビジョンドの 41 は下限式(40,000 ÷ 1,000 = 40)どおりで、むしろオンデマンドの 64 が 1.6 倍に上振れしている、という関係です。1,000 多重を狙うなら、オンデマンドで warm を上げるほうが有利だったわけです。
結論として、オンデマンド/warm で現実的に到達できるのは S=64、実効で約 600 多重までです。その先の Lambda 同時実行枠(1,000)に到達するには、Streams の書き込みクォータの引き上げ申請が要ります。今回はそこまでは踏み込んでいません。
実環境への適用
S は warm で増やせる、天井は S=64。ここからは、これを実務の判断にどう落とすかを考えます。いつ warm を触るのか、外部 API のレート制限とどう折り合うのか、そして S × P で足りないときの次の一手まで見ていきます。
Kiro Roasters での現実
ここまでは「シャードは増やせる」という技術の話でした。では、Kiro Roasters の実務で、いつ、何を触ればいいのか。 本連載の題材である EC 注文で考えます。
S=4・P=10 なので、同時実行は 40 で頭打ち。消費能力は S × P ÷ D × 0.84 で、D=3.6 秒なら約 570 件/分です。通常の月間 500 件規模ではまったく詰まりませんが、EC が成長して恒常的に数百〜千件/分を捌くようになると、話が変わってきます。
ありがたいことに、D を 3 秒前後で固定すれば、どのくらいの負荷でどれだけ滞留するかは計算で予測できます。 前回の式(滞留増加率 = 投入 − 消費能力、回復時間 = 滞留 ÷ 消費能力)に数字を入れるだけです。
投入レートと待ち時間
まず、同じ「1 時間の負荷」でも、投入レートが上がるほど最後の注文が片付くまでの待ち時間が伸びます(消費能力 570 件/分、D=3 秒固定の理論計算)。
| 投入レート | 能力比 | 滞留の増え方 | 1 時間後の滞留 | 消化しきる時間 |
|---|---|---|---|---|
| 600 件/分 | 1.05 倍 | +30 件/分 | 1,800 件 | 約 3 分 |
| 800 件/分 | 1.40 倍 | +230 件/分 | 13,800 件 | 約 24 分 |
| 1,000 件/分 | 1.75 倍 | +430 件/分 | 25,800 件 | 約 45 分 |
| 1,500 件/分 | 2.63 倍 | +930 件/分 | 55,800 件 | 約 98 分 |
能力をわずかに超えた 600 件/分なら、1 時間の負荷でも滞留は 3 分で片付きます。ところが 1,000 件/分になると、投入が止まってから消化しきるまで 45 分。1 時間ぶんの注文の後始末に、さらに 45 分かかるわけです。
継続時間と滞留の蓄積
次に、投入レートを 1,000 件/分に固定して、継続時間を変えます。滞留は線形に積むので、続くほど後始末が伸びます。
| 継続時間 | 停止時の滞留 | 消化しきる時間 | 評価 |
|---|---|---|---|
| 15 分 | 6,450 件 | 約 11 分 | まだ待てる |
| 1 時間 | 25,800 件 | 約 45 分 | そろそろ厳しい |
| 3 時間 | 77,400 件 | 約 136 分 | 我慢の限界 |
この 2 つの表を自分のワークロードに当てはめれば、「いまの投入レートと継続時間なら、滞留は我慢できる範囲か」を事前に見積もれます。 滞留が我慢できる範囲なら、何もしなくて構いません。詰まっても投入が止まれば自然に消化されます。
warm を上げる判断
問題は、この滞留が業務として我慢できなくなってきたときです。
ここで効くのが、消費能力の式 S × P ÷ D × 0.84 の S です。D(後続処理の速さ)は決済 API 次第で簡単には縮められないし、P(並列化係数)は最大 10。残る打ち手は S を増やすことで、その唯一のレバーが warm throughput です。
ただし、EC の負荷では warm は自動では上がりません。自動分割が誘発されるには書き込みピークが現在の warm(4,000 write/s = 分あたり 24 万件)を超える必要があり、1,000 件/分の注文は桁が 2 つ以上足りません。自動拡張を待っても永遠に増えないので、自分で warm を引き上げるという判断になります。
とはいえ、いきなり最大まで振る必要はありません。warm は一度上げると下げられないので、必要なぶんだけ見積もって上げるのが筋です。目標の投入レートを捌ける S を式から逆算します。
- 1,000 件/分を余裕を持って捌くなら、消費能力は 1,200 件/分ほど欲しい
-
S × P ÷ D × 0.84 ≥ 1,200を満たす S は 9〜10 程度(既定の 4 では足りない) - S は warm に対して約 1.6 倍なので、必要な warm は 6,000〜8,000 程度
既定の 4,000(S=4)から 6,000〜8,000 へ少し上げれば、1,000 件/分は滞留なく流れる計算です。ただし 6,000〜8,000 のような中間値での S は実測していない(4,000 → 4、40,000 → 64 の 2 点からの見積もり)ので、設定後は DescribeStream で実際の S を確認してください。下げられない設定である以上、自分の投入レートから必要な S を逆算して、控えめに上げるのが実務的です。
なお、すでに滞留を抱えてから warm を上げる場合は、増えた並列がすぐにはフルに効きません(先に見たとおり、親シャードが捌けてから効き始めます)。できれば詰まる前に備えるのが理想です。
ここまでの判断を 1 枚のフローにするとこうなります。
ここで強調したいのは、何を触ればいいかを裏付けを持って選べるようになったことです。「滞留が増えてきた」という症状に対して、犯人は S で、レバーは warm、必要な量は投入レートから逆算、効き始めるのは親シャードの消化後。公式ドキュメントにも書かれている話かもしれませんが、自分の手で実測して裏を取れたので、迷わず判断できます。
warm throughput は一度上げると下げられません。課金は一回限りの差分で、us-west-2 の write は \$0.00065 / unit。4,000 → 8,000 なら \$2.60、4,000 → 40,000 でも \$23.40 でした。保有期間中の追加課金はありませんが、「下げられない」前提で判断してください。
自動拡張に任せるという手
さきほど「EC の負荷では自動では増えないので手で上げる」と書きましたが、では仮に自動分割が誘発されるほどの負荷が入ったら、S は勝手に育ってそのまま使えるのでしょうか。挙動として確かめておきます。
現在の warm を超えるピークを与えて分割そのものを誘発してみます。warm 4,000(S=4)から、その warm を明確に超えるレートを投入し続けました。
その結果、S はピークに追従して 4 → 8 → 16 → 32 と 2 のべき乗で増えました(分割はピーク更新から 1 分以内)。手で上げなくても、負荷のピークが現在の warm を超えれば DynamoDB が分割を始めます。
ただし、仮にこの状況に至ったとしても、育った S をそのまま使えるとは限りません。負荷任せに育てた S=20 で同時実行を測ると、約 81(S × P = 200 の 4 割)で頭打ちでした。
| 状態 | シャード S | 同時実行数 |
S × P に対して |
|---|---|---|---|
| 滞留を抱えたまま(負荷が続く) | 20 | 最大81 | 40% |
| 滞留をゼロにしてから流す | 20 | 199 | 99.5% |
同じ S=20 でも、滞留の有無だけで同時実行が 2.5 倍変わりました。理由は Streams の順序保証です。Streams は親シャードを読み切ってから子シャードを読むので、負荷が定常的に続いて滞留を抱えていると、処理はまず詰まった親シャードの消化に張り付き、分割で増えた子シャード(=広げたはずの並列度)はすぐには使えません。親が片付いて初めて S × P の並列が出ます。前回の記事での高負荷試験でも、これがネックになって滞留がなかなか捌けませんでした。
もっとも、この頭打ちは過渡的なものです。滞留が消化されて親シャードが片付けば、同時実行は S × P(上の表の 199)に戻ります。普段どおり滞留ゼロで回っているぶんには関係ありません。ただ、並列を本当に使いたいのは突然の高負荷で滞留を抱えたときなので、その肝心な局面で並列度は増えないのは覚えておく価値があります。結局、確実に S を効かせたいなら、自動を待つより先回りして手で上げるのが実務的です。
外部 API のレート制限
もう一つ、実務で効いてくる制約があります。今回、後続処理は外部の決済 API を叩くことを想定していますが、外部 API にはたいていレート制限があります。たとえば「50 req/s を超えないこと」。決済の呼び出しレートは 同時実行数 ÷ D なので、同時実行が増えればレートも上がります。
今回の構成では order-processor に予約同時実行数(Reserved Concurrency)を設定していません。つまり同時実行の上限は S × P だけで決まる、剥き出しの状態です。この状態での決済レートをシャード別に見ると、こうなります(処理時間 D=3.6秒として)。
| 条件 | 決済の呼び出しレート | 外部APIのレート(50 req/s)に対して |
|---|---|---|
| S=4 | 約 9 req/s | 収まる |
| S=64(飽和時) | 約 178 req/s | 3 倍以上の超過 |
S=4 のときは、たまたま決済 API を守れていました。S × P = 40 が同時実行の上限を作り、それが結果的に流量制御として働いていたからです。ところがこれは副作用であって、流量制御の仕組みではありません。warm で S を 64 に増やした瞬間、同時実行の上限が 640 に跳ね上がり、決済レートは 50 req/s を大きく破ります。
予約同時実行で守る
では S=64 のまま 50 req/s を守るにはどうするか。S × P に頼るのをやめて、予約同時実行数で明示的に絞ります。守りたいレートから必要な同時実行数を逆算します。
- 守りたい: 50 req/s、D=3.6 秒
- 必要な同時実行数:
50 × 3.6 = 180前後
order-processor に予約同時実行数を 180 程度に設定すれば、S がいくつでも(64 でも)同時実行が 180 で頭打ちになり、決済レートは 50 req/s に収まります。予約同時実行は、内部ポーラー経由で起動される Lambda(Streams や SQS)では同時に処理する数を制限する弁として働き、あふれたレコードは Streams 側に保持されます(AWS ドキュメント)。
この場合、予約同時実行は S × P より小さいときだけ意味を持ちます。S × P より大きく設定しても、先に S × P で頭打ちになるので効きません。あくまで「S × P が作る上限を、さらに下へ絞る」機構です。
絞ると滞留はどこへ行くか
ただし、ここにトレードオフがあります。予約同時実行で絞ると、消費能力そのものも落ちます。
| 絞らない(S=64) | 予約同時実行 180 | |
|---|---|---|
| 同時実行の上限 | 640 | 180 |
| 消費能力(目安) | 約 8,800 件/分 | 約 2,500 件/分 |
| 決済レート | 最大 約 178 req/s | 約 50 req/s |
消費能力が下がったぶん、投入がそれを超えると Streams に滞留します。滞留が出る場所は記事1・2と同じ IteratorAge で、現象も同じ。違うのは「消費能力の天井を S × P ではなく予約同時実行数で引き下げた」点だけです。せっかく S=64 に増やして 640 並列を手に入れても、決済 API を守るために 180 で蓋をするので、その並列度は使えません。
つまり「決済 API を守る」と「滞留しにくさ(速く捌く)」は、同じ order-processor の中で両立できません。これは direct 構成(Streams 直結の単一 Lambda)の構造的な限界です。
次の一手(SQS か Kinesis か)
S × P の壁を外す、あるいは S × P に頼らず並列度を自分で握るには、大きく3つの方向があります。Streams の利点である CDC(変更データキャプチャ)を続けるか捨てるか、で分かれます。
方向 A は、CDC の出力先を Streams から Kinesis Data Streams に替えます。Kinesis は DynamoDB Streams と違ってシャード数を自分で指定できるので、S そのものを握れます。順序保証(シャード単位)も、複数の consumer が同じ変更を独立に読めることも残ります。「Streams の次は Kinesis」と紹介されることが多いのは、この性質ゆえです。
方向 B・C は、後続処理の並列度を S × P から切り離し、SQS を消費するワーカーの同時実行で握る狙いです。SQS をソースにした Lambda は、シャード数と無関係に大きくスケールします(AWS ドキュメント)。
いずれの方向でも、後続処理の並列度を S × P の外に出して自分で握れるようになります。実際にどう組んで、どこまで並列度を伸ばせるのか、今後確認していければと思います。
まとめ
最後にまとめます。
- シャード数 S は warm throughput で決まる。warm 上限 40,000 で S が 4 → 64 に増えた。
-
S × P= 640 の Lambda 同時実行も実測で裏づけた(約 600 多重)。 - クォータ制限に阻まれて Lambda 同時実行数の Max枠 1,000 には届かなかった。
DynamoDB Streams x Lambda のイベント駆動の限界を知りたい場合、まずは自分の構成で、DescribeStream でオープンシャード数を数え、投入レートが消費能力 S × P ÷ D × 0.84 を超えないかを確かめるところから始めてみてください。また、消費能力に不安がある場合は、warm write を増やすことで限界を上げておいてください。
なお、本連載 3 本で使った検証リポジトリ(検証装置・負荷生成・計測の仕組みと、実測結果のドキュメント)を公開しています。検証結果は結構細かくドキュメントに残しているので、手元で確認したい方はこちらをどうぞ。






