一致性與 Quorum
資料存 N 份時,寫入要幾份確認、讀取要問幾份?只要 W + R > N,讀到的就一定包含最新的寫入;網路斷開時,則要在一致和可用之間選一個。
3
2
2
1
讀到舊資料
0.0%
讀取延遲 p50
13.1 ms
寫入成功
99.8%
讀取成功
99.7%
N = 3、1 台故障時的所有 W、R 組合:讀到舊資料的比例/操作成功的比例。有框線的格子 W + R > N。
| W \ R | 1 | 2 | 3 |
|---|---|---|---|
| 1 | 9.6% / 100% | 0.0% / 100% | 0.0% / 1% |
| 2 | 1.0% / 100% | 0.0% / 100% | 0.0% / 1% |
| 3 | 0.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 | 寫入 p50 | 1 台故障:寫入成功 | 1 台故障:讀取成功 | |
|---|---|---|---|---|---|
| W1 R1(最快) | 14.7% | 3.2 ms | 3.3 ms | 100.0% | 100.0% |
| W2 R1 | 6.8% | 3.2 ms | 7.9 ms | 99.8% | 100.0% |
| W2 R2(多數決) | 0.0% | 7.7 ms | 7.9 ms | 99.8% | 99.7% |
| W3 R1(寫全部) | 0.0% | 3.2 ms | 16.0 ms | 0.2% | 100.0% |
| W1 R3(讀全部) | 0.0% | 16.4 ms | 3.3 ms | 100.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 台故障。