複製:單主、多主、無主,與延遲的三個怪象

· tech

#distributed-systems#book-notes#replication

📑 目錄

進入 Part II,資料開始跨機器。第一招是複製(replication):同一份資料放多台,為的是三件事——一台掛了還能服務(高可用)、資料放得離使用者近(低延遲)、讀流量攤出去(擴讀)。如果資料不會變,複製就是複製貼上而已;所有的難,都難在「資料會變」——變更要怎麼傳到每一份副本? 我在 Redis 主從那篇拆過其中一種(單主、非同步)的實戰;這篇拉到 DDIA 的高度:全世界的複製方案,其實只有三種拓撲,而它們的差別,是「把寫入衝突放在哪裡處理」。

三種拓撲:衝突不會消失,只會搬家

單主 single-leader Leader follower follower 所有寫入 衝突:源頭消滅(單一寫入點) 代價:failover 難(誰接任?) MySQL / Postgres / Redis / Kafka 多主 multi-leader Leader A Leader B 資料中心 1資料中心 2 兩邊同時改同一筆 → 衝突! 得利:各地就近寫、斷網照收 代價:寫入衝突要事後解 跨資料中心 / 離線編輯 / 協作文件 無主 leaderless 副本 副本 副本 client 同時寫 n 份 quorum:w + r > n 得利:沒有 leader,無 failover 代價:讀寫路徑複雜、read repair Dynamo / Cassandra 同一道題的三種答案:寫入衝突,你想在「源頭擋、事後解、還是讀時調和」?
單主:所有寫入走唯一的 leader——衝突在源頭就被消滅(只有一個寫入點),代價是 leader 掛了的 failover 很難(誰發現、誰接任、怎麼不裂腦)。多主:多個資料中心各自有 leader 就近收寫、互相同步——斷網也能寫,但兩邊同時改同一筆的衝突,得事後解決(誰贏?怎麼合併?)。無主:沒有 leader,客戶端直接同時寫多個副本、讀也多讀幾份,靠 w + r > n 的重疊保證讀到新值——不用 failover,但讀寫路徑變複雜(讀到舊值要 read repair 補寫回去)。衝突不會消失,只會搬家——三種拓撲,是選它搬去哪

無主那個 quorum 值得多一句:n 份副本,寫入要 w 份確認、讀取要問 r 份,只要 w + r > n(例如 n=3、w=2、r=2),讀跟寫的集合必有交集,你一定會碰到至少一份最新值(再從中挑新的)。數學很漂亮——但 DDIA 誠實地列了它的邊角:並行寫入的先後難定、寫入部分失敗不回滾、sloppy quorum 時保證會鬆動。它是機率上很強的工程保證,不是絕對的數學證明,這個分寸後面講共識時會再回來。

複製延遲的三個怪象:有名字,才能對症下藥

只要複製是非同步的(絕大多數都是,原因見 Redis 那篇的取捨),replica 就永遠慢半拍,於是會冒出各種「見鬼了」的讀取結果。DDIA 最有價值的貢獻,是給這些怪象起了名字——三個病,三種藥:

① 讀不到自己的寫 read-your-writes 我留言 → 寫進 leader ✓ 重新整理 → 留言不見了?! (讀到還沒追上的 replica) 藥:自己的資料,讀 leader ② 時光倒流 monotonic reads(單調讀) 第一次讀:看到留言(新 replica) 再讀一次:留言消失(舊 replica) (兩次打到進度不同的 replica) 藥:同一使用者固定讀同一台 ③ 因果錯亂 consistent prefix(一致前綴) 旁觀者先看到:「答:沒有」 才看到:「問:吃飯了嗎?」 (問與答走不同分區,複製快慢不同) 藥:有因果的寫入,進同一分區 怪象是物理,消滅不了;但每個病都有便宜的對症藥——不必為此上完整的強一致
三個怪象、三帖藥:① read-your-writes——自己剛留言、重新整理卻不見了(讀到落後的 replica);藥是「自己的資料讀 leader,別人的隨便」。② 單調讀——兩次讀打到進度不同的 replica,東西出現又消失、像時光倒流;藥是「同一個使用者固定讀同一台」(例如按 user id 挑 replica)。③ 一致前綴——問句與答句走不同分區、複製快慢不同,旁觀者先看到答案再看到問題;藥是「有因果關係的寫入放同一分區」。注意每帖藥都只治那個病、成本很低——這正是分級一致性的精神:按需下藥,而不是為了怪象直接上最貴的強一致

這三帖藥合起來,就是所謂的因果一致性的民間版本:不追求「全世界同步」,只保證「跟你有關的、有因果的部分看起來是對的」。多數產品要的其實就是這個——而它比強一致便宜太多了。

反思

三種拓撲,是「把衝突放在哪」的三種選擇

讀完這章,我把三種拓撲收斂成一句話:寫入衝突不會消失,只會搬家——你只是在選它搬去哪。 單主把衝突消滅在源頭(單一寫入點),於是把難處搬到了 failover(誰發現、誰接任、怎麼不裂腦);多主讓各地都能寫,把難處搬到了事後的衝突解決(LWW 會默默丟資料、合併邏輯是應用的痛);無主不搞 leader,把難處搬進了每一次讀寫的路徑(quorum、read repair)。這個「難處守恆」的視角,跟我在 infra 系列講「沒有真正無狀態的系統,只有把狀態推去別處的系統」是同一種思維。所以選複製方案時,我的問題從「哪個好」變成了「這三種苦,我的團隊最吞得下哪一種?」——多數團隊的答案是單主,因為 failover 的苦有 Sentinel、K8s、managed 服務幫你扛,而衝突解決的苦只能自己吞。

給怪象起名字,是這章最被低估的貢獻

read-your-writes、monotonic reads、consistent prefix——第一次讀會覺得是學術詞彙,但實戰過就知道:這些名字是「把玄學變工單」的把手。 使用者回報「我留言不見了、重新整理又出現」,沒讀過這章的人會當成靈異事件重啟服務;讀過的人立刻說「這是單調讀破了,把同一個 user 釘到同一台 replica」——病有名字,就有藥方,還能估價。這跟 K8s 排障那張「狀態→病因」對照表是同一種力量:工程能力的很大一部分,是腦中有一本「症狀 → 病名 → 藥方」的字典。DDIA 這章就是複製延遲的那幾頁字典,背下來,值回整本書價。

一致性是菜單,不是開關

這章教我最實用的心態是:一致性不是「開或關」,是一份分級菜單——全要(強一致)最貴,全不要(最終一致)最便宜但怪象叢生,而中間有一排「單點的藥」:自己的資料讀 leader、同一人固定一台、因果放同一分區。多數產品要的不是「全世界即時一致」,是「跟我有關的部分看起來對」——那用兩三帖便宜的藥就夠了,不必為此把整套系統升級成同步複製或分散式共識。這也是我做架構評審時常踩的煞車:有人一遇到延遲怪象就喊「上強一致」,我會先問——你的使用者實際碰到的是三個病裡的哪一個? 對症下那帖藥,成本常常只有十分之一。真正需要強保證的場景留給後面共識那章;在那之前,先記住這句:買一致性跟買保險一樣,買你需要的那幾項,不是全險。