《Designing Data-Intensive Applications(DDIA)》Podcast 準備稿:複製
書名: 設計資料密集型應用 Designing Data-Intensive Applications(The Big Ideas Behind Reliable, Scalable, and Maintainable Systems) 作者: Martin Kleppmann 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|DDIA.單主、多主、無主——複製的三條路 (5/12) 涵蓋範圍: 第6章 複製(單主/多主/無主複製、同步 vs 非同步、複製延遲的一致性問題、quorum、happens-before 與 version vectors)
背景速覽
歡迎進入 Part II——分散式資料。前四集都在單機世界,從這集起資料要散到多台機器上。第一個主題是複製(replication):把同一份資料存到多台機器。目的有三:降延遲(資料靠近使用者)、提可用性(部分節點掛了還能跑)、擴展讀取吞吐量。聽起來簡單——複製一次就好?難就難在資料會變,而所有的麻煩都來自「怎麼處理變更」。這集講三種複製策略,以及複製延遲帶來的一致性陷阱。
一句話重點
複製的全部難題不在「複製資料」,而在「處理變更」——單主簡單但有單點寫入瓶頸與 failover 風險、多主能跨資料中心寫但要面對寫入衝突、無主放棄 leader 換來高可用但一致性保證最弱;而即使你只用最簡單的單主非同步複製,也躲不掉複製延遲帶來的三個經典坑:讀不到自己剛寫的、時間倒流、因果錯亂。
值得討論的重點
1. 單主複製:最常見、最好懂,但 failover 是地雷區
leader-based(主從)複製:一個 leader 收所有寫入,把變更以 replication log 傳給 followers;讀取可從任何副本。Postgres、MySQL、MongoDB、Kafka 都這麼幹。
關鍵取捨在同步 vs 非同步:同步複製保證 follower 有最新資料(leader 掛了不丟資料),但同步 follower 一卡住整個寫入就阻塞。實務上多用 semi-synchronous(一個同步、其餘非同步)。而完全非同步最常見——leader 不等任何人,但 leader 掛了且救不回時,沒複製出去的寫入永久遺失(即使客戶端已收到「成功」)。
failover 才是真正的雷區:判定 leader 死掉(timeout 多長?)、選新 leader、重新配置——每一步都可能出事:非同步下新 leader 缺資料、split brain(兩個節點都自認 leader)、GitHub 真實事故(MySQL 自增主鍵與 Redis 快取不一致導致私人資料外洩)。所以很多團隊寧願手動 failover。
2. 複製延遲的三個經典坑(這段最實用)
非同步複製讓「讀取擴展」很爽,但 follower 資料可能過時——這叫最終一致性。三個必踩的坑:
- 讀不到自己的寫入(read-after-write):你剛改完個人檔案,立刻重整卻看不到——因為讀到了還沒同步的 follower。解法:自己可能改過的資料從 leader 讀、或記住寫入時間戳確保讀到的副本夠新。
- 時間倒流(monotonic reads):連續讀兩次,先讀到新副本、再讀到舊副本,畫面「倒退」。解法:讓同一使用者固定讀同一個副本。
- 因果錯亂(consistent prefix reads):你看到「回答」出現在「問題」之前——因為問答寫在不同分區、延遲不同。解法:有因果關係的寫入放同一分區。
這三個坑超級實戰,是節目最該讓聽眾帶走的東西。
3. 多主複製:能多處寫入,但「寫入衝突」是它的原罪
multi-leader:多個節點都能收寫入,彼此互相複製。適用場景:多資料中心(每個 DC 一個 leader,本地寫入、背景跨 DC 同步)、離線客戶端(手機行事曆)、協作編輯(Google Docs)。
代價是寫入衝突——兩人同時改同一個 wiki 標題,各自在不同 leader 成功,同步時才撞上。解法光譜:避免衝突(同一筆永遠路由同一 leader)、收斂到一致(LWW 取最新時間戳——常見但會丟資料、或自動合併、或保留所有版本讓應用處理)。進階技術:CRDTs(能自動合理合併的資料結構)、operational transformation(Google Docs 用的)。
4. 無主複製:放棄 leader,靠 quorum 撐住一致性
leaderless(Dynamo 風格,Cassandra/Riak):沒有 leader、沒有 failover,客戶端直接寫多個副本、平行讀多個副本,靠版本號判斷誰新。靠兩個機制收斂:read repair(讀時發現過時就寫回)、anti-entropy(背景對帳)。
核心公式:w + r > n——n 個副本,寫要 w 個確認、讀要查 r 個,只要 w+r>n 就保證讀到至少一個含最新值的節點。常見 n=3, w=2, r=2。但 Kleppmann 誠實地列出一堆邊界情況(sloppy quorum、並行寫、寫一半失敗……)即使滿足公式仍可能讀到舊值——所以 Dynamo 風格本質是為最終一致性最佳化的。
5. happens-before 與 version vectors:怎麼判斷「並行」
並行寫入衝突的本質問題:怎麼知道兩個操作是「並行」還是有先後?關鍵不是物理時間(分散式時鐘不可靠),而是它們是否知道對方存在:
- A 發生在 B「之前」(B 知道 A、依賴 A)→ A happens before B。
- 兩者都不知道對方 → 並行(concurrent)。
單一副本可用版本號追蹤因果(購物車範例);多副本則需要 version vector(每個副本、每個 key 都維護版本號)——這才是多副本環境下正確追蹤因果的資料結構。注意 version vector 和 vector clock 不完全是一回事。
注意事項
⚠️ Last Write Wins(LWW)是「方便的資料殺手」。 它用時間戳取最新者為贏家,簡單,但並行寫入時會默默丟掉除了贏家以外的所有寫入——即使它們都回報成功。Cassandra 是少數推薦用 LWW 的資料庫,但前提是 key 只寫一次然後當不可變(建議用 UUID)。提醒聽眾:看到 LWW 要警覺「我能接受丟資料嗎」。 下一集講分散式時鐘時,你會發現 LWW 還會因時鐘偏移而錯得更離譜。
⚠️ 「最終一致性」的「最終」是個刻意模糊的詞。 正常情況延遲可能不到一秒,但系統接近容量上限或網路出問題時,延遲可能飆到數秒甚至數分鐘。別讓聽眾以為「最終一致=幾乎即時」——在最糟的時刻,那個「最終」可能長到使用者已經客訴了。
⚠️ w + r > n 不是「一致性的保證書」。 這個公式給人安全感,但 Kleppmann 花了一整段列邊界情況打臉。它保證的是「正常情況下大概率讀到新值」,不是線性一致性。真正要強一致,得等第9集講共識。這條要講清楚,否則聽眾會誤把 quorum 當成萬靈丹。
專家補充
💡 複製延遲的三個坑,是「最終一致性」最具體、最好懂的入口。 「最終一致性」這詞很抽象,但「你看不到自己剛發的貼文」「畫面時間倒退」「留言出現在問題之前」——這些都是聽眾親身遇過的。用這三個生活化的坑來解釋一致性問題,比任何形式化定義都有效。這也是為什麼本書的教學如此經典。
💡 複製日誌的實作方式,藏著第11集的伏筆。 Kleppmann 列了四種:statement-based(坑多)、WAL shipping(與儲存引擎緊耦合、難跨版本升級)、logical/row-based log(與引擎解耦、外部系統好解析)、trigger-based(最靈活但最重)。其中 logical log 正是 CDC(變更資料捕獲) 的基礎——MySQL 的 binlog 就是這個。把「複製日誌」和「事件流」連起來,是 DDIA 後半最漂亮的一條線。
💡 協作編輯本質上是複製問題,這個視角很有啟發。 Google Docs 不是什麼魔法,它就是 multi-leader——每個使用者的編輯先套用到本地副本,再非同步複製給別人,衝突用 operational transformation 解。把「即時協作」還原成「複製+衝突解決」,會讓聽眾對日常工具有完全不同的理解。
討論問題
🎙️ 開場鉤子:「你發了一則貼文,立刻重整頁面——結果它不見了。三秒後又出現。你沒瘋,這是分散式系統最經典的坑之一,叫『讀不到自己的寫入』。今天我們把這類坑一個一個拆開。」
🎙️ 自問自答:「為什麼很多團隊寧願半夜爬起來『手動』切換資料庫主節點,也不敢開自動 failover?」用 split brain 和 GitHub 事故回答:自動切換判斷錯誤的代價,可能是兩個節點都自認 leader、資料寫花,甚至私人資料外洩——有時候,讓人類按下那個按鈕反而更安全。
🎙️ 帶走的一題:「想想你的系統有沒有『讀從庫』的設計——如果有,使用者改完資料立刻重整,會不會看到舊的?如果會,你就需要 read-after-write 一致性,而不是假裝沒事。」
更大範圍關聯
- 這是「分散式系統三部曲」的第一部:複製(這集)→ 麻煩(第8集,為什麼分散式這麼難)→ 一致性與共識(第9集,怎麼在麻煩中提供保證)。這三集是 Part II 的核心,也是 DDIA 含金量最高的段落。複製先讓你看到「多副本的具體問題」,後兩集再上升到理論高度。
- 與面試的分工:系統設計面試常要你「加個讀從庫提升讀取吞吐」,但很少有人追問「那 read-after-write 怎麼辦」。DDIA 這集給你的就是這層深度——你能主動指出「加了從庫會有複製延遲,使用者讀不到自己的寫入,要這樣處理」,這就是資深和初級的差距。(見 [軟體工程領域切分藍圖] §5「軟體架構演進」)
- CRDT 的當代意義:本地優先(local-first)軟體運動近年很熱(Kleppmann 本人就是推手),CRDT 是它的技術核心。這集埋的 CRDT 種子,是「離線可用、自動合併」這類現代應用的關鍵——一個 2017 年的章節,預言了之後的趨勢。
錄製建議
- 建議時長:約 24–26 分鐘。配比:三種複製策略約 10 分(單主含 failover 4 分、多主含衝突 3 分、無主含 quorum 3 分);複製延遲三個坑約 8 分(最實用,慢慢講);happens-before/version vectors 約 5 分。
- 討論策略:三種策略用「誰收寫入」這條主軸串起來(一個收 / 多個收 / 大家都收),聽眾就不會混。複製延遲三個坑是本集的情感高峰——全用生活化例子(看不到自己的貼文、畫面倒退、留言錯位),讓聽眾「啊我遇過!」。LWW 的危險和 quorum 的局限各埋一句伏筆,接到第8、第9集。
- version vector 偏理論,點到為止即可:講清楚「並行的判準是『知不知道對方存在』、不是物理時間」這個核心洞見就夠,細節留給願意翻書的人。