DynamoDBのホットパーティション問題でスロットリング地獄から脱出した書き込みシャーディング戦略

本番環境でのフラッシュセール開始直後、モニタリングダッシュボードが真っ赤に染まり、アプリケーションログが ProvisionedThroughputExceededException で埋め尽くされる──これは多くのバックエンドエンジニアが経験する悪夢です。原因は明白で、特定のアイテム(人気商品や集計カウンター)にアクセスが集中するホットパーティション問題でした。本記事では、この物理的な制限を回避するために私たちが採用した、接尾辞を利用した書き込みシャーディングの実装と、それを支えるAWSデータベースNoSQLモデリング手法について解説します。

なぜスロットリングが起きるのか:パーティションの限界

DynamoDBは無限にスケールするように見えますが、単一のパーティションには物理的なハードリミットが存在します。具体的には、1つのパーティションキーに対し、書き込みは毎秒1,000 WCU、読み込みは毎秒3,000 RCUが上限です。AWS公式ドキュメントにもある通り、これを超えるとアクセス集中(Skew)が発生し、テーブル全体のスループットに余裕があってもリクエストは拒否されます。

警告:オートスケーリングを設定していても、単一キーへの集中アクセスによるホットパーティション問題は解決できません。これはキャパシティの問題ではなく、物理パーティションの配置設計の問題だからです。

私たちが直面したのは、特定のキャンペーンIDに対して数万ユーザーが同時に「参加」ステータスを書き込むシナリオでした。当然、単純なDynamoDB設計では耐えきれません。

解決策:ランダムサフィックスによる書き込みシャーディング

この問題を解決するための最も堅牢なアプローチは、書き込みシャーディング(Write Sharding)です。これは、本来単一であるはずのパーティションキーに「接尾辞(Suffix)」を付与し、論理的に別のアイテムとしてデータを分散させる手法です。

例えば、Campaign#101 というキーに書き込む代わりに、Campaign#101#1Campaign#101#N という複数のキーに分散させます。以下は、Python(Boto3)を用いた実装例です。

import boto3
import random
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('CampaignEvents')

# シャードの総数(トラフィック予測に基づいて設定)
# 例: 予想WCUが10,000なら、1パーティション1,000WCU制限のため、余裕を見て15〜20シャード必要
SHARD_COUNT = 20

def record_participation(campaign_id, user_id):
    # 1. ランダムな接尾辞を生成して負荷を分散させる
    shard_suffix = random.randint(1, SHARD_COUNT)
    
    # 2. シャーディングされたパーティションキーを構築
    # PK: Campaign#101#3, SK: User#9999
    sharded_pk = f"Campaign#{campaign_id}#{shard_suffix}"
    
    try:
        response = table.put_item(
            Item={
                'PK': sharded_pk,
                'SK': f"User#{user_id}",
                'JoinedAt': datetime.utcnow().isoformat(),
                'Status': 'ACTIVE'
            }
        )
        return response
    except Exception as e:
        print(f"Error writing to shard {shard_suffix}: {e}")
        raise

# 使用例
# record_participation("101", "user_555")
実装のポイント:シャード数(SHARD_COUNT)は固定値にするか、設定ファイル等で動的に変更できるようにしておくことが重要です。トラフィックが予想を超えた場合、デプロイなしで値を増やせると障害復旧が迅速になります。

分散データの集約:GSIとScatter-Gatherパターン

書き込みを分散させると、今度は「特定のキャンペーンに参加した全ユーザーを取得する」といった読み込みクエリが複雑になります。Query 操作は単一のパーティションキーに対してしか行えないからです。

これに対処するには、以下の2つの戦略があります:

  1. アプリケーション側での並列クエリ(Scatter-Gather): 全シャード(1~20)に対して並列で Query を発行し、アプリ側で結果をマージする。
  2. GSI(グローバルセカンダリインデックス)の活用: 書き込みシャーディングはPKに行いますが、GSIのパーティションキーには分散させない値(または別の粒度)を使用し、検索要件に合わせる。

例えば、StackOverflowの議論でもよく見られるパターンですが、読み込み頻度が書き込みほど高くない場合、単純に全シャードをループしてクエリするのが最もコスト効率が良いです。

戦略 書き込みパフォーマンス 読み込みの複雑さ 適しているケース
単一キー (Before) 低 (1000 WCU限界) 低 (1回のQuery) 低トラフィックなマスタデータ
書き込みシャーディング (After) 極高 (理論上無限) 中 (N回の並列Query) ログ収集、投票システム、イベント参加

結論:物理制約を設計で超える

DynamoDBにおけるホットパーティション問題は、サービスの成長と共に必ず直面する壁です。しかし、適切なNoSQLモデリング書き込みシャーディングを導入することで、この壁は容易に突破できます。

安易にオンデマンドキャパシティへ切り替えてコストを増大させる前に、まずはデータのアクセスパターンを見直し、キー設計による負荷分散を検討してください。ログに ProvisionedThroughputExceededException が出たときこそ、アーキテクチャを進化させる絶好の機会です。

Post a Comment