何が起きているか
DynamoDBがリクエストを拒否しているのは、テーブルがトラフィックに追いつけていないためです。すべてのテーブルには**RCU(読み取りキャパシティユニット)とWCU(書き込みキャパシティユニット)**で測定された固定の読み書き予算があります。その予算を超えると――たとえ一瞬であっても――DynamoDBは即座にリクエストをスロットリングします:
ProvisionedThroughputExceededException: The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API.
よくある原因としては、突然のトラフィックスパイク、バルクインポートジョブ、またはベーステーブルより少ないキャパシティでプロビジョニングされたグローバルセカンダリインデックス(GSI)が挙げられます。どれが原因かを特定しましょう。
ステップ1 — スロットリングされている箇所を特定する
まだキャパシティ設定には手をつけないでください。まずCloudWatchでボトルネックを確認します。
# 特定テーブルのスロットルメトリクスを一覧表示
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name ReadThrottleEvents \
--dimensions Name=TableName,Value=YourTableName \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 300 \
--statistics Sum
ReadThrottleEventsとWriteThrottleEventsの両方で実行してください。GSIには独自のスロットルカウンターがあります――個別に確認するには、ディメンションにName=GlobalSecondaryIndexName,Value=YourIndexNameを追加してください。
次に、現在のプロビジョニング値を取得します:
aws dynamodb describe-table --table-name YourTableName \
--query 'Table.{RCU:ProvisionedThroughput.ReadCapacityUnits, WCU:ProvisionedThroughput.WriteCapacityUnits, BillingMode:BillingModeSummary}'
これでベースラインが得られました。5分間で500件以上のスロットルイベントが発生している場合は、毎分10件ずつじわじわ発生している場合とは異なる問題です。
ステップ2 — エクスポネンシャルバックオフを追加する(最速の対処法)
リトライロジックなしでDynamoDBを呼び出している場合、それが最初の問題であり、キャパシティの問題ではありません。AWS SDKにはエクスポネンシャルバックオフが組み込まれています。有効になっていて、積極的に設定されていることを確認してください。
Python(Boto3):
import boto3
from botocore.config import Config
config = Config(
retries={
'max_attempts': 10,
'mode': 'adaptive' # スロットリング時に自動的にバックオフする
}
)
dynamodb = boto3.resource('dynamodb', config=config)
table = dynamodb.Table('YourTableName')
Node.js(AWS SDK v3):
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({
maxAttempts: 10, // SDKのデフォルトは3 — スロットリングされたワークロードには低すぎる
});
Boto3のmode: 'adaptive'がよりスマートな選択です――リアルタイムのエラーレートを計測し、それに応じてリトライのタイミングを調整します。一時的なスロットリングの多くは、キャパシティに手をつけることなく、これだけで例外を止めることができます。
ステップ3a — プロビジョニングキャパシティを増加する
スロットリングが一時的なスパイクではなく持続している場合は、上限を引き上げる時です。CLIから数秒で実行できます:
# 書き込みキャパシティを100 WCUに引き上げる
aws dynamodb update-table \
--table-name YourTableName \
--provisioned-throughput ReadCapacityUnits=50,WriteCapacityUnits=100
独自のスループット設定を持つGSIの場合:
aws dynamodb update-table \
--table-name YourTableName \
--global-secondary-index-updates \
'[{"Update":{"IndexName":"YourGSIName","ProvisionedThroughput":{"ReadCapacityUnits":50,"WriteCapacityUnits":50}}}]'
キャパシティの変更は数秒で反映されます。知っておくべき制限事項として、AWSではテーブルごとに1日(UTC)に最大4回の減少が許可されています。増加は無制限なので、自由にスケールアップできます。
ステップ3b — オンデマンドモードへ切り替える(よりシンプルな選択肢)
トラフィックが予測不可能な場合は、キャパシティプランニングを完全にスキップしてください。オンデマンドモードでは、DynamoDBが任意のリクエストレートを自動的に処理します。
aws dynamodb update-table \
--table-name YourTableName \
--billing-mode PAY_PER_REQUEST
トレードオフとして、オンデマンドは適切にチューニングされたプロビジョニングキャパシティと比べて、100万リクエストあたり約6〜7倍のコストがかかります。毎日1,000万回の読み取りを安定的に行うテーブルでは、その差は積み重なります。しかし、急激なピークと長い閑散期を持つワークロードでは、過剰プロビジョニングよりもコストが安くなることがよくあります。実際のトラフィックパターンを把握したら、プロビジョニングに戻しましょう。
ステップ3c — オートスケーリングを有効にする(両方のいいとこどり)
オートスケーリングは使用率を監視し、リアルタイムでプロビジョニングキャパシティを調整します。70%のターゲットを設定することで、スロットリングが発生する前に突然のスパイクに対して30%のヘッドルームバッファが確保されます。
# テーブルをスケーラブルターゲットとして登録
aws application-autoscaling register-scalable-target \
--service-namespace dynamodb \
--resource-id "table/YourTableName" \
--scalable-dimension "dynamodb:table:WriteCapacityUnits" \
--min-capacity 5 \
--max-capacity 500
# スケーリングポリシーを設定
aws application-autoscaling put-scaling-policy \
--service-namespace dynamodb \
--resource-id "table/YourTableName" \
--scalable-dimension "dynamodb:table:WriteCapacityUnits" \
--policy-name "WriteAutoScaling" \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration \
'{"TargetValue": 70.0, "PredefinedMetricSpecification": {"PredefinedMetricType": "DynamoDBWriteCapacityUtilization"}}'
オートスケーリングは持続的な負荷に反応することに注意してください――通常、有効になるまでに1〜2分かかります。最初の60秒でテーブルに急激なトラフィックスパイクが発生した場合は対処できません。
ステップ4 — ホットパーティションの圧力を軽減する
総キャパシティが問題ではない場合もあります。単一のパーティションが原因のことがあります。DynamoDBはハッシュキーによってデータを分割します――多くのリクエストが同じキーを対象とすると、そのパーティションが上限に達し、他のパーティションはアイドル状態になります。1,000 WCUをプロビジョニングしていてもスロットリングが発生する場合があります。
- 連続または単調増加するキーを避ける — 自動インクリメントIDとパーティションキーとしてのUnixタイムスタンプは、ホットパーティションの典型的な原因です。UUIDを使用するか、
userId#shard-3のようにランダムなサフィックスを追加してください。 - バルク書き込みを分散させる — 10万行をインポートする場合、順番に書き込むのではなく、異なるパーティションキーに分散させてください。
- 読み取りが多いワークロードにDAXを追加する — DynamoDB AcceleratorはDynamoDBの前に置くインメモリキャッシュで、マイクロ秒レイテンシで読み取りスパイクを吸収します。2ノードのDAXクラスターは毎秒数千万回の読み取りを処理できます。
- 書き込みをバッチ処理する —
BatchWriteItemは最大25件の書き込みを1回のAPI呼び出しにまとめます。レートリミットと組み合わせてキャパシティ内に収めてください:
# Python: レートリミット付きバッチ書き込み
import time
def batch_write_with_limit(table, items, wcu_limit=50):
chunk_size = 25 # DynamoDBのBatchWriteItemあたりの最大アイテム数
for i in range(0, len(items), chunk_size):
chunk = items[i:i+chunk_size]
with table.batch_writer() as batch:
for item in chunk:
batch.put_item(Item=item)
time.sleep(1) # スループットを制御するためチャンク間で1秒待機
修正の確認
変更を加えたら、直近5分間のスロットルデータを取得してください:
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name WriteThrottleEvents \
--dimensions Name=TableName,Value=YourTableName \
--start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 60 \
--statistics Sum
カウントが0であれば問題ありません。また、ConsumedWriteCapacityUnitsをProvisionedWriteCapacityUnitsと比較してください――常に80%を超えている場合、次のトラフィックスパイクで再びスロットリングが発生する可能性があります。持続的な負荷に対しては使用率を80%以下に保ちましょう。
重要なポイント
- **まずアダプティブリトライを設定する。**無料で2分で完了し、キャパシティに手をつけることなく一時的なスロットリングのほとんどを吸収します。
- スロットルイベントにCloudWatchアラームを設定する。
ReadThrottleEventsとWriteThrottleEventsに毎分10件以上のしきい値でアラームを設定して、ユーザーが報告する前に問題を検出してください。 - **GSIのキャパシティはベーステーブルとは独立している。**低いWCUで頻繁にクエリされるインデックスは、見逃しやすい一般的な隠れた原因です。
- **オンデマンドが常に高コストな選択とは限らない。**1日20時間アイドル状態だがピーク時に大きなバーストが発生するテーブルでは、PAY_PER_REQUESTが過剰プロビジョニングよりもコスト面で有利になることがよくあります。

