bigywater.tech
微服务 · 5

熔断器的状态机:三个状态、半开放行多少探测请求,手写版与 gobreaker 对照

熔断器的三个状态怎么转换?半开状态该放行多少个探测请求?手写的状态机和 gobreaker 一不一样?下游恢复了但不稳定时,半开放得越多是越好还是越糟?这篇用本机的小服务和 gobreaker 量出来。

10 对 10
同一剧本下手写熔断器与 gobreaker v2.4.0 的状态转换次数,序列完全相同
2 / 6 / 18
服务不可用期间,半开放行 1 / 3 / 10 个时,放过去又失败的探测请求数
≥ 599 s
下游成功率 50%、半开放行 10 个时,从打开期满算起到关闭的中位数(放行 1 个是 0.00 s,放行 3 个是 5.42 s)

状态机

实测 · 本机执行输出
  时刻      | 从         | 到        
  --------+-----------+----------
  703 ms  | closed    | open     
  1759 ms | open      | half-open
  1759 ms | half-open | open     
  2810 ms | open      | half-open
  2901 ms | half-open | closed   
  4201 ms | closed    | open     
  5252 ms | open      | half-open
  5252 ms | half-open | open     
  6310 ms | open      | half-open
  6402 ms | half-open | closed   
  160 次调用里:真正到达下游 80 次(其中失败 12 次),被熔断器直接拒绝 80 次,半开状态因放行名额用完被拒 0 次

连续失败打开,到期后第一个请求触发半开,半开里连续成功才关闭,失败一次就重新打开。

和 gobreaker 对照

实测 · 本机执行输出
  #  | 手写:转换              | 时刻      | gobreaker:转换       | 时刻     
  ---+--------------------+---------+--------------------+--------
  1  | closed → open      | 703 ms  | closed → open      | 702 ms 
  2  | open → half-open   | 1759 ms | open → half-open   | 1752 ms
  3  | half-open → open   | 1759 ms | half-open → open   | 1752 ms
  4  | open → half-open   | 2810 ms | open → half-open   | 2802 ms
  5  | half-open → closed | 2901 ms | half-open → closed | 2902 ms
  6  | closed → open      | 4201 ms | closed → open      | 4200 ms
  7  | open → half-open   | 5252 ms | open → half-open   | 5252 ms
  8  | half-open → open   | 5252 ms | half-open → open   | 5252 ms
  9  | open → half-open   | 6310 ms | open → half-open   | 6302 ms
  10 | half-open → closed | 6402 ms | half-open → closed | 6402 ms
  转换序列完全相同(10 次对 10 次);gobreaker:到达下游 80 次(失败 12),拒绝(打开)80,拒绝(半开名额用完)0;手写:到达 80(失败 12),拒绝(打开)80,拒绝(半开名额用完)0

同一个剧本下,两边的转换序列完全相同,时刻差在 10 ms 以内。

半开放行多少

实测 · 本机执行输出
  半开放行数 | 打开的次数 | 进入半开的次数 | 到达下游的请求 | 半开期间到达下游的失败 | 被拒(打开) | 被拒(半开名额用完)
  ------+-------+---------+---------+-------------+--------+-----------
  1     | 3     | 3       | 1720    | 2           | 2295   | 20        
  3     | 3     | 3       | 1731    | 6           | 2276   | 27        
  10    | 3     | 3       | 1761    | 18          | 2270   | 7         
  (连接类错误 0 次;不是超时,应为 0)

下游仍不可用时,每次半开都要让 N 个请求去试;放行越多,下游多挨的越多。

恢复后不稳定

实测 · 本机执行输出
  下游成功率    | 半开放行 1                                 | 半开放行 3                                  | 半开放行 10                                   
  ---------+----------------------------------------+-----------------------------------------+-------------------------------------------
  q = 100% | 中位数 0.00 s / p90 0.00 s / 平均重新打开 0.0 次 | 中位数 0.10 s / p90 0.10 s / 平均重新打开 0.0 次  | 中位数 0.45 s / p90 0.45 s / 平均重新打开 0.0 次    
  q = 90%  | 中位数 0.00 s / p90 1.00 s / 平均重新打开 0.1 次 | 中位数 0.10 s / p90 1.20 s / 平均重新打开 0.4 次  | 中位数 1.80 s / p90 6.85 s / 平均重新打开 2.0 次    
  q = 70%  | 中位数 0.00 s / p90 1.00 s / 平均重新打开 0.4 次 | 中位数 1.15 s / p90 5.50 s / 平均重新打开 2.0 次  | 中位数 25.33 s / p90 81.73 s / 平均重新打开 32.0 次 
  q = 50%  | 中位数 0.00 s / p90 3.10 s / 平均重新打开 1.0 次 | 中位数 5.42 s / p90 18.55 s / 平均重新打开 7.4 次 | 中位数 ≥ 599 s / p90 ≥ 599 s / 平均重新打开 430.9 次
  半开要连续 N 个成功才关闭,所以下游成功率 q 不高时,N 越大越容易在半开里又失败一次、重新打开(q^N 是一轮半开通过的概率)

要连续 N 个成功才关闭,下游成功率不高时 N 越大越难关上。

分步演示

熔断器的状态机
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。手写的 Breaker 是 Python 3.14 的我自己的实现(连续失败计数、半开最多放行 N 个、N 个连续成功才关闭);gobreaker 是 github.com/sony/gobreaker/v2 v2.4.0,用 Go 1.27.1 编译,Settings 里 MaxRequests 3、Interval 0、Timeout 1025 ms;两边的剧本是每 50 ms 调用一次的函数,不经过网络;第三节用的是真实的本机 HTTP 服务和 20 个并发线程;第四节用虚拟时钟(不真的 sleep)
    • 没有验证:按失败率或慢调用率的统计窗口触发(见下一篇);gobreaker 的滚动窗口(BucketPeriod)与 Interval 的行为;真实网络和多实例下各实例熔断器状态不共享的影响;第三节只跑了一轮,三个放行数的“到达下游的请求”差异在噪声内,只有半开期间的失败数(2、6、18)的规律是由放行数直接决定的
    京ICP备2026067502号-1京公网安备11010502063487号