複製模型#
複製 (Replication) 是把同一份資料保存在多個節點上的技術,目的有三:在使用者附近放資料以降低延遲、在部分節點故障時仍能服務(可用性),以及水平擴展讀取吞吐量。難點不在「把資料抄過去」,而在於寫入發生後,多份副本之間如何協調。本章從複製拓樸出發,討論單主、多主、無主三種模型在資料丟失、可用性與一致性上的根本取捨。
本章談的是複製「模型」與其取捨;複製延遲導致的一致性異常與保證(讀己之寫、單調讀等)整理在 一致性模型,共識的理論基礎在 共識演算法。
三種複製拓樸#
依「誰可以接受寫入」來分類,所有複製系統都落在以下三種之一:
| 模型 | 寫入入口 | 衝突可能 | 典型代表 | 主要取捨 |
|---|---|---|---|---|
| 單主 (Single-leader) | 只有主節點 | 無 | PostgreSQL、MySQL | 寫入無衝突,但主節點是瓶頸與單點 |
| 多主 (Multi-leader) | 多個主節點皆可寫 | 有 | 跨資料中心、離線編輯 | 可用性高,但必須解決寫入衝突 |
| 無主 (Leaderless) | 任意節點,靠 quorum 收斂 | 有 | Dynamo、Cassandra | 容錯高,但一致性保證最弱 |
flowchart TB
subgraph SL["單主"]
L1[主] --> F1[從]
L1 --> F2[從]
end
subgraph ML["多主"]
M1[主A] <--> M2[主B]
M1 --> MF1[從]
M2 --> MF2[從]
end
subgraph LL["無主"]
C[客戶端] --> N1[節點1]
C --> N2[節點2]
C --> N3[節點3]
end
style L1 fill:#ffccbc
style M1 fill:#ffccbc
style M2 fill:#ffccbc單主複製:同步 vs 非同步#
單主複製只有一個節點接受寫入,再把變更(變更串流/WAL)傳給從節點。關鍵設計選擇是:主節點要等從節點確認後才回覆客戶端,還是不等?
同步 vs 非同步的資料丟失取捨#
| 模式 | 主節點行為 | 主節點故障時 | 代價 |
|---|---|---|---|
| 全同步 | 等所有從節點確認才回覆 | 不丟資料 | 任一從節點卡住,整個寫入阻塞 |
| 非同步 | 寫完主節點立即回覆 | 已回覆但未複製的寫丟失 | 延遲低、可用性高,但有資料丟失窗 |
| 半同步 | 至少一個從節點確認即回覆 | 通常不丟(視配置) | 折衷,最常見的生產配置 |
全同步複製在實務上幾乎不被採用:只要有一個從節點變慢或當機,整個系統就無法寫入。多數系統採用半同步——保證至少一份副本有最新資料,同時容忍個別從節點落後。
非同步複製的資料丟失窗:
T1: 客戶端寫入 → 主節點落地
T2: 主節點回覆「成功」 ← 客戶端認為已持久化
T3: 主節點當機(變更尚未送達從節點)
T4: 從節點被選為新主 ← T1 的寫入永久消失Failover 三難#
主節點故障時必須切換到新主,這個 Failover 流程牽涉三件互相矛盾的決策:
flowchart LR
A["1. 判定<br/>主節點真的死了嗎?"] --> B["2. 選主<br/>選哪個從節點接手?"]
B --> C["3. 切換<br/>如何讓客戶端與其餘節點認新主?"]
style A fill:#ffccbc
style B fill:#fff9c4
style C fill:#c8e6c9- 判定 (Detection):靠逾時 (timeout) 判斷。逾時太短會把變慢的主節點誤判為死亡,引發不必要的切換;太長則延長故障期間的不可用。沒有「正確」的逾時值——這是 共識演算法 中 FLP 困境的實際體現。
- 選主 (Election):通常選複製進度最新的從節點,以減少資料丟失。但非同步複製下,最新的從節點也可能落後於已回覆給客戶端的寫入。
- 切換 (Cutover):必須確保舊主真的退位,且所有節點與客戶端都指向新主,否則就會出現 split brain(腦裂)。
Failover 是分散式系統最危險的時刻。許多嚴重的線上事故不是「故障本身」造成,而是「對故障的自動處理」失控所致——例如下章談的重平衡期間級聯誤判。
Split Brain(腦裂)#
腦裂指同時存在兩個都認為自己是主節點的狀態,兩邊各自接受寫入,資料分岔且無法自動合併。
網路分區 → 舊主仍在 A 區接受寫入
→ B 區誤判舊主已死,選出新主也接受寫入
→ 兩份「真相」並存,需人工介入修復常見防護:
- Fencing token(柵欄令牌):每次選主遞增一個單調令牌,儲存層拒絕舊令牌的寫入,讓退位的舊主無法再造成傷害。
- 多數決 (Quorum):要求新主必須取得多數節點同意才生效,分區的少數側無法選出新主。詳見 共識演算法。
多主複製:寫入衝突解決#
多主允許多個節點同時接受寫入(例如每個資料中心一個主),以換取更低的寫入延遲與分區下的可寫性。代價是:同一筆資料可能在兩個主節點上被並行修改,產生衝突。
sequenceDiagram
participant A as 主A
participant B as 主B
Note over A,B: 同一筆 x,初始 = 1
A->>A: 寫 x = 2
B->>B: 寫 x = 3
A-->>B: 複製 x = 2
B-->>A: 複製 x = 3
Note over A,B: 衝突!兩邊收到對方的值,順序不同衝突解決策略#
| 策略 | 機制 | 風險/適用 |
|---|---|---|
| 避免衝突 | 同一筆資料固定路由到同一個主 | 最簡單可靠;主節點故障時路由要重新分配 |
| LWW (最後寫入勝出) | 用時間戳比大小,保留「較新」的 | 簡單但會默默丟資料,且依賴不可靠的時鐘 |
| CRDT | 設計成數學上可自動合併的資料結構 | 無需仲裁即收斂,但資料型別受限 |
| 應用層仲裁 | 保留所有衝突版本,交給應用/使用者 | 不丟資料,但增加應用複雜度(如購物車合併) |
LWW 的危險:它用時間戳決定勝負,但分散式時鐘並不可靠(見 分散式時間)。時鐘偏差會讓「較早」的寫入因為時間戳較大而勝出,於是較晚、客戶端以為成功的寫入被默默覆蓋——這是一種無聲的資料丟失。LWW 只適合資料丟失可接受的場景(如快取、可重建的指標)。
衝突解決的偽碼骨架:
function resolveConflict(versions):
if 策略 == LWW:
return versions.maxBy(v -> v.timestamp) // 危險:丟掉其餘版本
if 策略 == CRDT:
return versions.reduce(merge) // merge 保證可交換、可結合、幂等
if 策略 == 應用仲裁:
if versions 之間有 happen-before 關係:
return 較新者 // 用向量時鐘判斷因果,見第 07 章
else:
return application.merge(versions) // 真正並行 → 交給應用合併判斷「兩個寫入是因果先後還是真正並行」,不能靠時間戳,要靠向量時鐘 (Vector Clock)——詳見 分散式時間。只有真正並行的版本才需要仲裁。
無主複製:Quorum 與其邊界#
無主複製(如 Dynamo 風格)沒有主節點,客戶端(或協調節點)同時寫多個副本、同時讀多個副本,靠多數重疊來收斂。
設總副本數為 $n$,每次寫入要求 $w$ 個節點確認,每次讀取要求 $r$ 個節點回覆,則只要:
$$w + r > n$$
讀取集合與寫入集合必定有交集,於是讀取至少會碰到一個含有最新值的節點。常見配置是 $n = 3, w = 2, r = 2$。
n=3, w=2, r=2 → w + r = 4 > 3
寫入命中 {A, B},讀取命中 {B, C}
交集 = {B} → 讀取保證看到最新寫入$w + r > n$ 不保證強一致性#
這條不等式只保證「讀寫集合有交集」,不等於線性一致性(見 一致性模型)。以下邊界情況仍會讀到舊值:
- 並行寫入:兩個寫入並行落在不同節點,quorum 無法決定誰先誰後,仍需衝突解決(同多主)。
- 寫入部分失敗:寫只在 $w$ 中的一部分成功就回報失敗,但這些副本並不會回滾,後續讀取可能讀到「失敗」的寫入。
- 讀寫競態:寫入進行中讀取,不同副本回傳新舊混雜,無法保證讀到的是「已完成」的寫入。
- 節點恢復後回放舊值:當機節點恢復時若用舊資料覆蓋,可能讓副本數跌破 quorum 重疊保證。
Quorum 提供的是「最終一致 + 高機率讀到新值」,不是強一致。要靠它彌補延遲,還需要讀修復 (read repair)(讀到舊值時順手寫回新值)與反熵 (anti-entropy)(背景比對副本差異)兩個收斂機制。
模型選擇決策#
flowchart TD
Q1{需要強一致 /<br/>唯一性約束?}
Q1 -->|是| SL[單主<br/>所有寫入經主節點]
Q1 -->|否| Q2{多地點同時寫入 /<br/>離線可用?}
Q2 -->|是| ML[多主<br/>備妥衝突解決]
Q2 -->|否| Q3{追求極致容錯與<br/>寫入可用性?}
Q3 -->|是| LL[無主<br/>quorum + 讀修復]
Q3 -->|否| SL
style SL fill:#c8e6c9
style ML fill:#fff9c4
style LL fill:#ffccbc小結#
- 單主:寫入無衝突、推理簡單,但主節點是瓶頸與單點,核心難題是同步/非同步的資料丟失取捨與 Failover 三難。
- 多主/無主:用可用性換一致性,必然面對寫入衝突,而 LWW 會默默丟資料,因果判斷要靠向量時鐘。
- Quorum 的 $w + r > n$ 只保證讀寫交集,不等於強一致,邊界情況仍需衝突解決與背景收斂。
- 沒有最好的模型,只有最適合業務一致性需求的取捨——這正是 CAP 定理 在複製層的具體展開。