連鎖失效與過載:別讓一台倒下拖垮全部

· tech

#sre#reliability

📑 目錄

第一篇說可靠度的目標是「出錯也能運作」。但有一種故障特別難纏,因為它會自我放大:一個小問題引發骨牌,幾分鐘內把整個系統拖垮——這就是連鎖失效(cascading failure)。它最可怕的地方,是系統在過載時不是線性變慢,而是斷崖式崩潰

連鎖失效:小故障如何滾成大災難

最經典的劇本:一台機器過載倒下,它的流量被轉移到其他台,於是其他台也過載、跟著倒,一台壓垮一台,像骨牌:

一台倒下 → 流量轉移壓垮下一台 → 骨牌式全滅 Server A先過載 → 倒 ✗ Server B接手 A 流量 → 也倒 ✗ Server C全壓過來 → 倒 ✗ 而 retry storm(重試風暴)火上加油: 請求失敗 / 變慢 客戶端重試 總負載更高 正回饋:越失敗 → 越重試 → 越失敗(自我放大)
連鎖失效的兩個引擎:流量轉移讓故障像骨牌一台壓垮一台;retry storm 則是正回饋——失敗引發重試、重試加重負載、負載製造更多失敗,把小問題在幾分鐘內滾成全站崩潰

除了 retry storm,還有驚群(thundering herd):快取一失效或服務一重啟,大量請求同時湧向後端,瞬間把它打垮。它們的共通點,都是很多請求在同一時間、同一方向擠過去

過載時要「主動保護」,不要「硬吞」

面對過載,工程師的直覺常是「盡量都服務到」——但這正是災難的開始:硬吞的結果是排隊爆炸、資源耗盡,然後大家一起爛。正確的做法是主動保護:

❌ 硬吞(被動) 過載來襲 全部照收、無限排隊 資源耗盡 → 斷崖式全爛 想全服務,結果全滅 ✓ 主動保護 ① Load shedding 卸載丟掉一部分(回 503),保住其餘 ② Graceful degradation 降級回舊快取 / 關次要功能 → 堪用就好 ③ Backpressure對上游說「我滿了」→ 讓上游減速 部分成功 > 全部失敗 另可加 Circuit Breaker(斷路器):下游一直失敗就先別打、快速失敗,保護雙方
過載時的心法是「部分成功 > 全部失敗」:主動丟掉一部分(load shedding)、退回堪用的降級版(degradation)、或把「我滿了」往上游傳(backpressure)。硬要全服務,換來的是大家一起崩

讓連鎖失效不發生的幾招

把上面收斂成一份實用清單:

  • 重試要有節制:限次數、指數退避(exponential backoff)+ 抖動(jitter)(別讓大家同時重試)、retry budget(整體重試量設上限)。沒節制的重試,是連鎖失效最大的幫兇。
  • Circuit breaker(斷路器):偵測到某個下游一直失敗,就先不打了(快速失敗),給它喘息、也不讓自己被拖住,過一陣再試探性放行。
  • 限流 / 限並行(rate limiting):在入口就擋掉超過容量的量,別讓它進來排隊。
  • 容量規劃 + load shedding:平時留餘裕,過載時主動卸載——在崩潰前就丟,而不是崩了才丟。

反思

連鎖失效的可怕,在於「正回饋」

一般的故障是線性、局部的:一台壞了就少一台。但連鎖失效可怕在它有正回饋——失敗引發重試、重試加重負載、負載又製造更多失敗,自我放大成一個把整個系統吸進去的漩渦。所以我看系統時,會特別警惕任何「失敗會讓情況更糟」的迴圈:沒有 backoff 的重試、沒有防護的快取失效、沒有斷路器的下游呼叫——這些都是埋著的正回饋炸彈,平時看不出來,一旦點燃就是幾分鐘全站崩。找出並剪斷這些放大迴圈,比事後救火重要得多。

過載時,「部分成功」遠勝「全部失敗」

Load shedding 這招第一次聽很反直覺:系統已經在掙扎了,你還主動丟掉請求?但想通就懂了——硬要全服務,結果是全滅;主動丟掉 10%,保住 90%。 10% 的人拿到一個乾脆的 503,遠好過 100% 的人一起 timeout。這個「有損但可控 > 無損但失控」的取捨,跟 error budget、跟 SLO 的精神完全一致:承認你不能全都要,然後選一個守得住的點。 成熟的系統不是「永遠不拒絕」,是「懂得在對的時候、有尊嚴地拒絕」。

Backpressure:把「我滿了」誠實地往上游說

我最欣賞的一招是 backpressure。最健康的系統,是會誠實表達自己極限的系統——滿了就往上游傳訊號,讓上游減速,而不是假裝還吞得下、然後一起崩。這其實是一種謙遜。我在 Kafka 那條線看過同一件事的優雅版:消費者跟不上,就讓它按自己的節奏去拉,而不是被上游推爆——pull 模型天生就帶 backpressure。把「我做不到、請慢一點」誠實地往上游講,是分散式系統、其實也是團隊協作裡,最被低估的一種美德:逞強硬吞,往往才是拖垮全體的那個人。