寫入擴展 (Scaling Writes)
核心問題
寫入擴展往往比讀取更難。 讀取端工具(replica、cache)大家熟,但寫入是更大的挑戰:爆發性、高吞吐、又充滿競爭。面試官最愛問「黑色星期五訂單 4 倍怎麼辦」「跨年夜司機 3 倍怎麼辦」。
四個策略(組合運作)
1. 垂直擴展 + Database 選型 ── 先把單機/單 DB 壓榨到極限
2. Sharding 與 Partitioning ── 水平分散負載
3. Queue + Load Shedding ── 處理爆發流量
4. Batching + 階層式聚合 ── 改變寫入的「結構」
第一步:垂直擴展與 Database 選型
別預設「4~8 core + 機械硬碟」。200 CPU core、10Gbps 網路的機器並不罕見。先做 back-of-the-envelope 估算,確認真的碰到牆 → 詳見 02-Distributed-Systems/05-Numbers-to-Know。
Database 選型反映你對寫入模式的理解:
| Database | 寫入特性 | 代價 |
|---|---|---|
| Cassandra | append-only commit log,順序寫 → 10k+ 寫入/秒(傳統 RDB 約 1k) | 讀取差(要檢查多檔合併) |
| Time-series(InfluxDB/TimescaleDB) | 為 timestamp 高頻順序寫設計 + delta encoding | — |
| Log-structured(LevelDB) | append 而非 in-place | — |
| Column store(ClickHouse) | 分析型批次寫入 | — |
通用優化:停用 FK constraint / trigger、調整 WAL flush 頻率、減少 index 數量。
優化寫入常犧牲讀取,反之亦然。先搞清楚瓶頸在讀還是寫,針對性優化。面試別只說「用更好的資料庫」,要解釋為什麼 Cassandra 的 append-only 比 MySQL 的 B-tree 更新快。
第二步:Sharding 與 Partitioning
Horizontal Sharding(分 row)
把資料分散到多個 shard,理想下 10 台 server 處理 10× 寫入。Redis Cluster 範例:key 經 CRC hash 得 slot number → 分配給不同 node。
好 key(如 hash(userID))→ 資料均勻分散,真正達到 10× server = 10× 效能。
爛 key(如「用戶國家」)→ 中國 shard 過載、紐西蘭 shard 閒置。
原則:選讓各 shard 寫入量差異最小的 key,通常 hash 主要識別碼。
同時考慮讀取:若每個讀取都要從所有 shard 收集資料,讀取 overhead 很大。問自己「這個請求要打幾個 shard?多常發生?」詳見 05-Database-Advanced/02-Sharding、02-Distributed-Systems/04-Consistent-Hashing。
Vertical Partitioning(分 column)
把不同存取模式的資料拆成專門的表,再移到不同 DB instance:
| 拆出的表 | 存取模式 | 優化方向 |
|---|---|---|
| 貼文核心內容 | 一次寫、多次讀 | B-tree index、讀取優化 |
| 互動數字(讚/留言/觀看) | 高頻寫入 | in-memory / 專門計數器 |
| 分析資料 | append-only time-series | 行式壓縮 |
第三步:Queue 與 Load Shedding(處理爆發)
現實寫入流量不穩定。高峰 4× 代表平時只用 25% 容量 —— 大多數系統不這樣規劃。
擴縮要時間,且 DB 擴展過程常需停機/降吞吐 —— 偏偏發生在業務最忙時。
Write Queue(Kafka / SQS)
解耦「接受寫入」與「處理寫入」,吸收爆發。DB 以穩定速率消化,queue 當緩衝。
若 app 持續以比 DB 快的速度寫入,queue 無限成長、等待越來越久。只在爆發是短暫時用 queue,不要拿它 patch 連穩定負載都扛不住的 DB。詳見 09-Messaging-Coord/01-Data-Pipeline、10-Design-Patterns/03-Long-Running-Tasks。
Load Shedding(負載丟棄)
系統過載時,主動丟棄最不重要的寫入,保住重要的。Robotaxi 位置每幾秒更新一次 → 丟一筆,幾秒後就有更新鮮的;分析系統可暫丟 impression、保住 click。是「洩壓閥」,讓糟糕(過載)不演變成災難(崩潰)。
第四步:Batching 與階層式聚合
改變寫入結構。單筆寫入有 overhead(網路來回、transaction、index 更新),且 DB 批次寫入效率遠高於逐筆。
Batching(三層)
| 層次 | 做法 | 風險 |
|---|---|---|
| 應用層 | client 攢一批再送 | app 是 source of truth 時可能丟資料 |
| 中間處理層 | Like Batcher 一分鐘窗口聚合 100 讚 → 1 次寫入 | — |
| Database 層 | 調 flush 頻率(Redis 預設 100ms) | 「大鎚」解法,留給極端情況 |
若大多數貼文每小時才一個讚,一分鐘批次根本沒幫助 —— 要確認 batching 真的帶來收益。
Hierarchical Aggregation(階層式聚合)
最極端情況(分析/串流)。關鍵:聚合視圖可遞增建立。直播留言例:幾百萬人寫、又要傳給所有人 →
- 用 consistent hashing 把用戶分到 broadcast node,原本寫 N 個觀看者 → 寫 M 個 node
- 用 write processor 在時間窗口聚合按讚數 → 一批轉發給 root processor 合併
大幅降低任一系統的寫入量,代價是增加延遲。
常見 Deep Dive
8 → 16 shard,不能停機數小時。生產系統用漸進式遷移 + 雙寫(dual-write):同時寫舊 shard 和新 shard,讀取優先讀新 shard,保持可用性的同時漸進遷移。
即使選了最好的 key,爆紅推文(10 萬讚/秒)仍可能壓垮所在 shard。
- 方法一:固定拆分所有 key(split all keys):每 key 拆成 k 份(
post1Likes-0~-k-1),單 shard 寫入降 k 倍。缺點:資料/讀取量都 ×k - 方法二:動態拆分 hot key:偵測到熱才拆成 sub-key(如 100 個),讀取時聚合
- 只適用可聚合 metrics(讚/觀看/計數),不適用須原子的資料(profile)
- 讀寫者都要知道哪些 key 是熱的:大多數生產系統用「讀者永遠檢查所有 sub-key」(簡單,overhead 微不足道)
何時用 / 何時不用
| ✅ 適合 | ❌ 不適合 |
|---|---|
| Instagram(user ID sharding + vertical partitioning) | 根本不需要擴展時 —— 先 back-of-the-envelope 估算 |
| News Feed(名人寫入 fan-out) | 每種策略都有取捨:Queue→延遲、partition→讀取複雜、batch→延遲 |
| 搜尋應用(寫入前重前處理) | 最糟是製造出原本不存在的問題 |
| 直播留言(hierarchical aggregation 解 all-to-all) |
寫入擴展 = 降低每個元件的吞吐壓力。不管是分散到 10 個 shard、用 queue 平滑爆發、還是批次成 100 次批量,都是同一原則:讓每個元件只處理可管理的負載。
Related Notes
- 10-Design-Patterns/01-Scaling-Reads — 對偶問題:讀取擴展
- 05-Database-Advanced/02-Sharding — Shard key 三條件、跨 shard 挑戰
- 02-Distributed-Systems/04-Consistent-Hashing — Sharding routing、broadcast node 分配
- 10-Design-Patterns/03-Long-Running-Tasks — Queue + Worker Pool 解耦
- 09-Messaging-Coord/01-Data-Pipeline — Kafka、批次 vs 串流、階層式聚合
- 02-Distributed-Systems/05-Numbers-to-Know — 寫入吞吐量級、估算
- 10-Design-Patterns/Practice-Design-Patterns