DynamoDBバッチ操作:BatchGetItem & BatchWriteItem
多数のアイテムを一度に読み書きする必要があるとき、アイテムごとにGetItemやPutItemを1回ずつ撃つと、アイテムごとにネットワーク往復が1回 — 遅く、おしゃべりです。DynamoDBのバッチAPIは、多数のアイテム操作を単一のリクエストにまとめます。読み取りにはBatchGetItem、書き込みにはBatchWriteItemです。
これらはスループットとレイテンシの勝ちであって、整合性の保証ではありません — そしてその区別こそ、人が痛い目を見るところです。バッチはトランザクションではありません。
DynamoDBのバッチ操作とは?
DynamoDBのバッチ操作は、多数のアイテムの読み取りや書き込みを単一のリクエストにまとめます。BatchGetItemは最大100アイテムを取得し、BatchWriteItemは最大25件のput/deleteを行い、いずれも16 MBが上限です。これらが節約するのは往復であって、キャパシティではありません。重要なのは、バッチはトランザクションではないという点です — アイテムはそれぞれ独立して成功または失敗し、ロールバックはありません。
BatchGetItem— 1回の呼び出しで、1つ以上のテーブルにまたがって最大100アイテム(または16 MB)を取得。BatchWriteItem— 1回の呼び出しで最大25のput/delete操作(または16 MB)。更新はなし — putとdeleteのみ。- アトミックではない。 一部のアイテムが成功し、他が失敗しうる。ロールバックはない。
- 部分的失敗は正常。 スロットリングされたアイテムは
UnprocessedItems/UnprocessedKeysで返ってくる — 自分でバックオフ付きで再試行しなければならない。 - 個別の呼び出しと同じキャパシティコスト — バッチは往復を節約するのであって、キャパシティユニットではない。
問題:多数のアイテム、1往復
サポートデスクを運用しているとします。ダッシュボードはキューを描画するために50件のチケットをIDで読み込む必要があり、夜間ジョブは1,000件の解決済みチケットをアーカイブします。それを1アイテムずつ行うと、50回(または1,000回)の連続した往復になります — レイテンシが積み上がり、ジョブは這うように進みます。
バッチ処理はそれらをほんの数回の呼び出しに畳み込みます。50件のチケット読み取りは単一のBatchGetItemになり、アーカイブジョブは25件ずつのdeleteからなるBatchWriteItem呼び出しのストリームになります。往復ははるかに少なく、動かすデータは同じです。
バッチAPIの仕組み
**BatchGetItemは(1つ以上のテーブルにまたがる)プライマリキーのセットを取り、一致するアイテムを返します。テーブルごとに強い整合性のある読み取りを要求できます。読み取れなかったもの — 通常はリクエストがスループット制限に触れたため — は、呼び出し全体を失敗させるのではなくUnprocessedKeys**で返ってきます。
**BatchWriteItemはPutRequest / DeleteRequest操作のリストを取ります。欠けているものに注目してください。更新はありません。バッチ書き込みはアイテム全体を置き換える(put)か削除する(delete)かのいずれかです — 特定の属性を変更するには依然としてUpdateItemが必要です。書き込めなかったアイテムはUnprocessedItems**で返ってきます。
要となるメンタルモデル:バッチは独立した操作の束であり、各々が単独で成功または失敗する — 全か無かの1単位ではありません。
バッチはトランザクションではない
これが罠です。アーカイブジョブのバッチが途中でスループット制限に当たると、一部のチケットは削除され一部はされません — そしてDynamoDBは通ったものを元に戻しません。ロールバックもなければ、分離もなく、「25件全部か皆無か」もありません。
全か無かのセマンティクスが必要なら — 「チケットをアーカイブ済みに移動かつ未対応チケットのカウンターを減らす、さもなくばどちらもしない」 — それはバッチではなくTransactWriteItemsです。トランザクションはコストが高く(各操作は2倍課金される)、100アイテムで頭打ちになりますが、バッチが意図的に提供しないアトミック性を与えてくれます。
未処理アイテムの扱い
正しいバッチの呼び出し側は必ず未処理セットを確認して再試行します。DynamoDBは、リクエストが全体としては受理されたが一部のアイテムが処理できなかったとき — 典型的には一過性のスロットリング — にUnprocessedItems/UnprocessedKeysを返します。
未処理のアイテムのみを、指数バックオフとジッター付きで再送信します。バッチを撃ちっぱなしとして扱うと、書き込みを静かに取りこぼします — 何か月も後にデータ欠損として表面化する類のバグです。
DynoTableでのバッチ書き込み
まずDynamoDB料金計算ツールで一括ジョブのコストを見積もりましょう — バッチはまとめる個別の書き込みと同じキャパシティを消費しますが、リクエスト数は少なくて済みます。
DynoTableでは、編集をローカルにステージし、効率的なバッチ書き込みとしてコミットする前にレビューできます — 多数の行にまたがる一括変更は、変更ごとに1回のAPI呼び出しではなくグループ化されたリクエストとして送られ、未処理アイテムの再試行も代わりに処理されます。

落とし穴と次のステップ
UnprocessedItems/UnprocessedKeysは必ずバックオフ付きで再試行する — 例外ではなく、想定内です。- 部分的失敗のロールバックはない。 アトミック性が必要? トランザクションを使いましょう。
- バッチ書き込みに更新はない —
BatchWriteItemはput/deleteのみです。属性を変更するにはUpdateItemに手を伸ばしましょう。 - 呼び出しごとの上限に注意 — 書き込み25 / 読み取り100 / 16 MB。より大きなジョブはページングしましょう。ページネーションを参照してください。
再試行ループをスクリプトで書かずに一括読み書きを実行したいですか? DynoTableをダウンロードして、テーブルを直接編集してください。


