2. 熔断器模式详解:状态机与状态转换条件

熔断器模式,说白了就是一个保护开关。就像你家里的保险丝,电流过大就跳闸,防止烧了整个电路。在微服务架构里,上下游服务之间频繁调用,一旦某个下游开始抽风——响应超时、抛出异常、连接打满——整个调用链可能被拖垮。

我刚开始做分布式系统的时候,有次线上事故就是这么来的。一个老旧的支付服务突然变慢,结果所有依赖它的上游服务线程全被占满,最终导致整个交易链路瘫痪了十几分钟。从那以后我就养成了个习惯:所有远程调用必须有熔断保护。

2.1 状态机的三个核心状态

熔断器内部维护一个有限状态机,只有三个状态:关闭(Closed)、打开(Open)、半开(Half-Open)。你别看状态数少,正是这三个状态的来回切换,才实现了「错误快速隔离 + 自动恢复」的能力。

状态 含义 是否允许请求通过
关闭(Closed) 正常状态,熔断器不干预 是,全部放行
打开(Open) 熔断触发,直接拒绝请求 否,快速失败
半开(Half-Open) 试探性恢复,放行少量请求 是,但受限

2.2 状态转换条件详解

每个状态什么时候切换?我把它拆成四个关键转换动作,你记牢了:

  1. 关闭 → 打开:错误率达到阈值(比如最近 100 个请求里有 60% 失败了)。我一般设 50% 作为起始值,线上再调。
  2. 打开 → 半开:等待时间到了(比如 30 秒)。这个时间也叫「熔断休眠期」,让下游喘口气。
  3. 半开 → 关闭:试探请求全部成功,下游恢复正常。
  4. 半开 → 打开:试探请求只要有一个失败,立刻跳回打开状态,重新计时。

核心逻辑:关闭状态下统计失败率,触发就打开;打开状态等待超时后进入半开试探;半开试探通过则恢复,失败则再次打开。整个循环自动执行,不需要人为介入。

为什么会设计成半开状态?你想想看,如果直接从打开变回关闭,万一下游还没恢复,瞬间又熔断,这叫「震荡」。半开就像一个灰度探测窗口,只放行 1~3 个请求,风险可控。

我曾经就踩过这个坑——把熔断恢复窗口设置成 5 秒,结果下游服务重启确实只需要 3 秒,但还有残余连接没释放,半开试探的第一个请求过去了,第二个又超时,直接又打开了。后来我加了最小试探请求数(比如至少连续成功 3 次),彻底解决了这个振荡问题。

2.3 状态机流程图

下面这张图把三个状态和四个转换关系画清楚了。建议你保存下来,面试时直接手画它:

熔断器状态机流程图 关闭(Closed) 全部放行,统计失败率 打开(Open) 直接拒绝,快速失败 半开(Half-Open) 试探请求,验证恢复 失败率超阈值 超时窗口结束 试探全部成功 试探请求失败 说明:三个状态自动流转,形成闭环。虚线条件由配置参数控制。

2.4 状态转换的核心参数

这些参数你在配置熔断器时一定会碰到。我把我常用的推荐值列出来,方便你直接参考:

参数名称 说明 推荐初始值 我踩过的坑
failureThreshold 触发打开的失败次数或比例 50%(滑动窗口内) 设太低(如 30%)容易误触发
openTimeout 打开状态的持续时间 30 ~ 60 秒 我曾设 120 秒,用户投诉等太久
halfOpenMaxRequests 半开状态下允许的试探请求数 3 ~ 5 设 1 个试探不够稳定,至少 3 个
slidingWindowSize 统计窗口大小(请求数或时间) 100 次 / 10 秒 窗口太小统计失真,太大反应迟钝

小提示:我习惯把熔断参数放到配置中心(比如 Nacos、Consul),这样线上不用重启就能调。有一次双十一流量骤涨,我远程把 failureThreshold 从 50% 提到 70%,瞬间稳住了。

2.5 滑动窗口如何统计失败率

很多初学者以为熔断器是「累计失败 N 次就断开」,其实不是。更严谨的做法是用滑动窗口统计最近一段时间的失败率。为什么?

你想想,如果从系统启动开始累计失败次数,一旦之前积压了大量成功请求,后面突发一大堆失败也很难触发熔断,这就失去了保护意义。滑动窗口只看最近 10 秒(或者最近 100 个请求),能快速响应瞬时故障。

业界常见的实现方式有两种:

  • 计数滑动窗口:把时间切成多个桶(bucket),每个桶记录这 1 秒内的成功/失败数。窗口向前滚动时丢弃最老的桶。Hystrix 用的就是这个方案。
  • 令牌桶 + 衰减:每个请求按成功/失败乘以权重,使用指数衰减来保近弃远。Resilience4j 的滑动窗口采用了类似思路。

我更喜欢计数滑动窗口,原因很简单:它直观、容易推理。调试的时候你打开监控面板,能看到每一秒的请求量,一眼就知道刚才那一秒发生了什么。

注意:滑动窗口的分桶数不能太少。我曾见过有人只分 2 个桶(每个桶 5 秒),结果桶边界正好把异常请求劈成两半,导致两个桶单独看失败率都不超过阈值,熔断死活不触发。后来我改成了 10 个桶,每个桶 1 秒,问题就消失了。

2.6 半开状态的试探策略

半开状态是整个状态机中最精妙的部分。说白了就是「我不知道你是不是好了,先放一枪看看」。具体的试探策略有多种,我按实用度排序:

  1. 全量试探:放行指定数量的请求,全部成功才关闭。最保险,但恢复慢。
  2. 递增试探:先放 1 个,成功了再放 2 个,以此类推。一旦失败立即回退。适合下游启动成本高的情况。
  3. 时间窗口试探:在半开状态下,每隔几秒放一个请求,连续若干次成功才关闭。这种方式最平滑,我经常用。

我个人习惯用第三种——时间窗口试探。因为它不会一下子把试探请求送完,万一下游是间歇性抖动,这种缓慢恢复的节奏反而能保住剩余的正常流量。举个例子,在半开状态下每 5 秒放 1 个请求,连续 3 次成功就关闭,这样即使第三次刚好碰上下游 GC 停顿导致失败,也只损失了 2 个请求,代价很小。

2.7 实战避坑清单

  • 不要把所有异常都算失败:我只把超时、连接拒绝、500 错误计入失败率。像 404、400 这种客户端错误是不应该熔断的,熔断保护的是服务端。
  • 慢调用同样需要熔断:我曾经遇到过接口没报错,但响应时间从 50ms 涨到 30 秒,结果所有线程被卡住。所以除了异常率,还要监控慢调用比例。
  • 半开试探请求不走缓存:半开状态下放行的请求必须走完整链路,不能命中缓存或降级数据。否则你永远测不出真实恢复情况。
  • 熔断日志要单独打印:把状态切换事件打到一个独立的日志文件里,方便事后复盘。我每次排查线上事故,优先翻的就是熔断日志。