狂欢时刻已经开始。🎉
我们正在举办一场家庭派对,如果你住在班加罗尔附近,欢迎你参加。只需满足一个条件:
不要迟到。
我们都有那个总是迟到的朋友。我们依然邀请他们。然后我们开始查看手机。
“马上就到。”
与此同时,每个人都在等待,计划暂停,食物变凉,冰块融化,耐心慢慢消失。到了某个时刻,问题已不再是迟到的朋友。
问题在于大家还在继续等待那个迟到的朋友。
令人惊讶的是,分布式系统也有完全相同的问题。这包括你的智能体应用。🤖
某个服务变慢或开始失败,而系统其他部分并没有继续前进,而是继续等待它。
- 请求保持打开。
- 线程保持占用。
- 连接被消耗。
- 队列开始增长。
最终,一个服务的问题开始影响其他完全健康的服务。这正是断路器模式发挥作用的时候。
断路器字面意思就是如此。如果你不知道它做什么,让电气工程师给你解释。😎
电气工程中的断路器 ⚡
在勉强完成电气工程本科学业后,我第一次看到断路器作为设计模式出现时,确实感到害怕。
我的大脑立刻跳到电力系统、变压器、电流、电压,以及那些看起来像是专门为让我不及格设计的期末考试题。💀
但有趣的是:软件断路器借用了我们在电气工程中学到的相同理念。有三个重要状态:
闭合、断开和半闭合
- 闭合电路意味着电流可以正常流动。开关打开,电到达灯泡。
- 断开电路意味着电路中断。电流无法流动,所以灯泡关闭。
“半闭合”并不是真正的标准电气状态。对于我们的软件类比,可以将其视为测试状态。故障后,我们不会立即允许所有流量,而是谨慎地允许部分流量,以检查系统是否已恢复。如果一切正常,我们闭合电路。如果故障仍然存在,我们再次断开。
有了这个类比,让我们进入分布式系统。
分布式系统中的断路器 🧱
狂欢之夜,自然需要披萨。🍕
假设我们有一个 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。 - 单个智能体步骤可以扇出到许多模型调用(工具、重试、反思循环)——所以一个不稳定的依赖可能会快速放大。
现在想象一下经典的幼稚设置:
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.