嘉年華時間開始了。🎉

我們正在舉辦一場家庭派對,如果你人在班加羅爾或附近,你受邀參加。只需要一個條件:

不要遲到。

我們都有那個總是遲到的朋友。我們還是會邀請他。然後我們開始檢查手機。

「應該快到了。」

與此同時,每個人都站在那裡,計畫暫停,食物變冷,冰塊融化,耐心逐漸消失。某個時刻,問題已經不再是遲到的朋友。

問題是大家持續在等遲到的朋友。

而令人驚訝的是,分散式系統也有完全相同的問題。這也包括你的代理應用程式。🤖

當一個服務變慢或開始失敗時,系統的其他部分沒有繼續前進,而是持續在等它。

  • 請求保持開啟。
  • 執行緒保持佔用。
  • 連線被消耗。
  • 佇列開始成長。

最終,一個服務的問題會開始影響到原本健康的服務。這就是斷路器模式登場的時候。

斷路器字面上的意思就是這樣。如果你不知道它的作用,讓電機工程師來解釋給你聽。😎


電機工程中的斷路器 ⚡

在勉強撐過電機工程的 B.Tech 學業後,我第一次看到斷路器被寫成一個設計模式時,真的感到害怕。

我的大腦立刻跳到電力系統、變壓器、電流、電壓,以及那些期末考題,看起來像是專門設計來讓我不及格的。💀

但有趣的是:軟體斷路器借用了我們在電機工程學到的同樣概念。有三個重要的狀態:

閉合、開路、半開

  • 閉合電路意味著電流可以正常流動。開關是開啟的,電力可以到達燈泡。
  • 開路電路意味著電路中斷。電流無法流動,所以燈泡是關閉的。

Real Electrical Circuit for Analogy

「半開」並不是一個標準的電氣狀態。對於我們的軟體類比,可以將其視為一個測試狀態。發生故障後,我們不會立即允許所有流量,而是謹慎地允許部分流量來檢查系統是否已恢復。如果一切正常,我們就閉合電路。如果故障仍然存在,我們就再次開路。

有了這個類比,讓我們進入分散式系統。


分散式系統中的斷路器 🧱

這是嘉年華之夜,所以我們自然需要披薩。🍕

假設我們有一個 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 呼叫——同樣的規則適用:停止等待、快速失敗,並在再次信任之前測試恢復。

隨著你在分散式系統方面的工作越多,你會發現最有用的想法其實很簡單:

  • 停止等待
  • 快速失敗
  • 在再次信任之前測試恢復

如果答案是否定的...開路

我計劃撰寫更多關於分散式系統模式和後端工程的文章——這些是在真實正式環境系統中出現的東西,而不僅僅是教科書。

在那之前,去建立一些不會等待遲到朋友的東西。😄

如果你經營一個組織並希望我撰寫或製作影片教學,請務必與我聯繫 🤝