超时重试不是熔断——Circuit Breaker 的三个状态你一个都没用对

超时重试不是熔断——Circuit Breaker 的三个状态你一个都没用对

你的服务调用下游超时了,你加了个重试。超时三次了,你加了降级。然后有一天凌晨三点,下游恢复了,但你发现——你的服务还在降级。

因为你用的不是熔断器,是超时加重试。

超时 + 重试 ≠ 熔断

超时和重试解决的是"单次调用失败怎么办"。熔断器解决的是"连续失败怎么办"。

这俩的区别听起来像废话,但在生产环境里差别巨大:

超时+重试的逻辑:每次调用都尝试,失败了等一会儿再试。不管之前失败了多少次,下一秒的请求照样发出去。

熔断器的逻辑:连续失败到一定阈值后,直接拒绝后续请求,不再尝试调用。过一段时间后放一个探测请求试试,成功了才恢复正常调用。

区别在哪?超时+重试模式下,下游已经崩了,你的每个请求还在傻等超时时间(比如 3 秒),100 个并发请求就卡住了 300 秒的线程资源。线程池耗尽,上游也开始超时,雪崩就这么来的。

熔断器模式下,下游崩了,第 N 个请求触发熔断后,后续请求直接走降级逻辑,响应时间从 3 秒降到 10 毫秒。线程池没事,上游没事,雪崩链断了。

三个状态,大多数人只用了一个

Circuit Breaker 有三个状态:Closed、Open、Half-Open。

Closed(闭合):正常状态,所有请求直接通过。同时统计失败率。

Open(断开):熔断状态,所有请求直接走降级,不调用下游。

Half-Open(半开):试探状态,放少量请求通过试探下游是否恢复。

大多数人配置了 Closed → Open 的转换条件(比如"失败率 > 50% 触发熔断"),然后就没管了。Open 之后什么时候恢复?大部分人配了个固定时间,比如 30 秒后自动转回 Closed。

问题在哪?30 秒后下游还没恢复,你又全量放请求进去,又超时,又熔断,又等 30 秒,又全量放进去——你的服务在"熔断-恢复-熔断"之间反复横跳,下游永远得不到真正的恢复窗口。

这就是 Half-Open 存在的原因。Open 状态持续一段时间后,不是直接转回 Closed,而是转入 Half-Open。Half-Open 只放 1-5 个请求试探下游,如果这些请求成功了,才转回 Closed;如果还是失败,转回 Open,继续等待。

Half-Open 是熔断器最关键的状态,也是被忽略最多的状态。没有它,熔断器就退化成了"超时+降级+定时恢复",跟手动配一个降级开关没有本质区别。

状态转换的四个参数,配错一个就崩

熔断器的状态转换由四个参数控制:

  1. failureRateThreshold(失败率阈值):Closed → Open 的触发条件。比如 50%,意味着统计窗口内超过一半的请求失败就熔断。

  2. slidingWindowSize(滑动窗口大小):统计失败率的请求数量窗口。比如 100,意味着看最近 100 次请求的失败率。

  3. waitDurationInOpenState(Open 状态持续时间):熔断后保持多久再转入 Half-Open。比如 30 秒。

  4. permittedNumberOfCallsInHalfOpenState(Half-Open 允许的试探请求数):放多少个请求去试探。比如 5。

这四个参数最常见的配置错误:

滑动窗口太大。配了 1000,意味着要凑够 1000 次请求才能算失败率。如果你的 QPS 只有 10,得等 100 秒才能触发熔断——在这 100 秒里,每个请求都在傻等超时。窗口应该跟你的 QPS 匹配,QPS 低的时候用时间窗口而不是计数窗口。

失败率阈值太低。配了 10%,意味着 10 次请求里 1 次失败就熔断。偶发的超时、网络抖动都会误触发。阈值应该设在 50% 以上,偶尔的抖动不应该触发全局熔断。

Open 持续时间太短。配了 5 秒,下游还在重启你就开始试探了。应该配到下游的平均恢复时间以上——数据库故障可能需要 30-60 秒恢复,你的 waitDuration 也应该在这个量级。

Half-Open 试探数太多。配了 50,Half-Open 变成了"半恢复",50 个请求同时冲进去可能把刚恢复的下游又打崩。试探数应该是 1-5,够判断恢复状态就行。

Resilience4j vs Sentinel:同一个模式,两种实现

Java 生态里最主流的两个熔断器实现是 Resilience4j 和 Sentinel。它们的差异不在理念,在统计模型。

Resilience4j用滑动窗口统计。窗口分两种: - 计数窗口(Count-based):看最近 N 次请求的失败率 - 时间窗口(Time-based):看最近 N 秒内请求的失败率

计数窗口适合高 QPS,时间窗口适合低 QPS。低 QPS 场景下如果用计数窗口,窗口填充太慢,熔断触发延迟严重——这就是前面说的"100 秒才能熔断"的问题。

Sentinel用滑动窗口 + LeapArray。LeapArray 是 Sentinel 自研的统计结构,把时间切成一个个 Bucket(比如每秒一个 Bucket),每个 Bucket 记录该秒内的成功/失败数。统计时取最近 N 个 Bucket 聚合。

这两种实现各有坑:

Resilience4j 的坑:默认是 Count-based 窗口,低 QPS 场景下必须手动换成 Time-based,否则熔断触发太慢。很多人用默认配置上线,低 QPS 的服务根本熔断不了。

Sentinel 的坑:Bucket 粒度是秒级,如果你的 QPS 极低(比如 1 QPS),单个 Bucket 里只有 1 个请求,失败率要么 0% 要么 100%,统计精度不够。Sentinel 的应对方式是设一个 minRequestAmount,请求量不够时不触发熔断——但这又意味着极低 QPS 的服务永远不会熔断。

什么时候不该用熔断器

熔断器不是万能的。有几种场景用了反而添乱:

读操作的重试成本很低。查询下游缓存超时了,重试一次大概率就成功了。熔断器的代价是直接拒绝请求走降级,降级返回的数据可能是空的或过时的。对于读操作,重试比熔断更合适。

下游的故障是瞬态的。网络抖动导致的偶发超时,不应该触发全局熔断。如果失败率阈值设得太低,瞬态故障会被误判为持续故障,正常请求被无端降级。

你的服务没有可用的降级逻辑。熔断后走降级,降级返回什么?如果降级就是抛异常,那熔断的意义只剩"快失败"——线程池不会耗尽了,但用户体验一样烂。没有有意义的降级逻辑时,熔断器只是把"慢失败"换成了"快失败",治标不治本。

调用频率极低。一天调 10 次的接口,加熔断器是过度工程。这种场景下手动降级开关更实用——你不需要自动化统计和状态转换,因为故障的影响范围本身很小。

一个可运行的配置模板

如果你决定用熔断器,Resilience4j 的推荐起步配置:

yaml resilience4j.circuitbreaker: instances: orderService: failureRateThreshold: 50 slidingWindowSize: 100 slidingWindowType: COUNT_BASED waitDurationInOpenState: 30s permittedNumberOfCallsInHalfOpenState: 5 minimumNumberOfCalls: 10 slowCallRateThreshold: 80 slowCallDurationThreshold: 3s

注意两个额外参数:

  • minimumNumberOfCalls: 10:窗口内至少 10 次请求才开始计算失败率。防止启动阶段零星请求就触发熔断。
  • slowCallRateThreshold + slowCallDurationThreshold:慢调用也算失败。不是只有异常才算失败,超过 3 秒的调用也算。

Sentinel 的等价配置:

java DegradeRule rule = new DegradeRule("orderService") .setGrade(CircuitBreakerStrategy.RATIO.getGrade()) .setCount(0.5d) // 50% 失败率 .setTimeWindow(30) // Open 持续 30 秒 .setMinRequestAmount(10) .setStatIntervalMs(10000); // 统计窗口 10 秒

Sentinel 的 Half-Open 试探是自动的:waitDuration 结束后放一个请求试探,成功就恢复,失败就继续 Open。permittedNumberOfCallsInHalfOpenState 这个参数在 Sentinel 里不需要手动配。

熔断器的正确使用姿势

  1. 只用在写操作和高 QPS 读操作上。低 QPS 读操作用重试就行。
  2. 必须配 Half-Open。没有 Half-Open 的熔断器不配叫熔断器。
  3. 滑动窗口类型根据 QPS 选择:高 QPS 用计数窗口,低 QPS 用时间窗口。
  4. 失败率阈值 ≥ 50%。低阈值在瞬态故障下误触发概率太高。
  5. 必须有降级逻辑。没有降级的熔断器只是换了一种失败方式。
  6. Half-Open 试探数 ≤ 5。刚恢复的下游扛不住大批量请求。

  7. 监控状态转换。熔断→恢复的频率太高,说明下游有持续性问题需要根治,而不是靠熔断器兜底。

熔断器是一个安全装置,不是性能优化手段。它的目的是在下游故障时保护上游不跟着崩,给下游恢复的空间。如果你把熔断器当成"让服务更稳定"的手段,那你大概率只是在掩盖问题,而不是解决问题。


如果你对设计模式这种"一看就懂、一用就错"的内容感兴趣,我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个模式用漫画加答题的方式讲,搜一下就能找到。