這篇整理 DynamoDB partition key 反模式的識別與修復:寫入集中的 logical key 怎麼找、用 composite key 的 random shard 與 calculated shard 把寫入分散、capacity mode 對 hot partition 表現的影響,以及拓元售票案例的 6750x IOPS 擴展拆解。

Hot partition 是 partition key 的設計問題:單一 partition 的上限是 1000 WCU / 3000 RCU,跟 table 的總容量無關。以 event_id 當 PK 時,單一熱門場次的寫入全部落在同一個 partition,table 的總 capacity 再加多,那個 partition 仍然在 1000 WCU 被 throttle,其他 partition 閒置。

DynamoDB 適用度前置判讀:本篇假設 workload 已通過 DynamoDB 適用度 4 軸(PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定)— 詳見 single-table-design-pattern 開頭 4 軸前置判讀、本篇不重複展開。Partition key 反模式是 已選 DynamoDB 後 的 schema 修補議題;若 4 軸不成立、改回 SQL 比補 composite key 更合理。

跨 vendor 可逆性對照:MongoDB / DynamoDB / Cosmos DB 修改 partition key 的可逆程度不同(DynamoDB 要 backfill 到新 table、屬中度可逆),三家的並排比較在 DB3 Vendor Selection:document / KV / multi-model 三方選型。

核心機制:partition 上限是工程硬天花板

DynamoDB 把 capacity 抽象成 RCU / WCU、但底下仍是物理 partition。理解 partition 的 4 條硬規則:

  • 單 partition 上限:3000 RCU、1000 WCU、10GB storage;超過任一個觸發 partition split
  • 總容量公式:partition 數量 × 每 partition 上限、partition 數量由 vendor 自動管理
  • Adaptive Capacity:跨 partition 重新分配閒置容量、但 單 partition 仍硬上限;不解 single-key 集中
  • Splitting on heat:vendor 偵測 hot partition 後自動 split、有分鐘級延遲;突發流量來不及 split 就先 throttle

Amazon Ads 揭露同一 frame:「容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 是工程天花板」。Amazon Ads 90M reads/sec 靠的是 partition key 設計讓流量散到極多 partition、每個 partition 都在合理區間,而不是把單 partition 推到極限。

對應 knowledge card:hot partition、database-sharding。

Mode × Partition 交叉判讀

Hot partition 在 capacity mode 不同下表現不同、但根因都是 schema:mode 切換不解 partition 設計問題、partition 設計也不解 mode 選擇問題。

表現面Provisioned 模式On-demand 模式
Throttle 可見性WriteThrottleEvents 立即可見、CloudWatch 直接抓超過單 partition 上限時同樣被 throttle、記在 WriteKeyRangeThroughputThrottleEvents;SDK 自動重試把它攤成 SuccessfulRequestLatency p99 跳高
Application 表現ProvisionedThroughputExceededException 立即拋timeout / retry 加劇、看起來像「DynamoDB 變慢」
工程誤判風險低(exception 明顯)高(latency spike 容易被誤判成網路 / 應用層 / 下游服務問題)
解法改 PK schema(composite key / write sharding)改 PK schema(同左、不是切 mode)

Tixcraft 警惕段明示這個 frame:「DynamoDB 寫入排隊本身就是隱性限流」— 兩種 mode 超過單 partition 上限時都會記 throttle event、也都會被 SDK 重試攤成延遲,是同一個 schema 問題。

核心 frame:on-demand 不是 partition key 設計的逃避路徑。看到 on-demand 模式 latency spike、同時 WriteKeyRangeThroughputThrottleEvents 或 ReadKeyRangeThroughputThrottleEvents 不為零,第一個懷疑就是 hot partition、不是網路或應用層。

跟 on-demand-vs-provisioned 共軸閱讀:本篇從 schema 視角切入、那篇從 mode 選擇視角切入、合起來才是完整判讀。

修復流程

從 access pattern audit 到 composite key 設計的修復流程:識別寫入集中的 logical key、選 shard 數、設計 random shard 的 composite key、需要時改用 calculated shard、驗證寫入是否分散。

識別寫入集中的 logical key

審視 access pattern 表、抓出 寫入集中 的 key:

  • 單一 event / single user 寫入比例 > 10%(如熱門場次售票、bot 帳號)
  • 時間 bucket(PK = date / PK = hour)— 寫入永遠打當下 partition、舊 partition 閒置
  • 少數枚舉值(PK = status / PK = country 但只有 5-10 個值)

Tixcraft 揭露的具體場景:演唱會某一熱門場次的 event_id 為 PK、開賣瞬間 200K 用戶同時搶該場次、所有寫入集中到單一 partition。

選 shard 數

把單一 logical key 切成 N 個物理 shard。N 的估算邏輯:

1單 partition WCU 上限 = 1000
2留 20% buffer            = 800
3N = 單 logical key 預期峰值 WCU / 800(最小 shard 數)

Scope warning:「留 20% buffer、以 800 WCU 估 shard 數」是通用工程估算;拓元案例沒有公開用了幾個 shard,公開的是「composite key 分散」這個做法與「IOPS 從 20 衝到 135K」的結果。shard 數依單一 logical key 的預期峰值估算。

composite key 設計(random shard)

Composite Partition Key 把 logical key 加上 random suffix、把 hot logical 值分散到多個 partition:

1import random
2
3def write_order(event_id: str, user_id: str, order_data: dict):
4    # 寫入端:random suffix 分散到 N shard
5    shard = random.randint(0, N - 1)
6    pk = f"{event_id}#{shard}"
7    sk = f"USER#{user_id}#{timestamp}"
8    table.put_item(Item={"PK": pk, "SK": sk, **order_data})

讀取時 fan-out 到所有 shard:

1def query_event_orders(event_id: str):
2    results = []
3    for shard in range(N):
4        pk = f"{event_id}#{shard}"
5        page = table.query(KeyConditionExpression=Key("PK").eq(pk))
6        results.extend(page["Items"])
7    return results

calculated shard(讓同 user 仍可預測讀取)

random shard 的代價是讀取要 fan-out N 次。需要「同 user 寫入分散、但讀取 該 user 自己的資料時不要 fan-out」時,改用 calculated shard:

 1import hashlib
 2
 3def shard_for_user(user_id: str, n: int) -> int:
 4    h = hashlib.md5(user_id.encode()).hexdigest()
 5    return int(h, 16) % n
 6
 7def write_user_event(user_id: str, event_data: dict):
 8    shard = shard_for_user(user_id, N)
 9    pk = f"USER#{user_id}#{shard}"
10    # 同一 user_id 永遠拿到同一 shard

讀單一 user 只 query 一個 shard、讀全平台 user 才 fan-out N 個 shard。

選擇:

  • random shard:寫入完全均勻、但所有讀路徑都要 fan-out;適合 flash-sale / 緩衝層(讀路徑是後端慢消費、不在乎 fan-out latency)
  • calculated shard:寫入按 hash 均勻、user-level 讀路徑單 shard;適合 user-facing OLTP(user 讀自己資料延遲敏感)

驗證寫入是否分散

  • Contributor Insights 看 top-N PK 訪問是否平均分布
  • CloudWatch partition-level throttle = 0
  • Application 端 read fan-out latency 在預算內

Rollback boundary:composite key 寫入端可雙寫舊 + 新 key 一段時間(雙寫窗口)、application read 端 fallback 到舊 PK;不可逆動作只在「移除舊 key」階段。

失敗模式

production case 揭露的 5 個踩雷情境:

時間序 PK 集中

PK = date 或 PK = hour — 寫入永遠打當下 partition、舊 partition 閒置。每日凌晨換 partition 時瞬間冷啟動、寫入 latency spike。修法:date#shard 把當下 partition 拆 N 個物理 shard、或改用 event-stream pattern(每個 event 獨立 ID 為 PK)。

bot user 集中

PK = user_id、某個 bot 帳號每秒寫 1000 次、單 user_id 達 1000 WCU 上限。修法:

  • 偵測高頻 user 後動態加 shard suffix(user_id#shard0 … user_id#shardN)
  • 或在 application 層 rate limit、不讓 bot 直接打 DynamoDB

composite key 但 read 端忘記 fan-out

寫入分散到 100 shard、讀取只 query 一個 shard、結果不完整。修法:讀取必須 N 次 query 並 application 端合併、或建反向 GSI(GSI PK = event_id、不加 shard suffix;但 GSI 自己也會 hot partition)。

shard 數選太多 read fan-out latency 爆

N 過大時讀取 fan-out latency 從 5ms 變 200ms(具體數字隨網路延遲跟並行度變動、拓元案例未揭露用幾個 shard)。修法:shard 數依「單 logical key 預期峰值 / 800」估算、不是越多越好;read latency 跟寫入分散度是 trade-off。

on-demand 模式以為不會 hot partition

on-demand 仍受單 partition 1000 WCU 限制、超過時一樣回 throttling exception 並記 WriteKeyRangeThroughputThrottleEvents;AWS SDK 自動重試這類 exception,application 端多半只看到 latency 拉長。team 只看 application 錯誤率就以為沒問題、實際 p99 已經從 5ms 跳到 50ms。修法:on-demand 不是 partition key 設計的逃避路徑、依然要做 composite key;觀測上看 SuccessfulRequestLatency p99 不只看 throttle。跟 on-demand-vs-provisioned 共軸閱讀。

Anti-recommendation:access pattern 寫入分散自然均勻(如 UUID 為 PK、無 logical hot key),不要預先 sharding;增加 read 端 fan-out 複雜度沒帶來收益。

容量與觀測

CloudWatch metric:

  • WriteThrottleEvents / ReadThrottleEvents:按 table 跟 GSI 分;兩種 mode 的 throttle 都彙總在這裡,partition 上限造成的另看 WriteKeyRangeThroughputThrottleEvents / ReadKeyRangeThroughputThrottleEvents
  • SuccessfulRequestLatency p99:SDK 重試把 throttle 攤成延遲,hot partition 時與 KeyRange throttle metric 一起跳高
  • partition-level metric 透過 Contributor Insights 看,不是 CloudWatch 預設 panel

Contributor Insights 必開:top-N partition key by access frequency;每月 cost ~$0.02 per million event、值得開。沒開 Contributor Insights 就看不到 partition-level 分布、只能從總 capacity 跟 throttle 反推。

DynamoDB Streams:可用來抓 hot key debugging — 寫入事件落 Lambda 後統計 PK 頻率。

Mode × partition 觀測差異(重申交叉判讀):

  • Provisioned 模式:看 WriteThrottleEvents、立即可見
  • On-demand 模式:看 SuccessfulRequestLatency p99、看 partition-level Contributor Insights、看 application 端 timeout / retry trend

接回 9.4 Saturation Discovery 的 partition 章節。

邊界與整合

Tixcraft 6750x 擴展的工程拆解

Tixcraft 揭露的數字:IOPS 從 20 衝到 135K(6750 倍)、6 servers 變 800 servers、總成本 $4200、throttle rate 0.26%。但「6750x 擴展」是 partition key 均勻分散 + 架構解耦 的組合結果,而不是 DynamoDB 本身的特性:

  • partition key 均勻:composite key(event_id 加分散 suffix)把單一熱門場次散到多個 partition、每個 partition 都在合理區間(case 揭露概念、未揭露具體 shard 數)
  • 架構解耦:DynamoDB 當 durable queue、後端傳統 server(金流 / 票庫)用自己節奏消費、不被前端 130x 流量拖垮(見 single-table-design-pattern 的 durable queue 段)
  • 付款層獨立:付款由另一層獨立服務處理,不經過 DynamoDB,避免搶票流量影響付款

讀者該學的不是「DynamoDB 能撐 6750x」、是「composite key + 架構解耦 + 服務分層」三件事一起做才能撐。