嘉年華時間開始了。🎉
我們正在舉辦一場家庭派對,如果你人在班加羅爾或附近,你受邀參加。只需要一個條件:
不要遲到。
我們都有那個總是遲到的朋友。我們還是會邀請他。然後我們開始檢查手機。
「應該快到了。」
與此同時,每個人都站在那裡,計畫暫停,食物變冷,冰塊融化,耐心逐漸消失。某個時刻,問題已經不再是遲到的朋友。
問題是大家持續在等遲到的朋友。
而令人驚訝的是,分散式系統也有完全相同的問題。這也包括你的代理應用程式。🤖
當一個服務變慢或開始失敗時,系統的其他部分沒有繼續前進,而是持續在等它。
- 請求保持開啟。
- 執行緒保持佔用。
- 連線被消耗。
- 佇列開始成長。
最終,一個服務的問題會開始影響到原本健康的服務。這就是斷路器模式登場的時候。
斷路器字面上的意思就是這樣。如果你不知道它的作用,讓電機工程師來解釋給你聽。😎
電機工程中的斷路器 ⚡
在勉強撐過電機工程的 B.Tech 學業後,我第一次看到斷路器被寫成一個設計模式時,真的感到害怕。
我的大腦立刻跳到電力系統、變壓器、電流、電壓,以及那些期末考題,看起來像是專門設計來讓我不及格的。💀
但有趣的是:軟體斷路器借用了我們在電機工程學到的同樣概念。有三個重要的狀態:
閉合、開路、半開
- 閉合電路意味著電流可以正常流動。開關是開啟的,電力可以到達燈泡。
- 開路電路意味著電路中斷。電流無法流動,所以燈泡是關閉的。
「半開」並不是一個標準的電氣狀態。對於我們的軟體類比,可以將其視為一個測試狀態。發生故障後,我們不會立即允許所有流量,而是謹慎地允許部分流量來檢查系統是否已恢復。如果一切正常,我們就閉合電路。如果故障仍然存在,我們就再次開路。
有了這個類比,讓我們進入分散式系統。
分散式系統中的斷路器 🧱
這是嘉年華之夜,所以我們自然需要披薩。🍕
假設我們有一個 PartyService,它需要呼叫 PizzaService 來下訂單。通常情況下,一切看起來都很美好:
Host
│
▼
PartyService
│
▼
PizzaService
│
▼
Pizza on the way ✅
Enter fullscreen mode Exit fullscreen mode
主辦人下訂單。PizzaService 接受它。PartyService 獲得確認。每個人都吃得很開心。
但是當 PizzaService 開始遇到壞日子時會發生什麼?
- 也許廚房超載(這是嘉年華之夜)。
- 也許服務正在崩潰。
- 也許網路很慢。
- 也許有人決定在週五晚上進行部署... 👀
現在流程看起來像這樣:
PartyService
│
▼
PizzaService
│
▼
Request Timeout ❌
Enter fullscreen mode Exit fullscreen mode
一個訂單等待幾秒鐘不一定是問題。但想像一下數千名飢餓的客人同時下訂單:
1000 Orders
│
▼
Slow PizzaService
│
▼
Threads Waiting
│
▼
Connections Waiting
│
▼
Queues Growing
│
▼
Resources Consumed
Enter fullscreen mode Exit fullscreen mode
而現在有趣的部分開始了。PizzaService 不健康。但PartyService 也可能變得不健康。
真正的問題:連鎖故障 🌊
想像以下架構:
Host
│
PartyService
╱ ╲
PizzaService DrinkService
Enter fullscreen mode Exit fullscreen mode
現在 PizzaService 宕機。PartyService 持續呼叫它。訂單持續等待。更多客人到來。更多執行緒被佔用。突然間 PartyService 也開始耗盡資源。
PizzaService ❌
│
▼
PartyService ❌
│
▼
Whole Application ❌
Enter fullscreen mode Exit fullscreen mode
一個服務失敗,但故障透過系統傳播。這就是我們所說的連鎖故障——而這正是斷路器試圖防止的。
斷路器如何運作?🚦
斷路器位於你的服務和遠端依賴之間:
PartyService → Circuit Breaker (watches every call) → PizzaService
Enter fullscreen mode Exit fullscreen mode
- 如果一切看起來健康,請求會正常通過。
- 如果依賴開始重複失敗,斷路器會說:「好吧,暫停呼叫該服務一段時間。」
這給我們三個狀態。
1. 閉合狀態 🟢
正常狀態。請求通過,斷路器持續監控。
Request 1 → ✅
Request 2 → ✅
Request 3 → ❌ (network fail, packet drop, kitchens have bad days)
Request 4 → ✅
Enter fullscreen mode Exit fullscreen mode
少數失敗並不意味著服務已死亡。所以當故障率在可接受範圍內時,電路保持閉合。
2. 開路狀態 🔴
現在失敗開始累積:
Request 1 → ❌
Request 2 → ❌
Request 3 → ❌
Request 4 → ❌
Enter fullscreen mode Exit fullscreen mode
某個時刻斷路器決定:「嗯...這裡有問題。」 😅
CLOSED --(too many failures)--> OPEN
Enter fullscreen mode Exit fullscreen mode
現在它停止向 PizzaService 發送請求。
Request → Circuit Breaker → ❌ → Fail Fast / Fallback
Enter fullscreen mode Exit fullscreen mode
請求立即失敗。沒有不必要的網路呼叫,沒有等待超時,不會給已經不健康的服務增加額外負擔。
3. 半開狀態 🟡
但我們不能讓電路永遠開路。如果 PizzaService 恢復了呢?等待一段時間後,斷路器移至半開狀態,並允許一些測試請求通過。
Test Request 1 → ✅
Test Request 2 → ✅ → back to CLOSED 🟢
Test Request 1 → ❌ → back to OPEN 🔴
Enter fullscreen mode Exit fullscreen mode
這就是半開狀態的重要性。我們不會盲目地再次信任服務——我們先測試它。
完整流程
CLOSED 🟢
│
│ too many failures
▼
OPEN 🔴
│
│ wait timeout elapses
▼
HALF-OPEN 🟡
│ │
success failure
│ │
▼ ▼
CLOSED 🟢 OPEN 🔴
Enter fullscreen mode Exit fullscreen mode
心智模型很簡單:
閉合 → 讓請求通過
開路 → 停止呼叫依賴
半開 → 仔細測試它是否已恢復
現在讓我們來編寫一個。
讓我們用 Java 建立一個斷路器
使用函式庫而不了解底層發生的事情,有點像開車不知道煞車的作用。它可以運作...直到它不能運作為止。😅
首先,我們的狀態:
public enum CircuitState {
CLOSED,
OPEN,
HALF_OPEN
}
Enter fullscreen mode Exit fullscreen mode
現在是斷路器本身。我們追蹤當前狀態、失敗次數、閾值、開路持續時間,以及它何時開路。
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class CircuitBreaker {
private final int failureThreshold;
private final long openWaitMillis;
private final AtomicReference<CircuitState> state =
new AtomicReference<>(CircuitState.CLOSED);
private final AtomicInteger failureCount = new AtomicInteger(0);
private volatile long openedAt = 0;
public CircuitBreaker(int failureThreshold, long openWaitMillis) {
this.failureThreshold = failureThreshold;
this.openWaitMillis = openWaitMillis;
}
public <T> T execute(Callable<T> call, Callable<T> fallback) throws Exception {
if (state.get() == CircuitState.OPEN) {
if (System.currentTimeMillis() - openedAt >= openWaitMillis) {
state.set(CircuitState.HALF_OPEN);
} else {
return fallback.call(); // fail fast
}
}
try {
T result = call.call();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
return fallback.call();
}
}
private void onSuccess() {
failureCount.set(0);
state.set(CircuitState.CLOSED);
}
private void onFailure() {
int failures = failureCount.incrementAndGet();
if (failures >= failureThreshold) {
state.set(CircuitState.OPEN);
openedAt = System.currentTimeMillis();
}
}
}
Enter fullscreen mode Exit fullscreen mode
注意當電路開路時發生什麼:我們根本不呼叫遠端服務。我們只是快速失敗。
如果我們配置 new CircuitBreaker(3, 10_000):
3 failures → OPEN
Wait 10s → HALF_OPEN
Test succeeds → CLOSED
Test fails → back to OPEN
Enter fullscreen mode Exit fullscreen mode
使用方式如下:
PizzaOrder order = breaker.execute(
() -> pizzaService.order(guestId),
() -> PizzaOrder.fallback(guestId)
);
Enter fullscreen mode Exit fullscreen mode
一旦達到閾值,下一個訂單甚至不會到達 PizzaService——它會快速失敗。依賴不健康,所以我們停止向它發送流量。
但這並非可供正式環境使用 👀
在有人將這個類別複製到正式環境服務之前...請不要這樣做。😅
這只是足夠理解概念。真正的斷路器需要處理故障率、滑動視窗、並行性、超時、慢速呼叫、例外過濾、指標和回退。
例如,50 次失敗算嚴重嗎?
1000 successful requests + 50 failures → probably fine
Last 100 calls: 50 failures (50%) → definitely not fine
Enter fullscreen mode Exit fullscreen mode
一個最近的呼叫視窗比原始計數更有意義。這就是為什麼正式環境應用程式通常會選擇使用函式庫。
斷路器 vs. 重試 vs. 超時
你可能在想:「為什麼不在請求失敗時重試呢?」
重試解決的是不同的問題。
- 重試說:「也許這是暫時的。讓我們再試一次。」
- 斷路器說:「我們已經看到足夠多的失敗。停止呼叫這個依賴。」
想像 PizzaService 已經超載,你有 10,000 個訂單,每個訂單重試三次。現在你用數千個額外請求來打擊一個掙扎的服務。這基本上是敲你遲到朋友的門四次而不是一次。😂
所以分散式系統結合這些模式,每個模式有不同的工作:
| 模式 | 它回答的問題 |
|---|---|
| 超時 | 我願意等待多久? |
| 重試 | 我應該再試一次嗎? |
| 斷路器 | 我應該停止呼叫此服務嗎? |
等等,生成式 AI 應用程式也需要這個 💎
這是大多數人錯過的部分。
啟發這篇部落格的戰爭故事 ❤️🔥
就在幾週前,我自己也錯過了這一點,這也是這篇部落格存在的原因。
我正在建立一個模型編排器——一個系統,其中一個傳入的請求不只是呼叫單一 LLM,而是透過一個小型的 LLM 呼叫管道,最後才得到答案:
Request
│
▼
Router LLM → figures out what the user actually wants
│
▼
Agent LLM → does the actual task (search, generate, calculate, etc.)
│
▼
Judge LLM → double-checks the agent's answer before sending it back
│
▼
Final Response
Enter fullscreen mode Exit fullscreen mode
把它想像成一個餐廳訂單經過三個人:有人接你的訂單,有人烹飪,有人裝盤然後送到你的桌上。如果其中任何一個人安靜地什麼都不做,只是傳遞一個空盤子,你仍然會得到某樣東西——只是不是你點的東西。
這基本上就是發生的事情。
使用者開始得到奇怪、不一致的回應。最初的 30 分鐘,我還不夠擔心去檢查——監控顯示92% 的請求回應 200 OK。在大多數系統中,200 狀態意味著「這有效」。所以我假設一切正常。
事實並非如此。當使用者持續回報糟糕的答案時,我們終於看過狀態碼,查看實際的回應負載——並發現一個我沒有密切關注的欄位:confidence = 0。
這是實際在底層發生的事情:
Router LLM ──fails/times out──▶ falls back silently
Agent LLM ──fails/times out──▶ falls back silently
Judge LLM ──fails/times out──▶ falls back silently
│
▼
Response: 200 OK, confidence = 0
(looks healthy in the logs — nothing "crashed")
Enter fullscreen mode Exit fullscreen mode
管道的每個階段都在失敗,並安靜地回退到預設回應。沒有例外。沒有 500 錯誤。沒有警報。只是一個技術上成功的 HTTP 回應,帶著一個無用的答案。
我已經為鏈中單一 LLM 呼叫可能失敗的情況建立了回退處理。我沒有考慮的是整個鏈一起失敗——因為實際的根本原因根本不在我的程式碼中。與此同時,我們也開始看到 429 Too Many Requests 錯誤,最終追蹤到供應商端的事件:Anthropic 的狀態頁面確認模型 API 當天已降級。
解決方案是在控制器層級新增一個斷路器——位於所有三個 LLM 呼叫前面的層級。在用真實流量信任管道之前,斷路器會發送一個微小、便宜的探測請求(只需一個 token)來確認 LLM 實際上正在接受和回應呼叫。如果探測失敗,斷路器立即開路,整個管道快速失敗並返回明確的錯誤,而不是安靜地返回包裝成成功的損壞答案。
這就是多代理系統真正的危險:故障並不總是看起來像崩潰。有時它看起來像是三個回退禮貌地同意對你說謊。
生成式 AI 是新的火源。每個人都建立在 LLM 供應商之上——OpenAI、Anthropic、Gemini、自架模型,無論是什麼。而這些呼叫中的每一個...都是對遠端依賴的網路呼叫,而遠端依賴可能緩慢、受速率限制或宕機。
聽起來很熟悉嗎?
LLM 呼叫基本上是最終版的遲到朋友:
- 回應可能需要數秒(一個接一個地串流 token)。
- 當你達到速率限制時,供應商會返回
429 Too Many Requests。 - 單一代理步驟可以扇出成許多模型呼叫(工具、重試、反思迴圈)——所以一個不穩定的依賴可能快速倍增。
現在想像一下經典的 naive 設定:
Your AI App → prompt fails or times out → Retry ×3 → Retry ×3 → already rate-limited LLM provider
Enter fullscreen mode Exit fullscreen mode
重試受速率限制的模型就像從已經著火的廚房訂更多披薩。每次重試都會讓你更深入速率限制,你的延遲會爆炸,你的 token 帳單也會安靜地成長。
在模型用戶端前面放一個斷路器可以解決這個問題:
AI Request
│
▼
Circuit Breaker
╱ │ ╲
CLOSED OPEN HALF-OPEN
│ │ │
▼ ▼ ▼
LLM Fallback Test call
Provider to LLM
Enter fullscreen mode Exit fullscreen mode
而回退是生成式 AI 應用程式可以展現聰明之處的地方:
Model unavailable
├── Return a cached / similar answer
├── Drop to a smaller, cheaper model
├── Return a "please try again shortly" message
└── Queue the request for later processing
Enter fullscreen mode Exit fullscreen mode
所以下次你的代理迴圈在 5 秒內呼叫模型 40 次時,請記住:即使是最精緻的 AI 也需要在繼續敲供應商的門之前先有一個斷路器。
同樣的舊模式,全新的火源。
回退應該做什麼?🤔
回退並不意味著 return null。請不要這樣做。
Dependency Down
├── Return cached data
├── Queue the request
├── Drop to a cheaper path
└── Return a graceful error
Enter fullscreen mode Exit fullscreen mode
重要的規則:不要偽造成功。如果披薩訂單失敗,不要因為你的回退需要返回某樣東西而告訴客人「訂單已確認 ✅」。
優雅的失敗比成功的謊言好得多。
一個重要的問題 🚨
應該讓每個例外都開路嗎?不是。
400 Bad Request(客人發送了無效輸入)並不意味著服務不健康。但是這些是強烈的信號:
Connection refused
Timeout
503 Service Unavailable
429 Too Many Requests
Enter fullscreen mode Exit fullscreen mode
所以在配置斷路器時,最重要的問題之一是:
哪些失敗應該真正計數?
這個決定是應用程式特定的。
把我們遲到的朋友帶回來 😊
- 閉合 🟢 — 朋友:「我還有 5 分鐘就到。」 大家:「好,👍」。派對繼續。
- 開路 🔴 — 在「5 分鐘」→「快到了」→「塞車」→「找停車位」 兩個小時後,你終於說:「沒有人再等了。」 派對繼續進行,沒有他們。
- 半開 🟡 — 稍後:「兄弟,你到了嗎?」 → 「是的」= 派對繼續。「再 5 分鐘」= 門再次關閉。
這就是斷路器。
最終結論
每當你聽到斷路器,請記住三個狀態:
CLOSED → requests flow normally
OPEN → requests stop immediately
HALF-OPEN → a few requests test recovery
Enter fullscreen mode Exit fullscreen mode
或者,用班加羅爾家庭派對的術語:
Friend is coming → Everyone waits → Friend keeps failing → STOP WAITING → Party Continues 😎
Enter fullscreen mode Exit fullscreen mode
斷路器不會修復失敗的服務。它會在該服務失敗時保護系統的其餘部分——無論是披薩店、支付 API,還是你最愛的 LLM。
故障是不可避免的。連鎖故障不一定要發生。
結語 💚
首先,非常感謝你讀到這裡。🎉
請記住,分散式系統有點像那個遲到的朋友——故障是不可避免的,但你如何處理它們完全取決於你。斷路器不會修復損壞的服務,但它會阻止一個壞的依賴帶著你的整個系統(或你的整個派對)一起垮掉。
這篇部落格的目的是要揭開一個聽起來很嚇人的模式,一旦你將它視為三個狀態和一個計時器,它就真的很簡單。無論是披薩訂單、支付 API,還是 LLM 呼叫——同樣的規則適用:停止等待、快速失敗,並在再次信任之前測試恢復。
隨著你在分散式系統方面的工作越多,你會發現最有用的想法其實很簡單:
- 停止等待
- 快速失敗
- 在再次信任之前測試恢復
如果答案是否定的...開路。
我計劃撰寫更多關於分散式系統模式和後端工程的文章——這些是在真實正式環境系統中出現的東西,而不僅僅是教科書。
在那之前,去建立一些不會等待遲到朋友的東西。😄
如果你經營一個組織並希望我撰寫或製作影片教學,請務必與我聯繫 🤝

0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.