跳到主要內容

系統設計

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

主題 · CAP 定理

CAP 定理

C、A、P 是一致性(Consistency)、可用性(Availability)、分區容錯(Partition tolerance)。網路分區一定會發生,發生的時候只能選一個:拒絕請求以保住一致性,或繼續服務但兩邊的資料可能分歧。常聽到的「三取二」,其實是這個取捨的簡化說法。

做法
C一致性(Consistency)
每次讀取都拿到最新一次成功寫入的值,好像整個系統只有一份資料。
A可用性(Availability)
每個送到正常節點的請求都會得到回應,而不是錯誤或一直等。
P分區容錯(Partition tolerance)
節點之間的網路斷掉、訊息遺失時,系統仍然繼續運作。
A 區(60% 的用戶)B 區A1A2B1✕
兩區之間的網路斷了A 區B 區0.08 s · 讀取 · 正確 · 00.19 s · 讀取 · 正確 · 00.23 s · 讀取 · 正確 · 00.26 s · 讀取 · 正確 · 00.42 s · 累加 · 正確 · 10.47 s · 讀取 · 正確 · 10.48 s · 讀取 · 正確 · 10.54 s · 讀取 · 正確 · 10.60 s · 累加 · 正確 · 20.72 s · 累加 · 正確 · 30.76 s · 累加 · 正確 · 40.81 s · 累加 · 正確 · 50.91 s · 累加 · 正確 · 61.05 s · 累加 · 正確 · 71.06 s · 累加 · 正確 · 81.12 s · 累加 · 正確 · 91.13 s · 讀取 · 正確 · 91.17 s · 累加 · 正確 · 101.18 s · 累加 · 正確 · 111.20 s · 讀取 · 正確 · 111.23 s · 讀取 · 正確 · 111.29 s · 累加 · 正確 · 121.35 s · 讀取 · 正確 · 121.47 s · 讀取 · 正確 · 121.47 s · 讀取 · 正確 · 121.49 s · 讀取 · 正確 · 121.50 s · 累加 · 正確 · 131.50 s · 讀取 · 正確 · 131.53 s · 讀取 · 正確 · 131.57 s · 讀取 · 正確 · 131.63 s · 累加 · 正確 · 141.67 s · 讀取 · 正確 · 141.69 s · 讀取 · 正確 · 141.71 s · 讀取 · 正確 · 141.74 s · 累加 · 正確 · 151.78 s · 累加 · 正確 · 161.80 s · 累加 · 正確 · 171.80 s · 累加 · 正確 · 181.92 s · 累加 · 正確 · 192.09 s · 讀取 · 正確 · 192.25 s · 累加 · 正確 · 202.33 s · 讀取 · 正確 · 202.47 s · 累加 · 正確 · 212.62 s · 讀取 · 正確 · 212.71 s · 讀取 · 正確 · 212.81 s · 讀取 · 正確 · 212.90 s · 讀取 · 正確 · 212.91 s · 累加 · 正確 · 222.94 s · 讀取 · 正確 · 223.02 s · 累加 · 正確 · 233.08 s · 讀取 · 被拒絕3.15 s · 讀取 · 正確 · 233.19 s · 累加 · 正確 · 243.23 s · 累加 · 被拒絕3.23 s · 讀取 · 被拒絕3.42 s · 累加 · 正確 · 253.49 s · 讀取 · 正確 · 253.59 s · 累加 · 被拒絕3.61 s · 讀取 · 正確 · 253.65 s · 讀取 · 正確 · 253.65 s · 累加 · 正確 · 263.69 s · 讀取 · 正確 · 263.70 s · 累加 · 正確 · 273.74 s · 讀取 · 正確 · 273.77 s · 累加 · 正確 · 283.79 s · 累加 · 正確 · 293.95 s · 讀取 · 正確 · 294.14 s · 讀取 · 正確 · 294.15 s · 累加 · 正確 · 304.21 s · 讀取 · 正確 · 304.26 s · 累加 · 正確 · 314.28 s · 讀取 · 正確 · 314.30 s · 累加 · 正確 · 324.41 s · 累加 · 被拒絕4.54 s · 讀取 · 正確 · 324.65 s · 讀取 · 被拒絕4.82 s · 累加 · 正確 · 334.88 s · 累加 · 正確 · 344.89 s · 讀取 · 正確 · 344.94 s · 讀取 · 正確 · 345.03 s · 累加 · 正確 · 355.06 s · 累加 · 正確 · 365.14 s · 讀取 · 被拒絕5.26 s · 累加 · 正確 · 375.30 s · 讀取 · 正確 · 375.36 s · 累加 · 正確 · 385.40 s · 累加 · 被拒絕5.45 s · 讀取 · 正確 · 385.52 s · 累加 · 正確 · 395.54 s · 累加 · 正確 · 405.58 s · 累加 · 被拒絕5.66 s · 讀取 · 正確 · 405.74 s · 讀取 · 正確 · 405.80 s · 累加 · 被拒絕5.82 s · 累加 · 正確 · 415.88 s · 讀取 · 正確 · 416.02 s · 讀取 · 被拒絕6.17 s · 讀取 · 正確 · 416.17 s · 讀取 · 正確 · 416.31 s · 累加 · 正確 · 426.40 s · 讀取 · 正確 · 426.56 s · 讀取 · 正確 · 426.61 s · 累加 · 正確 · 436.82 s · 讀取 · 被拒絕6.85 s · 讀取 · 被拒絕6.85 s · 累加 · 正確 · 446.86 s · 累加 · 被拒絕6.96 s · 累加 · 被拒絕7.11 s · 讀取 · 正確 · 447.17 s · 累加 · 正確 · 457.23 s · 累加 · 正確 · 467.25 s · 讀取 · 正確 · 467.39 s · 讀取 · 正確 · 467.41 s · 讀取 · 正確 · 467.46 s · 讀取 · 正確 · 467.46 s · 讀取 · 正確 · 467.52 s · 累加 · 正確 · 477.59 s · 累加 · 正確 · 487.60 s · 累加 · 正確 · 497.63 s · 累加 · 正確 · 507.67 s · 累加 · 正確 · 517.73 s · 累加 · 正確 · 527.76 s · 累加 · 正確 · 538.06 s · 累加 · 正確 · 548.18 s · 累加 · 正確 · 558.21 s · 讀取 · 正確 · 558.25 s · 讀取 · 正確 · 558.33 s · 累加 · 正確 · 568.60 s · 累加 · 正確 · 578.70 s · 讀取 · 正確 · 578.71 s · 累加 · 正確 · 588.73 s · 累加 · 正確 · 598.75 s · 讀取 · 正確 · 598.80 s · 讀取 · 正確 · 598.94 s · 累加 · 正確 · 608.96 s · 讀取 · 正確 · 608.99 s · 讀取 · 正確 · 609.04 s · 累加 · 正確 · 619.23 s · 累加 · 正確 · 629.43 s · 讀取 · 正確 · 629.52 s · 讀取 · 正確 · 629.53 s · 累加 · 正確 · 639.54 s · 讀取 · 正確 · 639.68 s · 讀取 · 正確 · 639.72 s · 累加 · 正確 · 649.73 s · 累加 · 正確 · 659.81 s · 累加 · 正確 · 669.85 s · 讀取 · 正確 · 669.88 s · 累加 · 正確 · 679.96 s · 讀取 · 正確 · 670s2s4s6s8s10s
正確讀到錯的值被拒絕上排實心:累加;下排空心:讀取
有回應的請求
90%
讀到錯的值
0 / 75
遺失的累加
0 / 67
平均延遲 A / B 區
2 / 162 ms

網路斷掉的 4 秒裡,B 區只剩 3 個副本中的 1 個、湊不到多數,15 個請求被拒絕(整體 90% 有回應)。A 區有 2 個、照常服務。沒有任何一次讀到錯的值:一致性是靠犧牲少數那一側的可用性換來的。

延遲模型:跨區單程 80 ms、區內 1 ms。B 區的 CP 請求計一次跨區往返加兩次區內傳遞(162 ms);平均值不含被拒絕的請求。CP 假設已有領導者及共用的已提交日誌;只確認連得到多數副本,不足以讓獨立副本具有線性一致性。未模擬選主、日誌複製細節、排隊或儲存延遲。

亮起來的是這一步執行的程式碼
// CP sketch: one shared committed value, assuming an established leader/log.
// This shows the availability gate only; it is not a consensus protocol.
// Independent instances do not replicate or become linearizable by this check.
class MajorityReplica {
private value = 0;
constructor(private replicas: number) {}
increment(reachable: number): number {
if (reachable * 2 <= this.replicas) throw new Error("unavailable");
this.value += 1;
return this.value;
}
read(reachable: number): number {
if (reachable * 2 <= this.replicas) throw new Error("unavailable");
return this.value;
}
}
// AP, last writer wins: one value and its timestamp per replica.
type Versioned = { value: number; ts: number; site: string };
class LwwReplica {
state: Versioned = { value: 0, ts: -1, site: "" };
constructor(private site: string) {}
increment(now: number): Versioned {
this.state = { value: this.state.value + 1, ts: now, site: this.site };
return this.state;
}
merge(remote: Versioned): void {
const s = this.state;
if (remote.ts > s.ts || (remote.ts === s.ts && remote.site > s.site)) {
this.state = remote;
}
}
}
// AP, CRDT: each replica counts only its own increments; merging keeps
// the largest count seen per replica, and the value is the sum.
class GCounter {
counts: Record<string, number> = {};
constructor(private site: string) {}
increment(): void {
this.counts[this.site] = (this.counts[this.site] ?? 0) + 1;
}
merge(remote: Record<string, number>): void {
for (const [site, n] of Object.entries(remote)) {
this.counts[site] = Math.max(this.counts[site] ?? 0, n);
}
}
value(): number {
return Object.values(this.counts).reduce((a, b) => a + b, 0);
}
}

模型假設與範圍

  • 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
  • 固定三副本、兩個區域與分割時段;一致性指模型裡的讀取語意,可用性是非故障節點是否回覆。不是把延遲或一般可靠性歸成 CAP 選項。

什麼時候用

  • 選 CP(Consistency+Partition tolerance,分區時保住一致性):絕不能讀到錯的值或重複執行的時候——庫存扣減、帳戶餘額、分散式鎖、選主。寧可在分區時拒絕少數那一側的請求。
  • 選 AP(Availability+Partition tolerance,分區時保住可用性):永遠要能回應、資料可以事後合併的時候——購物車、按讚數、動態、DNS。要先想好怎麼合併(CRDT,Conflict-free Replicated Data Type;或保留多個版本讓應用決定)。
  • 沒有分區的平常日子也要選:要低延遲還是強一致。這是 PACELC(Partition → Availability/Consistency,Else → Latency/Consistency)補上的那一半,而且是比較常發生的那一半。

和其他主題的關係

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

操作平均最差
CP:遠端區域的一次讀寫
要等多數派確認;分區時湊不到多數就直接失敗(✕)
1 RTT✕
AP:一次讀寫
在本地完成,不等其他區域
O(1)O(1)
最後寫入勝出的合併O(1)O(1)
CRDT 計數器的合併
R 是副本數:每個副本一格
O(R)O(R)

空間:O(R),CRDT 計數器每個副本記一個數字;最後寫入勝出只要一個值和時間戳

和其他做法比

網路有回應讀到錯的值遺失的累加B 區平均延遲
CP:多數決正常100%0 / 750 / 75162 ms
CP:多數決(分區)斷 4 秒90%0 / 750 / 67162 ms
AP:最後寫入勝出正常100%70 / 7515 / 751 ms
AP:最後寫入勝出(分區)斷 4 秒100%70 / 7530 / 751 ms
AP:CRDT 計數器正常100%20 / 750 / 751 ms
AP:CRDT 計數器(分區)斷 4 秒100%42 / 750 / 751 ms

同一串 150 個請求(60% 來自 A 區、一半讀一半累加),兩區之間單程 80 ms、區內 1 ms、時鐘一致。「讀到錯的值」是讀到的數字不等於當下所有已確認累加的次數;「遺失的累加」是回報成功、但所有副本最後都看不到的累加。

真實世界裡的它

  • CP:ZooKeeper、etcd、Consul 用多數決(ZAB〔ZooKeeper Atomic Broadcast〕、Raft);Spanner、CockroachDB 預設強一致。
  • AP:Cassandra、DynamoDB(一致性可調)、Riak(內建 CRDT 型別)、DNS。
  • Amazon 的 Dynamo 論文讓購物車永遠能加東西,衝突時保留兩個版本、合併時取聯集。

取捨與陷阱

  • 「三取二」是誤導:網路分區不是可以不選的選項,所謂的 CA(同時要 Consistency 與 Availability)只存在於沒有網路的單機。真正的選擇只有一個:分區發生時要 C 還是 A。
  • 最後寫入勝出會悄悄丟資料:回報成功的寫入事後消失,連錯誤訊息都沒有(上面的計數器)。時鐘不同步會讓它更糟。
  • CAP 的 C 是「線性一致性」(每次讀都看到最新寫入),和 ACID 的 C(資料符合約束)是兩回事。
  • 很多系統的一致性是可調的(每次請求選 quorum 大小),不是整個系統只能貼一個 CP 或 AP 的標籤。