資料複寫 (Replication)
Replication 是「把同一份資料的副本放多台機器」,目的有三:低延遲、高可用、讀取擴展。
真正的挑戰不是複製,是當資料持續更新時,副本要如何保持一致。
三大架構一覽
| 架構 | 寫入端 | 優點 | 缺點 | 適用 |
|---|---|---|---|---|
| Single-Leader | 只有 primary 接寫 | 簡單、無寫入衝突 | Leader 是 SPOF | 絕大多數讀取密集應用 |
| Multi-Leader | 多 leader 都能寫 | 跨 DC 寫入快、容忍 DC 失效 | 寫入衝突難解 | 多資料中心、離線編輯 |
| Leaderless | 任何 replica 都能寫 | 高可用寫入 | 一致性弱、需應用層合併 | 高可用 + 最終一致可接受 |
1. Single-Leader Replication
基本運作
- Leader(primary):所有寫入必須先打到 leader
- Follower(replica/secondary):從 leader 拉取 replication log,照順序套用
- 讀取可以打 leader 或 follower;寫入只能打 leader
採用者:PostgreSQL(9.0+)、MySQL、MongoDB、Kafka、RabbitMQ
同步 vs 非同步(核心取捨)
| 模式 | 寫入路徑 | 一致性 | 可用性 |
|---|---|---|---|
| 同步 | Leader 等 follower 確認才回 client | ✅ Follower 保證有最新 | ❌ Follower 沒回應就卡住 |
| 非同步 | Leader 發完就回 client | ❌ Leader 掛了可能丟資料 | ✅ 快、不卡住 |
| 半同步(Semi-sync) | 一台同步、其他非同步 | 折衷:至少 2 份保有最新 | 折衷 |
「同步 vs 非同步沒有誰更好,看你的系統對一致性 vs 可用性的要求」—— 這種講法比硬說某個更好有深度。
Failover(Leader 掛掉)
1. 偵測失效 ← 通常用 timeout(如 30 秒沒心跳)
2. 選出新 leader ← 投票 / controller 指定,最好選資料最新的
3. 重新設定 ← Client 寫入導向新 leader;舊 leader 回來時變 follower
Failover 的地雷:
- ❌ 非同步資料遺失:新 leader 可能還沒收到舊 leader 最後幾筆寫入 → 丟資料
- ❌ Split Brain(腦裂):兩個節點都以為自己是 leader,兩邊都接受寫入 → 資料損毀
- 解法:STONITH(Shoot The Other Node In The Head)、fencing token
- ❌ Timeout 難拿捏:太長 → 復原慢;太短 → 系統忙時誤觸發 failover
Replication Log 的四種實作
| 方式 | 做法 | 缺點 |
|---|---|---|
| Statement-based | 直接傳 SQL 語句 | NOW() / RAND() 等非確定函數會不一致 |
| WAL Shipping | 直接傳預寫日誌 | Storage engine 緊耦合,版本升級可能要停機(PostgreSQL/Oracle) |
| Logical(row-based) | Row 層次變更 + 自有格式 | 解耦 storage engine,外部系統可解析 → CDC 基礎(MySQL binlog) |
| Trigger-based | 用 trigger / stored procedure | Overhead 高,但客製靈活、可跨資料庫 |
Replication Lag 三大不一致問題
非同步 replication 下,從 follower 讀可能看到過時資料(最終一致性 / eventual consistency)。
問題 1:Read-After-Write(讀自己剛寫的)不一致
用戶發貼文 → 馬上看 → 看不到(讀到還沒同步的 follower)
解法:
- 讀自己改過的東西時從 leader 讀:「自己的 profile 永遠從 leader 讀」
- 追蹤最後寫入位置(LSN / binlog position):client 帶上 LSN,replica 必須追上才能服務
- 短暫讀 primary 視窗:寫入後 N 秒內全部讀 primary
Primary: [LSN 1230][1231][1232][1233][1234]
Replica A: [LSN 1230][1231][1232] ← 還沒追上
Replica B: [LSN 1230][1231][1232][1233][1234] ← 已追上
Client 帶 LSN 1234 → 路由到 Replica B 或 Primary
牆上時鐘有 clock skew,log position 沒有這問題。
問題 2:Monotonic Reads(單調讀取)不一致
用戶讀到朋友的留言 → 刷新被路由到 lag 更大的 follower → 留言消失(時光倒流)
解法:同一用戶總是讀同一台 replica(如用 hash(user_id) 路由),那台掛了才換。
問題 3:Consistent Prefix Reads(因果一致性)不一致
A 問問題、B 回答 → 觀察者因不同 replica lag 先看到 B 回答才看到 A 問題(因果顛倒)
解法:把有因果關係的寫入放同一 partition;或用 version vector 記錄因果依賴。
2. Multi-Leader Replication
複雜度遠超過好處。跨多個 DC 的場景才有意義。
適用場景
| 場景 | 為什麼 |
|---|---|
| 多 DC 部署 | 每 DC 自己有 leader,跨 DC 非同步複寫,DC 失效可獨立運作 |
| 離線編輯 | 手機行事曆 App 無網路時繼續編輯,連網再同步 |
| 協作編輯 | Google Docs:每用戶的本地副本是一個 leader |
Multi-Leader 最大挑戰:寫入衝突
用戶 A 和 B 同時在不同 leader 改同一筆,都成功 → 非同步複寫時衝突。
衝突迴避(最簡單)
確保特定記錄的所有寫入走同一個 leader(如同一用戶永遠路由到同一 DC)。DC 掛了就失效。
收斂到一致狀態(Converging)
| 方法 | 怎麼做 | 代價 |
|---|---|---|
| Last Write Wins(LWW) | 比 timestamp,最大者勝 | 會丟資料(Cassandra 唯一支援) |
| Replica 優先序 | 編號大的 replica 優先 | 一樣丟資料 |
| 合併值 | 按字母順序串接 | 業務語義不見得對 |
| 記錄衝突、稍後解決 | 保留所有版本 | 需要應用程式或用戶決定 |
自訂衝突解法
- on write:偵測時立刻呼叫衝突處理函數(background process)
- on read:保留所有衝突版本,下次讀取時全回傳給 application 決定
自動衝突解法(研究方向)
- CRDT(Conflict-free Replicated Datatypes):集合、計數器、有序列表等可自動合併
- Mergeable Persistent Data Structures:類似 Git 的三方合併
- Operational Transformation:Google Docs / Etherpad 用的演算法
Replication Topology
| Topology | 形狀 | 缺點 |
|---|---|---|
| Circular | 環狀(MySQL 預設) | 任一節點掛 → replication 中斷 |
| Star | 一個中心節點轉發 | 中心節點是 SPOF |
| All-to-all | 全互連 | 容錯好,但寫入到達不同 replica 順序可能不一致(需 version vector) |
3. Leaderless Replication
源自 Amazon Dynamo,任何 replica 都能直接接受寫入。代表系統:Riak、Cassandra、Voldemort。
基本運作
n=3 個 replica,其中一台暫時不可用:
Client 同時寫 3 台
→ 2 台成功就算寫入成功(w=2)
→ 忽略沒回應那台
問題:那台 replica 復原後資料過時。
解法(讀取時補救):
| 機制 | 做什麼 |
|---|---|
| Read Repair(讀取修復) | 並行讀多台,發現舊版本就把新值寫回去(頻繁讀的 key 很有效) |
| Anti-entropy Process(反熵) | 背景持續掃描差異補資料(沒順序保證、可能有延遲) |
Quorum 讀寫
設總副本 n、寫入確認 w、讀取查 r:
w + r > n → 讀寫節點必然有重疊,讀取至少有一台有最新資料
| 配置 | 容忍失效節點 |
|---|---|
| n=3, w=2, r=2 | 1 個 |
| n=5, w=3, r=3 | 2 個 |
| n=3, w=3, r=1 | 0 個(讀超快、寫卡住) |
| n=3, w=1, r=3 | 0 個(寫超快、讀貴) |
Quorum 的限制(仍可能讀到舊值)
- Sloppy Quorum:寫到「非家節點」時,w/r 可能完全沒重疊
- 並發寫入:兩寫同時,LWW 的 clock skew 可能丟資料
- 節點失效 + 舊資料復原:持有新值的節點掛、從舊副本復原,新值副本數可能 < w
Dynamo-style 主要針對能接受最終一致性的場景優化。需要 read-after-write / monotonic reads / consistent prefix 要靠 transaction 或 consensus。
Sloppy Quorum & Hinted Handoff
網路中斷讓 client 連不到「家節點」但連得到其他節點:
| 選項 | 行為 |
|---|---|
| 嚴格 quorum | 返回錯誤(CP) |
| Sloppy quorum | 接受寫入,暫存到「現在連得到」的節點(不在原本 n 台裡)→ AP |
Hinted Handoff:網路恢復後把這些臨時保管的寫入送回家節點。
「就像沒帶鑰匙借住鄰居,拿到鑰匙後再搬回自己家。」
偵測並發寫入:Version Vector
每個 replica 為每個 key 維護自己的 version number 集合 → version vector(並非 vector clock,但概念相近)。
Server 為每個 key 維護 version;每次寫入遞增
Client 讀取 → server 回傳所有未被覆蓋的值 + 最新 version
Client 寫入 → 帶上前次讀的 version + 合併前次讀到的所有值
Server 收到帶 version 的寫入 → 覆蓋 ≤ 該版本的值,保留更高版本(並發)
Happens-before 關係:如果 B 知道 A 存在或建立在 A 之上 → A 先於 B。若兩操作互不知情 → 並發(concurrent)。
系統設計面試怎麼談 Replication
主動帶進議題
| 場景 | 主動說 |
|---|---|
| 讀取密集 | 「加 read replica 分散讀取,需要討論 replication lag」 |
| 高可用 | 「Primary 是 SPOF,設計 automatic failover」 |
| 地理分散 | 「每 DC 一個 leader,multi-leader 跨 DC 非同步複寫」 |
| 高可用 + 寫入擴展 | 「Cassandra quorum 寫入(w=2, n=3)」 |
常見面試題型一句話對應
| 題型 | 預設架構 |
|---|---|
| 社群媒體(Twitter/IG) | Single-leader + read replica,自己的內容從 primary 讀 |
| 金融(支付/庫存) | Single-leader + 半同步,寧停機不要不一致 |
| 全球 CDN / cache | Multi-leader + LWW(資料遺失可接受)+ TTL |
| IoT / metrics | Leaderless(Cassandra),高寫入、LWW 可接受 |
| 協作 App(Docs 類) | Multi-leader + CRDT / OT |
高頻 Deep-Dive 問題
「Read-After-Write 怎麼保證?」
- 自己的資料從 primary 讀
- 追蹤 LSN / binlog position(不要用牆上時鐘 —— clock skew)
- 短暫讀 primary 視窗
「Failover 怎麼避免資料遺失?」
- 半同步 replication(至少一台 follower 同步)
- 選資料最新的 follower 為新 primary
- 強一致要求高 → Raft / Paxos consensus(PostgreSQL 的 Patroni、etcd)
「Multi-leader 衝突怎麼處理?」
- 先問衝突有多嚴重(金融不能丟、社群 profile LWW 可接受)
- 衝突迴避(路由到同一 leader)
- 保留所有版本 + 應用層合併(購物車類)
- CRDT(計數器、集合、有序列表)
「Quorum + 網路分區怎麼辦?」
- CP:嚴格 quorum、連不到足夠節點就返回錯誤 → 金融系統
- AP:sloppy quorum + hinted handoff → 用戶行為記錄、IoT
「我會用 leader-follower。寫入走 primary,3 台 replica 分散讀取。非同步 replication 有幾秒 lag,多數讀取可接受;但用戶剛發的貼文要馬上看到,我在 session 裡追蹤最後寫入的 LSN,那段時間強制從 primary 讀。」
「為了高可用設計 automatic failover:每個 follower 監控 replication lag,primary 超過 30 秒沒心跳就把 lag 最小的 follower 提升為新 primary。注意 split brain 風險,用 fencing token 確保舊 primary 完全下線才讓新的接管。」
Related Notes
- 02-Distributed-Systems/02-CAP-Theorem — Quorum 配置就是 CP vs AP 的具體選擇
- 02-Distributed-Systems/03-Scalability — Read replica 是水平擴展讀取的手段
- 02-Distributed-Systems/05-Numbers-to-Know — Lag 通常幾 ms,極端情況可到分鐘
- 05-Database-Advanced/01-Transactions — 跨 replica 的強一致需要 consensus
- 05-Database-Advanced/02-Sharding — Sharding + replication 是常見組合
- 07-Caching-Storage/01-Caching — Cache 是 replication 之外的讀取擴展手段