第 8 / 12 部 待錄

分散式系統的麻煩

《Designing Data-Intensive Applications(DDIA)》Podcast 準備稿:分散式系統的麻煩

書名: 設計資料密集型應用 Designing Data-Intensive Applications(The Big Ideas Behind Reliable, Scalable, and Maintainable Systems) 作者: Martin Kleppmann 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|DDIA.網路、時鐘、暫停——分散式系統為什麼這麼難 (8/12) 涵蓋範圍: 第9章 分散式系統的麻煩(部分失敗、不可靠網路、不可靠時鐘、process pauses、fencing tokens、拜占庭故障、系統模型與 safety/liveness)

背景速覽

這一章 Kleppmann 自己說是「把悲觀主義推到極致」——全面檢視分散式系統中可能出錯的一切。單機程式要嘛正常、要嘛整台崩潰;但在分散式系統,部分失敗(partial failure) 才是常態:一部分壞了、其他部分還在跑,而且你無法確定某個操作到底成功了沒。這集是 Part II 的情緒低谷(壞消息全在這),也是理解第9集「怎麼在這麼爛的環境裡仍提供保證」的必要鋪墊。

一句話重點

分散式系統有三個不可靠的根源——網路會無界延遲(封包遺失/延遲,你分不清是對方掛了還是回應丟了)、時鐘會漂移(NTP 同步有限,物理時間戳不可信)、程序會任意暫停(GC、VM 暫停可達數分鐘,醒來還不知道自己睡過去了);而最深的領悟是:節點無法確知任何事,它只能根據不可靠網路收到(或沒收到)的訊息猜測,所以真相必須由多數決(quorum) 決定,而不是任何單一節點說了算。

值得討論的重點

1. 部分失敗:分散式系統的定義性特徵

單機是確定性的——相同操作相同結果,壞了就整台崩潰。分散式系統徹底不同:部分失敗是非確定性的——同樣操作有時成功有時失敗,你甚至無法確定某操作是否成功(因為訊息傳輸時間也不確定)。Kleppmann 對比兩種哲學:超級電腦(HPC) 把部分失敗升級為全面失敗(節點掛了就停整個叢集、從 checkpoint 重啟);雲端運算 必須隨時在線、不能停機修理。本書講的網路服務屬於後者——必須接受部分失敗、在軟體裡建容錯。

2. 不可靠的網路:你分不清三件事

shared-nothing 架構下機器只能靠網路通訊,而網路是非同步封包網路——不保證何時到、甚至是否到。你發請求沒收到回應時,無法區分:請求遺失了?對方掛了?對方處理完了但回應遺失了?唯一的工具是 timeout——但 timeout 設多長是無解的難題:長了故障偵測慢,短了容易誤判(把暫時變慢的節點宣告死亡),誤判又可能觸發級聯失敗(把工作轉給別人→別人也過載→全倒)。延遲的根源是排隊(交換器排隊、CPU 忙、VM 被暫停、TCP 流量控制)。

關鍵洞見:無界延遲不是自然定律,是成本與效益的取捨——電路交換(電話網路)能保證延遲上界但利用率低、貴;封包交換利用率高、便宜,代價就是無界延遲。我們選了便宜,就得接受不確定。

3. 不可靠的時鐘:物理時間戳是個陷阱

兩種時鐘要分清:日曆時鐘(wall-clock,會被 NTP 強制重設而向後跳、忽略閏秒,不適合量測經過時間)vs 單調時鐘(只向前走、適合量間隔,但絕對值無意義、跨機器不可比)。

NTP 同步的準確度受一堆因素拖累(石英漂移、網路延遲、防火牆擋 NTP、閏秒、VM 虛擬化)。最危險的陷阱:用日曆時間戳排序事件(多主複製的 LWW)——時鐘落後的節點寫的值會被誤判為「較舊」而丟棄,操作就這樣靜默消失了(接第5集的 LWW 警告)。解法是邏輯時鐘(遞增計數器,只追蹤相對順序,不量實際時間)。

時鐘讀數應該被當成信賴區間而非精確時間點。Google 的 TrueTime(Spanner 用)就明確回報 [earliest, latest],靠 GPS/原子鐘把不確定度壓到約 7ms,再用「刻意等待」確保因果順序。

4. Process Pauses:你的程式可能睡了一分鐘還不自知

租約(lease)的範例程式碼看似合理卻有致命缺陷:程式在「檢查租約有效」和「實際處理請求」之間,可能發生任意長的暫停——GC(Java 的 stop-the-world 可達數分鐘)、VM live migration、筆電合蓋、OS 上下文切換、同步磁碟 I/O、記憶體換頁、SIGSTOP 信號。暫停期間租約可能已過期、別的節點已接管,但這個程序醒來後完全不知道時間過去了,繼續用過期的身分幹活,造成資料損壞。結論:節點必須假設自己可能在任意時刻被暫停很久。

5. 真相由多數決定,與 fencing tokens

既然節點無法確知任何事、也不能信任自己對局勢的判斷,那「誰是 leader」「誰持有鎖」這類「只能有一個」的問題怎麼辦?答案是 quorum(法定人數)——靠節點間投票,最常見是絕對多數(系統裡只能有一個多數,不會出現兩個多數做矛盾決策)。即使一個節點自認還活著,只要多數宣告它死了,它就得認。

但光靠 quorum 還不夠——HBase 真實的資料損壞 bug:Client 1 取得租約後 GC 暫停、租約過期,Client 2 取得租約開始寫,Client 1 醒來仍以為持有鎖也去寫,撞車。解法是 fencing token:每次授予鎖都附一個遞增的 token,每個寫入請求都帶 token,儲存端檢查——發現處理過更高的 token(34)就拒絕較舊的(33)。關鍵:資源端要主動檢查、拒絕過時請求,不能只靠客戶端自律。

注意事項

⚠️ 不正確的時鐘最可怕的地方是「悄無聲息」。 CPU 壞、網路斷會有明顯症狀,但時鐘有缺陷或 NTP 設錯時,大部分功能看起來都正常,時鐘卻悄悄偏離現實,結果是靜默的資料遺失而非明顯的崩潰。所以必須主動監控所有機器之間的時鐘偏移。這條值得重點講——「無聲的錯誤」比「大聲的崩潰」難對付一萬倍。

⚠️ fencing 必須在「資源端」執行,不能信任客戶端。 一個常見的錯誤實作是「讓客戶端自己檢查鎖還有效嗎」——但暫停過的客戶端根本不知道自己過期了。正確做法是讓儲存服務檢查 token、拒絕舊的。Kleppmann 強調這也是良好的防禦性設計:服務不該假設客戶端總是乖乖的。 這個思想(端點負責正確性、不信任中間環節)會在第12集的「端到端論證」升級。

⚠️ 本書假設節點「不可靠但誠實」,這是有邊界的。 節點可能慢、可能過時,但若回應了就是據其所知遵守協議。若節點會「說謊」(傳偽造回應),那是拜占庭故障——航太(輻射翻位元)、區塊鏈(參與者想欺詐)才需要拜占庭容錯,一般資料中心(節點都自己人)不需要,成本太高。別讓聽眾以為「分散式系統都要搞拜占庭容錯」——那是過度工程。

專家補充

💡 這一章是「謙卑」的一課。 它系統性地摧毀了我們對「電腦很精確」的直覺——時間不可信、網路不可信、連自己的程序有沒有在跑都不可信。但這種悲觀不是虛無,而是設計可靠系統的起點。Kleppmann 引了一句很美的話:「從不可靠的元件建構可靠的系統」是計算領域的老智慧(TCP 在不可靠的 IP 上提供可靠傳輸)。系統可以比其組成元件更可靠——但這有極限,而認清極限正是這一章的價值。

💡 safety vs liveness 是評估任何分散式演算法的兩把尺。 Safety(安全性):壞事永不發生,違反了可以指出具體時刻、無法撤銷(如 fencing token 的唯一性)。Liveness(活性):好事最終會發生(如請求最終收到回應)。分散式演算法通常要求 safety 在所有情況下都成立(即使全部節點崩潰),而 liveness 允許附加條件(如只在多數存活時保證進展)。這組概念是第9集評判共識演算法的工具,先在這裡建立。

💡 系統模型是「把混亂現實形式化」的橋樑。 時序模型(同步/部分同步/非同步)、故障模型(崩潰-停止/崩潰-恢復/拜占庭)——真實系統最有用的組合是部分同步 + 崩潰-恢復。這套抽象很學術,但它的價值是:讓你能在設計階段就推理「我的假設在什麼情況下會破」。Kleppmann 提醒:證明演算法正確不代表實作永遠正確,但理論分析能揪出藏很久的問題——理論與實證測試同等重要。

討論問題

🎙️ 開場鉤子:「你的服務呼叫了另一個服務,等了五秒沒回應。問題來了——對方是掛了?還是只是慢?還是它其實做完了、只是回應在路上丟了?這三件事你分不出來。而分不出來,正是分散式系統一切痛苦的源頭。」

🎙️ 自問自答:「為什麼不能相信電腦的時鐘?它不是一直在報時嗎?」回答:因為 NTP 會把它往回撥、GC 會讓它看起來跳一分鐘、不同機器的時鐘還互相對不準——你用時間戳排序事件的那一刻,就可能讓某個操作無聲無息地消失。

🎙️ 帶走的一題:「如果你的系統有『分散式鎖』,問一句——當持有鎖的程序 GC 暫停了 30 秒、鎖過期被別人搶走、它卻醒來繼續寫資料時,誰來擋住它?如果答案是『沒有』,你需要的是 fencing token。」

更大範圍關聯

  • 分散式三部曲的低谷與轉折:複製(第5集)讓你看到具體問題,這集(麻煩)告訴你為什麼這些問題如此難解——根源是網路、時鐘、暫停三大不確定性。而第9集(一致性與共識)則是反擊:在這麼爛的環境裡,我們仍然能造出提供強保證的演算法。這集是最黑暗的一集,但黑暗是為了襯托下一集的曙光。
  • 與第5、第7集的呼應:LWW 的危險(第5集)在這裡找到了根本原因(時鐘不可信);fencing token 解的「過期持有者」問題,呼應第7集「兩個交易撞車」的並行控制——只是搬到了分散式場景。DDIA 的章節是層層咬合的,這集把前面埋的線都收緊了。
  • 與系統設計實務:面試或實戰中「加個分散式鎖」聽起來很簡單,但這集讓你看到背後的深淵——timeout 設多少、GC 暫停怎麼辦、要不要 fencing。能講出這層深度,就是真正理解分散式系統而非只會喊名詞。(見 [軟體工程領域切分藍圖] §5「軟體架構演進」)

錄製建議

  • 建議時長:約 24–26 分鐘。配比:部分失敗+雲端 vs 超算約 4 分;不可靠網路約 6 分;不可靠時鐘約 7 分(含 LWW 陷阱與 TrueTime);process pauses 約 4 分;真相由多數決定+fencing token 約 5 分。
  • 討論策略:這集的情緒基調是「壞消息連發」,但要講得引人入勝而非令人沮喪——用「你分不清對方掛了還是慢了」這種人人有感的困境開場,把三大不確定性(網路/時鐘/暫停)當成三個反派依序登場。fencing token 用 HBase 真實事故講,畫面感強。
  • 結尾一定要轉折、給希望:「這一集全是壞消息——網路不可信、時鐘不可信、連程序在不在跑都不可信。但下一集,我們要看人類如何在這片廢墟上,造出能達成共識、提供強一致性保證的演算法。從絕望到曙光,就在下一集。」這個情緒弧線會讓聽眾欲罷不能。