Circuit Breaker(熔斷模式)
是什麼?
Circuit Breaker 是一種容錯模式,用來防止一個服務的故障級聯擴散到整個系統。它監控對某個服務的呼叫,當失敗率超過閾值時自動「斷開電路」,在一段時間內直接回傳 fallback 結果,避免持續呼叫已故障的服務。
ℹ️三種狀態
Closed(正常)→ 失敗率超過閾值 → Open(熔斷中)→ timeout 到期 → Half-Open(試探)→ 成功 → 回到 Closed;失敗 → 回到 Open。
核心觀念
三種狀態詳解
| 狀態 | 行為 | 轉換條件 | |------|------|----------| | Closed | 正常放行所有請求,監控失敗率 | 失敗率超過閾值 → Open | | Open | 直接回傳 fallback,不發送請求 | timeout 到期 → Half-Open | | Half-Open | 放行少量探測請求 | 成功 → Closed;失敗 → Open |
關鍵參數
| 參數 | 說明 | 典型值 | |------|------|--------| | Failure Threshold | 觸發熔斷的失敗率 | 50% | | Sampling Duration | 統計失敗率的時間窗口 | 30 秒 | | Minimum Throughput | 最低請求數才開始統計 | 10 次 | | Break Duration | Open 狀態持續時間 | 30-60 秒 | | Success Threshold | Half-Open 需要連續成功幾次才回到 Closed | 3 次 |
搭配的容錯策略
- Retry:短暫故障時重試(搭配 exponential backoff)
- Timeout:設定請求超時,避免無限等待
- Fallback:熔斷時提供備用回應(快取資料、預設值、降級服務)
- Bulkhead:限制同時對某個服務的並發請求數
順序組合:Timeout → Retry → Circuit Breaker → Fallback
常見誤區
⚠️常犯錯誤
- Circuit Breaker 沒有搭配 Timeout(請求卡住永遠不失敗,熔斷永遠不觸發)
- Retry 放在 Circuit Breaker 外面(重試的失敗也被計入熔斷統計)
- 熔斷後沒有 Fallback(直接回傳 500 給使用者)
- 所有服務共用一個 Circuit Breaker(應該每個目標服務各自獨立)
執行流程
正常運作(Closed)
請求正常轉發,同時監控失敗率
偵測到故障
失敗率超過 50%,觸發熔斷
熔斷啟動(Open)
直接回傳 fallback,不發送請求到目標服務
試探恢復(Half-Open)
timeout 到期,放行少量請求測試服務是否恢復
恢復或繼續熔斷
探測成功回到 Closed,失敗則繼續 Open
流程解讀:Circuit Breaker 就像一個智慧保險絲。正常時它透明地轉發請求並收集統計數據。當失敗率超過閾值時,它快速「斷開」,保護系統不被故障服務拖垮。Open 狀態的 timeout 到期後,它小心翼翼地放行少量請求測試水溫。如果故障已修復就恢復正常,否則繼續保護。
程式碼範例
C# 版本
// Polly — .NET 的 Resilience 函式庫
using Polly;
using Polly.CircuitBreaker;
// 定義 Circuit Breaker Policy
var circuitBreaker = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.CircuitBreakerAsync(
failureThreshold: 0.5, // 50% 失敗率
samplingDuration: TimeSpan.FromSeconds(30),
minimumThroughput: 10, // 至少 10 次請求才統計
breakDuration: TimeSpan.FromSeconds(30),
onBreak: (result, duration) =>
Console.WriteLine($"Circuit opened for {duration.TotalSeconds}s"),
onReset: () => Console.WriteLine("Circuit closed"),
onHalfOpen: () => Console.WriteLine("Circuit half-open")
);
// 搭配 Retry + Timeout + Fallback
var resilientPolicy = Policy.WrapAsync(
Policy.Handle<Exception>()
.FallbackAsync(_ => Task.FromResult(GetCachedData())),
circuitBreaker,
Policy.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, i => TimeSpan.FromMilliseconds(200 * i)),
Policy.TimeoutAsync(TimeSpan.FromSeconds(3))
);
// 使用
var result = await resilientPolicy.ExecuteAsync(async () =>
{
return await httpClient.GetAsync("http://inventory-service/api/stock");
});TypeScript 版本
// 手動實作 Circuit Breaker
enum CircuitState { Closed, Open, HalfOpen }
class CircuitBreaker {
private state = CircuitState.Closed;
private failures = 0;
private lastFailureTime = 0;
private readonly threshold: number;
private readonly timeout: number;
constructor(threshold = 5, timeoutMs = 30000) {
this.threshold = threshold;
this.timeout = timeoutMs;
}
async call<T>(fn: () => Promise<T>, fallback: () => T): Promise<T> {
if (this.state === CircuitState.Open) {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = CircuitState.HalfOpen;
} else {
return fallback(); // 熔斷中,回傳 fallback
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
return fallback();
}
}
private onSuccess() {
this.failures = 0;
this.state = CircuitState.Closed;
}
private onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = CircuitState.Open;
}
}
}
// 使用
const breaker = new CircuitBreaker(5, 30000);
const data = await breaker.call(
() => fetch("http://inventory-service/api/stock").then(r => r.json()),
() => ({ stock: -1, source: "fallback" })
);Python 版本
# pybreaker — Python Circuit Breaker
import pybreaker
import requests
# 定義 Circuit Breaker
inventory_breaker = pybreaker.CircuitBreaker(
fail_max=5, # 失敗 5 次觸發熔斷
reset_timeout=30, # 30 秒後進入 Half-Open
exclude=[requests.exceptions.Timeout], # Timeout 不計入失敗
)
@inventory_breaker
def get_inventory(product_id: str) -> dict:
response = requests.get(
f"http://inventory-service/api/stock/{product_id}",
timeout=3
)
response.raise_for_status()
return response.json()
# 使用 with fallback
def get_inventory_safe(product_id: str) -> dict:
try:
return get_inventory(product_id)
except pybreaker.CircuitBreakerError:
# 熔斷中,回傳快取資料
return get_cached_inventory(product_id)
except Exception:
return {"stock": -1, "source": "error"}
# 監控熔斷狀態
print(f"State: {inventory_breaker.current_state}")
print(f"Fail count: {inventory_breaker.fail_counter}")結構圖
圖中 Order Service 的所有請求經過 Circuit Breaker。Closed 狀態下請求正常轉發到 Inventory Service。當失敗率超過閾值,Circuit Breaker 進入 Open 狀態,直接回傳 Fallback(Cache)的資料。Timeout 到期後進入 Half-Open 試探,成功則回到 Closed。
面試常見問題
Q: Circuit Breaker 和 Retry 的關係?應該怎麼組合?
A: Retry 處理短暫的瞬時故障(網路抖動),Circuit Breaker 處理持續的服務故障。組合順序:外層 Fallback → Circuit Breaker → Retry → Timeout。Retry 在 Circuit Breaker 內部,這樣 retry 的失敗會被計入熔斷統計。如果 Retry 在外部,熔斷後的 retry 是無意義的。
Q: 什麼是 Bulkhead Pattern?和 Circuit Breaker 有什麼關係?
A: Bulkhead(艙壁)限制同時對某個服務的並發請求數,像船的水密隔艙一樣隔離故障。Circuit Breaker 是在故障發生後斷開,Bulkhead 是預防性地限制影響範圍。兩者搭配使用:Bulkhead 限制並發,Circuit Breaker 偵測故障。
Q: 熔斷時的 Fallback 策略有哪些?
A: 依場景選擇:回傳快取資料(最近的有效結果)、回傳預設值(空列表、預設設定)、降級服務(完整功能降級為基本功能)、將請求排入佇列稍後處理。關鍵是 fallback 不能也依賴同一個故障服務,否則 fallback 也會失敗。
理解測驗
🤔 Circuit Breaker 的 Open 狀態代表什麼?
🤔 Half-Open 狀態的作用是什麼?
🤔 Retry 和 Circuit Breaker 的正確組合順序是什麼?
重點整理
💡一句話記住
Circuit Breaker = 智慧保險絲,故障自動斷開,恢復自動接回。 口訣:「Closed 放行,Open 擋,Half-Open 去試探」
| 概念 | 說明 | |------|------| | Closed | 正常狀態,監控失敗率 | | Open | 熔斷中,回傳 fallback | | Half-Open | 試探中,放行少量請求 | | Fallback | 熔斷時的備用方案(快取/預設值) | | 核心目的 | 防止故障級聯擴散,保護整體系統 |