複製模型#

複製 (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 定理 在複製層的具體展開。