🎯 什麼情境該想到我

當一個下游服務變慢/掛掉,就把整條鏈路甚至整個系統拖垮(雪崩)時。

⚙️ 怎麼用(組合使用)

  1. 熔斷(Circuit Breaker):偵測到下游持續失敗就「跳閘」,快速失敗、暫停呼叫,給它時間恢復,避免堆積。
  2. 艙壁隔離(Bulkhead):把資源(執行緒池/連線)按依賴分艙,一個依賴爆掉不會吃光全部資源。
  3. 重試 + 退避:對暫時性失敗重試,但要指數退避 + 上限 + 冪等,避免重試風暴(見 工具-MQ消費端防禦三原則)。
  4. 降級(Fallback):失敗時回傳合理的預設/快取結果,保住核心體驗。
  5. 流量控制(限流):超過容量就擋,保護系統不被打垮。

★ 為什麼需要它:連鎖故障的機制

下游變慢(不是掛,只是慢)
  → 你的請求堆積,執行緒/連線被佔住
  → ★ 你自己的資源耗盡 → 你也開始變慢
  → 上游的請求在你這裡堆積 → 上游也垮
  → ★ 整條鏈一起倒

★ 最危險的不是「下游掛了」,是「下游變慢」。
掛了會快速失敗(連線被拒),資源立刻釋放;慢會持續佔住資源直到耗盡。
所以逾時是第一道防線,比斷路器更基本。

★ 五種手段的正確順序(由外而內)

限流 Rate Limit        ← ★ 最外層:擋掉超過容量的流量,保護自己
  艙壁 Bulkhead        ← 隔離資源,一個依賴爆掉不影響其他
    斷路器 Circuit Breaker  ← 下游持續失敗就快速失敗
      逾時 Timeout      ← ★ 必須在重試外層,否則重試會超出總時限
        重試 Retry      ← 只對暫時性錯誤
          實際呼叫

★ 順序錯了會失效:Retry 包在 Timeout 外面 → 總時間變成 n × timeout,SLO 直接破
(見 裝飾者模式 的 middleware 順序)。

★ 斷路器的三個狀態與參數

      失敗率超過閾值
CLOSED ──────────────▶ OPEN ──等待 cooldown──▶ HALF-OPEN
  ▲                     │                        │
  │   ★ 試探成功         │  ★ 直接快速失敗         │ 放少量請求試探
  └─────────────────────┴────────────────────────┘
                              試探失敗 → 回 OPEN

★ 它本質上是一個狀態機(見 狀態模式)。

參數怎麼定
失敗率閾值常見 50%。★ 要用「比例」不是「絕對次數」,否則低流量時永遠觸發不了
最小請求數★ 必要:10 次請求裡錯 5 次不該跳閘(樣本太小)
cooldown幾秒到幾十秒。太短 → 反覆試探打死下游;太長 → 恢復太慢
half-open 的試探量1~數個。不要一次全放行(會瞬間打垮剛恢復的下游)

★ 「什麼算失敗」要想清楚:逾時、5xx 算;4xx(參數錯)不算(那是你的問題,重試也沒用)。

★ 重試的三條鐵律

鐵律說明
① 只重試暫時性錯誤429、503、逾時、連線失敗 可以;400、401、404、422 絕對不要
② ★ 必須冪等非冪等的操作重試 = 重複下單、重複扣款。★ 帶冪等鍵(見 工具-MQ消費端防禦三原則)
③ ★ 指數退避 + jitter沒有 jitter,所有客戶端會同步醒來再打一次,製造週期性尖峰
// ★ full jitter:業界標準做法
delay := time.Duration(rand.Int63n(int64(min(base<<attempt, maxDelay))))

★★ 重試放大是連鎖故障最常見的加速器:

三層服務,每層重試 3 次 → 最底層收到 3³ = 27 倍的流量
★ 下游已經在掙扎了,你送 27 倍過去 = 徹底打死

對策:

  • ★ 只在最靠近失敗點的那一層重試,其他層直接往上回報
  • 重試預算(retry budget):全域限制「重試佔總請求的比例」(如 10%)
  • 尊重 Retry-After header

★ 降級要事先設計,不能臨時想

降級層次例子
回傳快取的舊資料★ 最常用。要標示「資料更新於 X」
回傳預設值 / 空結果推薦系統掛了 → 回熱門商品
關閉非核心功能購物車能結帳,但「你可能也喜歡」消失
★ 排隊 / 非同步化同步做不到 → 收下請求、回 202、之後處理
拒絕★ 明確的錯誤 + Retry-After,遠勝於超時等待

★ 降級開關要能手動觸發(feature flag),不要只靠自動偵測——
事故當下你需要能立刻關掉某個功能,而不是等斷路器自己判斷。

★ 艙壁隔離

❌ 共用一個執行緒池/連線池
   → 呼叫 A 服務的請求全部卡住 → 池子耗盡 → ★ 呼叫 B 服務的也沒資源了

✅ 每個下游依賴各自一個池(或 semaphore 限制併發數)
   → A 爆掉只影響 A 那一艙

★ Go 裡最簡單的實作就是每個依賴一個 semaphore(golang.org/x/sync/semaphore 或帶緩衝的 channel)。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • ★★ 逾時是第一優先,不是斷路器。 沒有逾時,其他手段全部失效——因為資源永遠不會被釋放。
    而且逾時要端到端傳遞(context),不是每層各設一個(見 工具-select超時與競速)。
  • ★ 重試放大是連鎖故障的加速器(見上)。只在一層重試。
  • ★ 斷路器在「低流量」時不可靠。一小時只有 20 次請求時,失敗率的統計意義很弱。
    → 設最小請求數,或對低流量服務不用斷路器。
  • ★ 降級的品質要能被察覺。默默回傳空結果 → 使用者以為「真的沒有資料」。
    要有 metric 追蹤降級觸發率,而且降級中的回應要能被識別(header 或欄位標示)。
  • 這些機制本身會製造 bug。斷路器誤跳、重試造成重複、降級邏輯有 bug——
    ★ 每一個容錯機制都要被測試(故障注入,見 工具-注入故障與韌性演練)。
  • 不要對所有依賴都套用全套。核心依賴掛了就是該失敗;只有「可以沒有」的依賴才值得降級。
    → 先分清楚關鍵路徑與非關鍵路徑。
  • 用成熟的函式庫(sony/gobreaker、failsafe-go、Envoy/Istio 的 outlier detection),不要自己寫。

🔗 相關工具