連鎖故障不是「某個東西壞了」,而是因正向回饋而隨時間擴大的故障:一個 replica 過載失敗,剩下的 replica 負載增加、更可能失敗,一路骨牌下去。局部問題被系統自己的迴路放大成全站崩潰——重點不是單一元件,而是那條會自我餵食的迴圈。
🧠 Intuition
IMPORTANT
最典型的引信是伺服器過載:cluster B 掛掉後它的流量全湧向 cluster A,A 從 1,000 QPS 突然要扛 1,200 QPS → 資源耗盡 → 崩潰 → load balancer 再把流量送去別處 → 全服務級過載。放大迴圈一旦轉起來,就會自己把自己推深。
- 資源耗盡有多種面貌,而且互相餵食:CPU 滿 → 請求變慢 → in-flight 數與佇列變長 → thread starvation → health check 失敗 → watchdog 殺 task;記憶體則有 Java 的 GC 死亡螺旋(CPU 少 → 請求慢 → 記憶體升 → GC 更頻繁 → CPU 更少)。在現場幾乎不可能追完整條因果鏈。
- crash-loop 帶有遲滯性:新啟動的 task 一上線就被打爆。10,000 QPS 健康、11,000 QPS 開始崩,但降回 9,000 QPS 也不一定回得來——因為只剩少數健康 task 在硬撐。進去容易,退出得比進去時更低的負載。
- retry 是主要放大器:後端上限 10,000 QPS、送進 10,100,失敗的 100 去 retry → 變 10,200 → 更多失敗 → 雪球;多層級 retry 還有乘法效應(4³ = 64 次)。因此系統級要有 retry budget,並用回應碼區分可不可 retry。
- 熔斷、降載只是「療法」:它們是防連鎖故障的手段之一,但這頁談的是「疾病本身」——迴圈怎麼起、怎麼測、怎麼急救。
- 很多觸發不是流量本身:Query of Death 或 assertion 失敗造成行程死亡、太多 task 同時更新、新版本改變請求 profile、有機成長吃掉餘裕、over-commit 的閒置 CPU 被別人的 MapReduce 拿走——都可能點火。
⚖️ Tradeoffs
硬撐所有請求 vs 讓部分讓步
- 系統過載時必須有東西讓步。超過斷裂點後,硬撐每一個請求只會讓放大迴圈把整台拖垮。
- 更好的選擇是讓部分使用者看到錯誤或較差品質:進入降級模式、丟非重要流量、關掉 batch 作業(索引更新、資料複製、統計蒐集),把資源留給撐得住的份量。
- 代價:降級路徑平常不走就等於平常壞了沒人知道——要定期演練,並監控「進入降級的伺服器數量」。
穩態最佳化 vs 連鎖故障韌性
- 許多「改善穩態」的改動——retry、shift load、kill unhealthy server、加一層 cache——反而會放大連鎖故障的風險。
- 例如「容量快取」(空 cache 就扛不住預期負載)是危險的隱性依賴:一次 task restart 或叢集 turn-up 帶來冷快取,就可能點火。
- 準則:評估每個變更時留意別「用一場故障換另一場故障」;穩態更漂亮,不代表過載時更安全。
🔑 Takeaways
- 連鎖故障的本質是正向回饋放大:過載傳播、資源耗盡互相餵食、crash-loop 的遲滯性讓它「降回原本安全的負載也回不來」。
- 要主動測:測到壞掉、再測壞掉之後——同時測「漸進增加」與「瞬間衝擊」,並確認高負載解除後能不能自動回到正常,知道斷裂點才知道容量餘裕。
- 發生時的急救手段:增加資源、停掉 health-check 殺 task、重啟伺服器、把流量壓到 1% 再慢慢加、進入降級模式、關掉 batch、擋掉 query of death;這正是該套用事件管理流程的時候。
No notes yet — jot your takeaways or Q&A here.