主題
CH 05
Part II · 分散式資料
Ch5 複製
複製解決可用性、製造一致性問題。
— 本站章首引
Ch 5 / 12·整本已讀 0%(0 / 12)
TL;DR · 本章重點
- 複製 = 在多個節點存同一份資料的副本:動機是低延遲(地理近)、高可用(容錯)、高讀吞吐(讀分散)。
- 三種架構:Single-Leader(最常見,PostgreSQL/MySQL)、Multi-Leader(跨資料中心)、Leaderless(Dynamo 風格,Cassandra/Riak)。
- 同步 vs 非同步的權衡:同步保證一致性但任一節點掛掉整體卡死;非同步快但有資料丟失風險。實務常用「半同步」。
- 複製延遲三大問題:read-your-writes、monotonic reads、consistent prefix reads —— 要靠應用層或路由策略解決。
- Leaderless(去 leader)架構靠多數票決(quorum 法定人數)達成一致——寫入要過半(W)、讀取也要過半(R)、重疊處就是最新資料(W + R > N)。Dynamo 風格的 sloppy quorum(鬆散法定人數)——寫不到原節點時先暫存到鄰居、等原節點回來再補——換來可用性但不再保證讀最新。版本向量(version vector,記錄每個節點寫了幾版)用來解決並發寫衝突。
5.1 為什麼需要複製
- 降低延遲:把資料放在使用者近的資料中心
- 提高可用性:節點掛了還能服務
- 提高讀吞吐量:讀請求可分散到 follower
但寫入需要被傳播到所有副本 → 這正是複雜度來源。
如果你是前端開發者:你已經踩過這章的痛點
這幾個情境其實都是「複製延遲」造成的:
- POST 一筆訂單後立刻 GET 列表,竟然看不到剛建的——你打到的 read replica 還沒同步到。違反 read-your-writes 保證。
- 使用者刷新兩次,第二次看到比第一次更舊的資料(時光倒流)——兩次請求被路由到不同 follower、其中一個更新較慢。違反 monotonic reads 保證。
- 樂觀更新(optimistic UI)撞到 server 回應:UI 已顯示「已按讚」、但 server 回 409 conflict——這是 multi-leader / leaderless 的並發寫衝突,要 client retry + CRDT 或 last-write-wins。
典型 SDK 的處理:
- Firestore 預設啟用 offline cache + local mutation,所以 POST 立刻 GET 拿到本地 cache,看似 read-your-writes ✓——但是用 cache 不是真的同步到 server。(注意:Firestore server-side 在單 region 內本就強一致;offline cache 是把網路延遲遮掉、不是弱化保證。要看「server 端其他 client 多久看得到」才是真正的 replication 問題。)
- Supabase / PostgREST 預設讀 leader(沒 replica)→ 沒這問題;但若啟用 read replicas(PG hot standby)就有。
- 解法 a:寫後一段時間內 sticky 到 leader;解法 b:寫後帶 timestamp,要求 read replica 至少同步到該時間。
5.2 Single-Leader 複製
Client write
↓
+-------+
|Leader | → replication log
+-------+
/ \
+--------+ +--------+
|Follower| |Follower|
+--------+ +--------+
↑ ↑
Client read Client read1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
同步 vs 非同步
Client
Leader
Follower 1
Follower 2
全同步(理論模型)— 等所有 follower ACK 才回 client
1
write x=1
2
replicate
3
replicate
4
ACK
5
ACK
6
OK ✓
強一致;任一 follower 慢就卡 — 實務上極少這樣做
非同步複製 — 立刻 ack、背景傳(MySQL 預設)
7
write x=2
8
OK ✓
9
replicate(背景)
10
replicate(背景)
快、但 leader 掛掉時尚未複製的寫會丟失
半同步 — 至少 1 個 follower 同步即可(實務常見折衷)
11
write x=3
12
replicate
13
replicate
14
ACK
15
OK ✓
16
ACK(晚到也沒關係)
一個 follower 同步就保證資料不丟 — MySQL 8.0+ `rpl_semi_sync_source_wait_for_replica_count=1`
- 同步:寫入要等 follower ACK → 強一致但 follower 掛了卡寫入。實務範例:PostgreSQL
synchronous_commit=on+synchronous_standby_names。 - 非同步:寫入立刻 ack → 快,但 leader 掛了可能丟資料。MySQL 預設。
- 半同步:至少一個 follower 同步、其餘非同步 → 實務常見折衷。MySQL 5.7 的
rpl_semi_sync_master_wait_for_slave_count=1、MySQL 8.0+ 改名為rpl_semi_sync_source_wait_for_replica_count(去除 master/slave 用詞,查官方文件要對應新版)。
DDIA 的「同步」≠「全部 follower ACK」
書中 / 業界談「synchronous」更常指至少一個 standby 確認(與本書「半同步」實質相同),極少系統真的要求「所有 follower ACK」才回 client——那會把可用性殺爆。PostgreSQL 的 synchronous_standby_names 也支援 FIRST 1 (s1,s2) / ANY 2 (s1,s2,s3) 等配置,多數情況不是「所有」。上圖第一個 sequence 的「全部 ACK」是為了視覺對比的極端情境、非真實實作;MySQL 的「半同步」另設一格只是因為該特定模式名稱,邏輯上不是第三類。
Failover(主節點切換)的麻煩
- 怎麼確定 leader 真的掛了?(網路抖動 vs 真當機)
- 選誰當新 leader?(資料最新者)
- 客戶端如何切到新 leader?
- 舊 leader 復活後,被 demote 的它怎麼處理「比新 leader 還新的寫入」?→ split-brain
GitHub 2012 故障:自動 failover 把舊 leader 的資料覆蓋掉新 leader,丟失資料。
實務工具錨點:你會怎麼真的做 PG / MySQL failover?
書本講 failover 三大麻煩很對、但 SRE 自學者會想「那實務上是怎麼解的」——記得工具名、production 撞到再深入:
| DB | 主流 failover 工具 | 核心機制 |
|---|---|---|
| PostgreSQL | Patroni | etcd / Consul / ZooKeeper 當 DCS(Distributed Configuration Store) + leader election 走 Raft + pg_rewind 處理時序倒流 |
| MySQL | Orchestrator / MHA(已 deprecated)/ InnoDB Cluster | MGR(MySQL Group Replication)+ Group Communication System + Paxos 內建 |
| MongoDB | 內建 Replica Set | Raft 變體、自動 election、priority 控制誰能當 primary |
| Redis | Sentinel / Cluster | Sentinel 用 gossip + quorum 決定 master |
「真壞了還是只是抖了」的判斷(SRE 最痛的決定):
- Patroni 用 etcd watchdog:master 失去 etcd lease → 自動降級為 replica;新 master 拿到 lease → 提升為 primary
- 「抖了」(暫時性失聯)+「自動切換」可能比「繼續等」造成更多問題(split-brain 風險)——所以 Patroni 預設
ttl=30秒 +loop_wait=10秒、給 master 機會恢復、不要太急
Ch9 §9.5 共識章會更深入:Raft 的 leader election 就是 Patroni / Orchestrator 底層在用的東西、Ch8 fencing token 是擋 zombie leader 的關鍵——這幾章串起來、你就理解「PG / MySQL failover 不是黑魔法、是 Ch5-Ch9 內容的工程化」。
W47:PG / MySQL / Redis 三家 replication「預設行為」落差表
mid-level 面試會被追問「你用的 DB 預設是 sync 還是 async?掛主節點會丟資料嗎?」—— 三家 OSS DB 的預設值差異常被誤解:
| DB | 預設模式 | 切 sync / semi-sync 的開關 | leader 掛常見資料行為 | 典型坑 |
|---|---|---|---|---|
| PostgreSQL | async(streaming replication) | synchronous_commit = on + synchronous_standby_names = 'FIRST 1 (s1,s2)' | 未 flush 的 WAL 丟失(async)/可保住已 commit(sync) | synchronous_commit=on 但 synchronous_standby_names 為空 = 仍是 async(最常見配錯) |
| MySQL | async(binlog → replica relay) | rpl_semi_sync_source_enabled=ON + rpl_semi_sync_source_wait_for_replica_count=1(8.0+ 改名前是 master/slave) | async 預設可能丟 0.1-1s 寫入;semi-sync 仍有 phantom write 風險 | InnoDB sync_binlog=0 + async replication = 雙重風險、production 建議 sync_binlog=1 |
| Redis | async(master → replica) | WAIT N M client command 阻塞直到 N 個 replica ACK(非 server-side 設定、是每次寫入決定) | 主掛即丟、Sentinel failover 自動切但確認資料丟失(min-replicas-to-write 是擋寫不是擋丟) | WAIT 不是 sync 模式、只是「等夠多人收到再回」、Sentinel 仍可能選到 lag 的 replica |
面試金句:「三家預設都是 async;要強保證得各自配 sync flag、且都有微妙陷阱」。詳細參數演進史見各家官方 release notes、別只 copy stackoverflow 老答案。
5.3 複製延遲的三個問題
1. Read-your-writes(讀自己的寫)
你發了則貼文,刷新後看不到 → 因為 read 走的 follower 還沒收到。 解法:寫後一段時間內,自己的請求走 leader;或記住 client 寫入時的 log position,讀時要求 follower 進度 ≥ 該值。
2. Monotonic Reads(單調讀)
連續刷新兩次,第一次看到評論、第二次評論不見了(因為兩次打到不同 follower,第二個更舊)。 解法:同一使用者固定打到同一個 follower。
3. Consistent Prefix Reads(一致前綴讀)
觀察者看到「答案早於問題」—— 兩個分區間的因果順序被破壞。 解法:把因果相關的寫入放在同一分區;或實作因果一致性協定(用 vector clock 記錄因果依賴,並讓接收端依因果序遞送訊息)。注意 vector clock 本身只是工具,需配合「延後遞送」的傳遞層才能解決問題。
本土場景:中華電信 MOD / KKTV / friDay 直播即時轉播
MOD 等 IPTV 服務有經典的 leader-follower 複製場景:
- 節目資料中心(leader):節目表、收藏、觀看歷史的 source of truth
- 各 CDN 節點(follower):把節目表 + 用戶觀看狀態複製到全台機房、用戶從最近的節點讀
- 複製延遲的真實後果:你在桌機按「加到我的收藏」、3 秒後手機 App 重新整理還是看不到——這就是 read-your-writes 違反;MOD App 用 session sticky(你寫過後一段時間內讀都鎖到 leader)解這個問題
跨機房寫入更難:北部資料中心宕機切到南部 follower 當 leader(failover)、原本還沒同步完的觀看歷史可能丟失幾秒——這就是同步 / 非同步複製的 trade-off。DDIA 原書用 GitHub commit / Twitter timeline、本站用 MOD / KKTV、底層的 leader / follower / replica lag 是同一套。
本土場景:中華電信 MOD 同步異常 + 蝦皮商品列表
台灣讀者熟悉的 replication lag 場景:
- 蝦皮上架新商品後立刻刷新看不到:賣家後台寫到 master、買家端列表頁是 read replica、replication lag 1-3 秒、買家就看到舊列表——這正是 §5.4 read-your-writes 問題(賣家自己的寫應該立刻能讀到、SDK / proxy 層要把該 session 的讀導回 master 或加 read-after-write 路由)
- 中華電信 MOD 帳號變更後 5 分鐘才生效:你升級套餐後不是立刻能看新頻道——後台多區資料庫非同步複製,營業系統 commit 完還要傳到頻道授權系統。這是「為什麼跨系統最終一致是常態」的真實面
- Line 訊息群組換管理員後其他成員看不到:寫到主庫 → 推送服務從 follower 讀群組設定 → follower 還沒跟上 → 推送用了舊管理員清單 → 通知漏發
DDIA 原書用 GitHub / 維基百科、本站用蝦皮 / MOD / Line、底層的 leader-follower lag、read-your-writes、monotonic read 都是同一套問題。
5.4 Multi-Leader 複製
每個資料中心一個 leader,互相複製。
優點:每個 DC 寫入低延遲、容忍跨 DC 網路問題 缺點:寫入衝突(同一筆資料在兩個 DC 同時被改)
衝突解決策略
| 策略 | 說明 | 風險 |
|---|---|---|
| Last-Write-Wins (LWW) | 用時間戳,最大勝(Cassandra 預設、DynamoDB 預設) | 時鐘不同步 → 並發寫被靜默丟失 |
| 應用層 merge | 給 client 看到衝突、自己決定(Riak 預設:keep all siblings、讀時讓 client resolve) | 複雜 |
| CRDT | 自動可合併資料結構 | 資料結構受限 |
| Mergeable persistent data | 像 git,保留分支 | 仍需應用處理 |
Riak 預設不是 LWW
Riak 預設啟用 allow_mult=true —— 並發寫保留 siblings、由 client 讀時 resolve(或用 CRDT data type 如 riak_dt_map 自動合併)。LWW(allow_mult=false)是 Riak 的可選設定、Basho 文件明確不建議用於可能有並發寫的場景(會靜默丟資料)。本表已修正:Cassandra / DynamoDB 預設 LWW、Riak 預設 sibling merge。
CockroachDB / Spanner / YugabyteDB ≠ LWW
這三家常被誤認為「用 HLC / TrueTime 做 LWW 衝突解決」,其實不是:它們是 Raft single-leader-per-range、每個資料範圍只有一個 leader 寫入,根本不存在並發 multi-leader 寫衝突。HLC / TrueTime 在這幾家用於 MVCC 時間戳排序與外部一致性(external consistency),不是用於解多寫者衝突。真正在「並發寫到不同副本、用時間戳挑勝者」的是 Cassandra / DynamoDB 這類 Dynamo-style leaderless 系統。
5.5 Leaderless 複製(Dynamo 風格)
每個 client 直接寫多個副本,讀也讀多個副本。
Quorum 公式
- N:副本總數
- W:寫入需 ACK 數
- R:讀取需回應數
- W + R > N 在嚴格 quorum 下,可保證讀集與寫集交集非空——若全網僅一個 writer、這即等於「讀到最新」;多 writer 並發場景下還需 version vector 或時間戳判定何者為最新(quorum 本身不解決「誰是最新」、只解決「最新值存在於讀集」)
白話三句話
假設 N = 3 個副本,設 W = 2、R = 2:
- 寫入時,要求 2 個副本確認才回 client OK(W=2)
- 讀取時,問 2 個副本(R=2),挑時間戳最新的當答案
- 因為 2 + 2 = 4 > 3,讀到的 2 個副本裡至少有 1 個是剛剛寫過的 —— 從這個副本拿到的就是最新值
這就是 W + R > N 公式的全部直覺。容忍 N − max(W, R) = 1 個節點掛掉。
例:N=3, W=2, R=2 → 容忍 1 個節點掛掉
Quorum 並非無條件保證
W+R>N 在下列情況不保證讀到最新值:
- Sloppy quorum:節點掛時,把寫入「臨時」轉交給原本不屬於該 key 的節點(hinted handoff)。關鍵問題:寫跑到「替補節點」,但讀仍對原 home 節點集合做 quorum read → 寫集與讀集完全不交集 → 讀就讀不到剛寫的最新值。Dynamo 用 hinted handoff 寫、replica 修復後背景傳回 home(Cassandra/Riak 預設啟用,以換取更高可用性)。
- 並發寫:W+R>N 只解決「最新值存在」,不解決「誰是最新」—— 需搭配版本向量或時間戳判定。
- 節點故障切換期間:副本切換時讀寫可能短暫打到不同節點集合。
純粹的 quorum 只是線性一致儲存的必要條件之一,不充分。要真正線性一致還需 read repair + write-back(ABD 演算法)。
修復機制
- Read repair:讀到舊資料時順便寫回最新值
- Anti-entropy process:背景同步副本
版本向量(Version Vector)
解決並發寫衝突:每個副本維護自己的計數器,比較兩個版本判斷「誰是後代、誰是並發」。
A 寫 v=1, vector=[A:1]
B 並發寫 v=2, vector=[B:1] ← 兩者無法比較,是衝突
合併後, vector=[A:1, B:1]1
2
3
2
3
Version Vector ≠ Vector Clock
兩者形式相似但用途不同:
- Vector Clock(Fidge 1988 / Mattern 1989 各自獨立提出):每個 process 一個計數器,用於分散式系統的因果序判定。常被誤掛在 Lamport 名下——Lamport 1978 那篇是 scalar Lamport timestamp(純量時鐘)、不是 vector clock。
- Version Vector(Parker 1983;Dynamo 後普及):每個 replica 一個計數器,用於 KV store 的並發寫衝突偵測。
DDIA Ch5 §5.4 的「版本向量」指後者。實務上 Dynamo / Riak / Voldemort 文獻幾乎都用 version vector,但口語常被簡稱為「vector clock」造成混淆。Ch9 §9.3 會回來談 causal order。
5.6 複製拓樸選型決策樹
把 Single-Leader / Multi-Leader / Leaderless 三大架構的選型壓成一張圖:
Q單 region 還是多 region?
單 region 為主(低延遲 + 強一致)
Single-Leader — PG / MySQL streaming + 半同步 + read replica
跨 region 但只一處寫
Single-Leader 跨 region — async follower 在其他 region;讀就近 / 寫回主
多 region 寫入(各 DC 都要能寫)
Q衝突可接受嗎?
衝突不可接受(金融 / 強一致)
Single-Leader + 在最近 region 部署 leader — 承擔跨 region 寫延遲(毫秒到秒級)
衝突可容忍(計數器 / 文件協作)
Q需要 commutative 結構嗎?
是
Multi-Leader + CRDT — Yjs / Automerge / Riak DT
否、用 LWW 接受最後寫贏
Multi-Leader + LWW — Tungsten Replicator / SymmetricDS、或自寫 LWW on Kafka / DynamoDB Global Tables
寫入 throughput 是瓶頸(大量 ingest)
Q需要範圍查詢嗎?
否、純 KV 點查
Leaderless — Cassandra / Riak / ScyllaDB;Quorum + read repair
是
回 Single-Leader + partition 分擔寫
選型快速結論:
- 預設選 Single-Leader(多數 web app 適用)
- 跨 region 寫但能容忍衝突 → Multi-Leader + CRDT / LWW
- 寫密集 + KV 點查 → Leaderless(Cassandra / Riak)
- **不要為了「未來可能跨 region」**先選 Multi-Leader——衝突解決的複雜度是寫應用 code 的隱形稅
Sloppy Quorum 的隱藏代價
選 Leaderless 時、Cassandra / Riak 預設啟用 sloppy quorum(節點掛時把寫轉到替補節點)→ 寫集與讀集可能不交集 → 即使 W+R>N 也讀不到剛寫的最新值。需要嚴格一致就要關掉 sloppy quorum(換可用性下降)、或改回 Single-Leader。
章末練習
思考題
- 在 PostgreSQL 設定 streaming replication(主從複製),觀察
pg_stat_replication中的延遲。 - 模擬「read-your-writes」問題:寫入後立刻從 follower 讀,能否重現「看不到自己的寫入」?
- 用 Python 實作一個 N=3, W=2, R=2 的簡易 leaderless KV store,包含 read repair。
Quiz 題目分級
- ★ 核心題(basic / applied):走 FirstReadShortcut「最小可用版」路徑也應答得出來
- ☆ 進階題(interview):通常需要讀過該章「第一次可跳」的小節、面試常考;第一次答不出來沒關係、之後回頭再挑戰
章末測驗 · ch05
Q1. 應用 ★ 「半同步複製」的設計動機是?
Q2. 應用 ★ Quorum 公式 W + R > N 保證的是?
Q3. 基礎 ★ 「Read-your-writes consistency」要解決的問題是?
Q4. ◆ 面試 ☆ Multi-Leader 架構中,下列哪個是「最後寫入勝出(LWW)」衝突解決的主要風險?
Q5. 基礎 ★ 你用 PostgreSQL async streaming replication、把讀流量導到 follower。最容易在使用者體驗上踩到的問題是?
Q6. ◆ 面試 ☆ Multi-Leader 系統用 LWW 解衝突、跟 Ch9 的 Linearizability(線性一致)兼容嗎?
面試怎麼問3 題 · 點開練習
想像面試官問你這幾題、自己心裡演練 90 秒講清楚。不必寫得長、能把關鍵字串起來就行。textarea 自動存。
- Q1. Read replica 你的應用看到「使用者寫完一筆資料、刷新後立刻 GET 看不到」。請說明可能的原因、以及至少兩種修法(含取捨)。
- Q2. 一致性取捨 請說明同步 / 半同步 / 非同步複製的取捨。如果是金融轉帳系統,你會選哪個?依據是什麼?PG / MySQL 預設行為各是什麼?
- Q3. Quorum Sloppy quorum 為什麼不保證 W+R>N 線性一致?請用「寫集 vs 讀集」的角度具體舉例。
我的筆記
儲存於:localStorage · 換瀏覽器不會同步
學習循環
延伸閱讀
- Jepsen analyses — 對各 DB(Cassandra / MongoDB / etcd / Redis 等)實際做了複製一致性測試
- PostgreSQL Streaming Replication — 官方文件,動手必看
- Dynamo: Amazon's Highly Available Key-value Store — Leaderless / Quorum 的原始論文
The Next Chapter
CH 06
Ch6 分區
預估 40 分鐘
Continue Reading→