DynamoDB Partition Key 反模式與 Write Sharding:composite key 修復跟 mode × partition 交叉判讀
這篇整理 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 resultscalculated 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/ReadKeyRangeThroughputThrottleEventsSuccessfulRequestLatencyp99: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 模式:看
SuccessfulRequestLatencyp99、看 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 + 架構解耦 + 服務分層」三件事一起做才能撐。
Sibling 與 cross-link
- single-table-design-pattern — PK 設計上游、本篇是 PK 不天然均勻時的補救
- on-demand-vs-provisioned — capacity mode 對 hot partition 表現的影響、mode × partition 交叉判讀的另一視角
- gsi-lsi-design — GSI 自己也會 hot partition、GSI PK 設計獨立 review
- Migration playbook:composite key migration 屬「topology re-layout」、寫入需雙軌;對應 migration playbook methodology
- 拓元 Tixcraft:售票搶購的瞬間爆量架構:售票模式的 6750x 擴展細節、composite key 是工程選擇而非 vendor 魔法
- Amazon Ads:DynamoDB 9000 萬 reads/sec 的廣告事件量測:容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 是容量天花板
- NTT DOCOMO Lemino:3 個月達 500 萬 MAU 的串流後端:connection-free scale 的另一面是 partition 設計責任
#backend #database #dynamodb #partition-key #hot-partition #write-sharding #deep-article