跳到主要內容

系統設計

把資料結構放大到好幾台機器

主題 · 一致性與 Quorum

一致性與 Quorum

資料存 N 份時,寫入要幾份確認、讀取要問幾份?只要 W + R > N,讀到的就一定包含最新的寫入;網路斷開時,則要在一致和可用之間選一個。

3
2
2
1
AB✕W 2 + R 2> N 3 ✓
讀到舊資料
0.0%
讀取延遲 p50
13.1 ms
寫入成功
99.8%
讀取成功
99.7%
N = 3、1 台故障時的所有 W、R 組合:讀到舊資料的比例/操作成功的比例。有框線的格子 W + R > N。
W \ R123
19.6% / 100%0.0% / 100%0.0% / 1%
21.0% / 100%0.0% / 100%0.0% / 1%
30.0% / 0%0.0% / 0%0.0% / 0%

W + R = 4 > N = 3:任何 2 個回應裡,一定有一個是上一次成功寫入的 2 台之一,所以永遠讀得到最新的確認值——模擬裡讀到舊資料的比例是 0.0%。 1 台不在線上時,寫入成功 99.8%、讀取成功 99.7%。這就是 CAP(Consistency、Availability、Partition tolerance):連不到的節點一多,要嘛堅持 W、R 而拒絕服務(選一致),要嘛降低 W、R 繼續服務但可能讀到舊值(選可用)。

假設:對同一個鍵每秒 200 次操作、共 10 秒,其中 30% 是寫入;每個節點在 1 ms 加上平均 10 ms 的指數分佈延遲後回應;每 1 秒重新挑哪幾台故障。故障的節點保留資料,但不寫也不回應——和被網路分割隔開時一模一樣。

亮起來的是這一步執行的程式碼
class Replica {
version = 0;
value: string | null = null;
up = true;
store(version: number, value: string): boolean {
if (!this.up) return false; // unreachable: no ack
if (version > this.version) {
this.version = version;
this.value = value;
}
return true;
}
}
class QuorumClient {
constructor(private replicas: Replica[], private w: number, private r: number) {}
// Sends to every replica; succeeds once w of them have stored it.
write(version: number, value: string): boolean {
let acks = 0;
for (const replica of this.replicas) {
if (replica.store(version, value)) acks++;
}
return acks >= this.w;
}
// replies: the replicas in the order their answers arrive.
read(replies: Replica[]): { version: number; value: string | null } | null {
const answered = replies.filter((x) => x.up).slice(0, this.r);
if (answered.length < this.r) return null;
let newest = answered[0];
for (const x of answered) {
if (x.version > newest.version) newest = x;
}
for (const x of answered) {
if (x.version < newest.version) x.store(newest.version, newest.value!);
}
return { version: newest.version, value: newest.value };
}
}

模型假設與範圍

  • 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
  • 固定 N 個副本,選擇 W 個寫入與 R 個讀取。W+R>N 表示集合交集;單靠交集無法保證線性一致性,尚需版本與併發協定。

什麼時候用

  • 沒有主節點的資料庫:任何一台都能接寫入,沒有「主資料庫掛了要選新的」這段空窗。
  • 想依每種操作調整:重要的寫入用 W = 多數,不怕舊的讀取用 R = 1 換速度。
  • 跨機房、跨地區部署,要在網路分割時決定要可用還是要一致。

和其他主題的關係

由這些組成
資料庫複製
延伸閱讀
一致性雜湊

出現在這些架構裡

時間與空間複雜度(Big O)

操作平均最差
寫入
送給全部 N 台;時間是第 W 快的那台
O(N)O(N)
讀取
問全部 N 台、等前 R 個;時間是第 R 快的那台
O(N)O(N)
讀取修復O(R)O(R)

空間:O(N·D),D 是資料量;每份資料存 N 份

和其他做法比

讀到舊資料讀取 p50寫入 p501 台故障:寫入成功1 台故障:讀取成功
W1 R1(最快)14.7%3.2 ms3.3 ms100.0%100.0%
W2 R16.8%3.2 ms7.9 ms99.8%100.0%
W2 R2(多數決)0.0%7.7 ms7.9 ms99.8%99.7%
W3 R1(寫全部)0.0%3.2 ms16.0 ms0.2%100.0%
W1 R3(讀全部)0.0%16.4 ms3.3 ms100.0%0.5%

N = 3。前三欄是全部在線時,後兩欄是任一時刻有 1 台故障時。多數決(W2 R2)是最常見的選擇:不會讀到舊資料、掛 1 台也照常服務,代價是讀寫都要等第二快的節點。寫全部或讀全部,只要有 1 台故障,那一邊就幾乎全部失敗。p50 是第 50 百分位(percentile):成功的讀取與成功的寫入分開統計,各自把延遲排好後取 50% 位置的值,不包含失敗操作的等待時間;要搭配成功率一起看,低延遲不代表高可用性。

真實世界裡的它

  • Amazon Dynamo 論文、Cassandra 的 ONE/QUORUM/ALL 一致性等級、Riak。
  • Raft、Paxos 等共識演算法也用多數決:一筆紀錄要多數節點寫下才算數。
  • etcd、ZooKeeper 通常跑 3 或 5 台,正是為了容忍 1 或 2 台故障。

取捨與陷阱

  • W + R > N 不等於強一致:同時有兩筆寫入時要靠版本號或時間戳決定誰贏;時鐘不準時,「最後寫入者勝」會悄悄丟掉資料。
  • 失敗的寫入不會自動回滾:只寫到少於 W 台時,那幾台已經存了新值,之後的讀取可能看到一個「失敗了」的寫入。
  • N 取偶數浪費:4 台的多數是 3,和 3 台一樣只能容忍 1 台故障。