3.1 熔断器:阈值与半开状态

熔断是保命层里最关键的一格:它不阻止下游故障,而是阻止故障顺着调用链放大。1.1 篇那次缓存抖动,入口 QPS 从 1200 跌到 150,根因就是没有熔断,慢请求把线程池拖满。这一篇把熔断状态机、阈值依据、半开探测全部写出来,参数来自 5.1 的压测反推。

场景:没有熔断时发生了什么

指标 无熔断 有熔断
下游抖动 20s 时入口 QPS 1200 跌到 150 最低 950
故障放大率 8 倍 约 1.3 倍
恢复后入口恢复时长 60s 8s

数字来自 5.1 的故障注入回放。有熔断后入口只掉 21%,说明调用链被截断了。

回放脚本的构造也交代一下:在库存服务前注入 20% 延迟加 40% 错误率,持续 20s 后撤除,入口流量恒定 1200 QPS 压 5 分钟,录三段曲线。这个脚本后来成了每次熔断参数变更的回归基线,参数一动先跑回放再上看板。无熔断曲线的 8 倍放大率拆解是:库存慢响应占住订单线程 20s,订单线程池 200 个全占满,网关等订单超时又拉满网关线程,放大 8 倍正好对应三段调用链的串联比。

方案:三态状态机

状态 进入条件 行为
关闭 默认 正常放行,滑动窗口统计
打开 5s 窗口错误率 ≥ 50% 且样本 ≥ 20 直接拒绝,走降级
半开 打开满 30s 放行 3 个探测

半开探测成功 3/3 回关闭,1 个失败立刻回打开,冷却时间翻倍到 60s。冷却翻倍是防「抖动期反复开合」的关键:下游没真正恢复时,固定 30s 冷却会让状态机每个周期都在开合边缘震荡,观测到的现象是错误率锯齿形。熔断状态机与探活摘除(2.4)是两套机制:探活管「实例级」健康,熔断管「方法级」行为,前者摘一个实例,后者拒一个接口的全部流量。两篇的对照表在 go-micro-cps-guide 的韧性章节也有,口径一致。

配置与代码:参数从哪来

// 熔断策略(govkit 默认值,全服务统一)
BreakerPolicy{
    Window:   5 * time.Second, // 统计窗口
    MinReq:   20,              // 最小样本,防低流量误熔断
    ErrRatio: 0.5,            // 错误率阈值
    OpenFor:  30 * time.Second,
    HalfOpen: 3,             // 探测数
}

窗口 5s 加 20 个最小样本的组合有讲究:低 QPS 服务(比如内部对账 5 QPS)5s 窗口可能不足 20 样本,此时不熔断(样本不够不做判定),宁漏勿错。错误率 50% 是 5.1 压测拿到的拐点:注入 40% 错误时下游还能自行消化,注入 60% 时线程池 10s 打满,50% 取在两者之间。

熔断器的计数是进程内原子变量,不做跨实例聚合。有同学提议把计数写进 Redis 做「全集群熔断」,被否:集群聚合会把一个实例的视角放大到全集群,某实例到下游网络抖动就触发全局熔断,影响面反而失控。跨实例状态用 etcd 或 Redis 的方案留给服务间限流(3.4),熔断坚持本地视角。

状态机的切换全部打点:breaker=open svc=orders reason=err_ratio 进 CLS,4.4 篇检索时直接看开关轨迹。

故障复盘:一次误熔断

上线第三周出过一次误熔断:订单服务熔断库存接口 40 分钟。查下来是库存服务一次发版,灰度桶里 20% 流量打到新实例,新实例冷启动期缓存未热,错误率顶到 61%,刚好过线。暴露的问题有两个:冷启动没有 warmup 期豁免;熔断告警只报了「熔断」没报「哪个方法、错误率多少」。之后两条都补上:实例注册带 warm=0 标记,warmup 内(60s)请求不计入熔断统计;告警文案固定带方法名与窗口快照,告警到值班时 10 秒内能定位。误熔断复盘的方法论与 k8s-pipeline-guide 的发布事故复盘一致:先定位参数,再定位数据。

复盘还顺带把一件事定死了:熔断告警的分级。打开持续 1 分钟是 P3(记录不呼叫),10 分钟是 P2(呼叫值班),30 分钟是 P1(拉群加同步下游组)。分级数字来自当时的事故时长分布:80% 的下游故障在 10 分钟内自愈,30 分钟仍未恢复的基本要人工介入。告警不分级的后果是 1 分钟就呼叫,一周下来呼叫 20 次、真故障 3 起,值班疲劳让后面两次 P1 的响应慢了 8 分钟。

半开探测的实测时序

半开是状态机里最容易写错的一段,把实测时序摊开:

时刻 动作 观测
T0 进入打开,拒绝 20s 全部走 3.2 兜底
T0+30 进半开,放行 3 个探测 探测带 breaker_probe 标记
T0+30.4 3 个探测返回 3/3 成功则回关闭
T0+30.4 任一失败 回打开,冷却翻倍 60s
T0+90 第二轮半开 探测数保持 3,不递增

两个细节:探测与正常流量走同一个方法,不单独开「健康探测接口」,因为探测接口被绕过缓存直打数据库时,恢复判定会假阳性;探测数固定 3 不递增,递增方案(1、2、4…)在恢复期会把真实流量提前放进半个窗口,我们实测过,假恢复率反而更高。半开期间正常流量仍走兜底,只有探测在探路,这是与「半开放行全部流量」方案的本质区别。

还有一个常被问的点:半开探测请求本身超时了算成功还是失败?我们的口径是「返回了(哪怕 500)就算成功,超时才算失败」。理由:下游已经恢复到能应答,哪怕应答是业务错误,说明链路通了;只有链路不通(超时)才证明没恢复。这个口径写进了单测,防后续维护者改反。

踩坑:四个实战教训

第一,熔断粒度不能太粗。最初按「整个服务」熔断,支付服务里一个慢接口拖挂全服务 6 个接口。改成「每方法一个熔断器」后隔离生效,代价是熔断器数量从 17 个涨到 52 个,但可观测性反而更好。第二,超时也计入错误。下游不报错但超时的请求必须算进错误率,否则慢故障绕过了熔断——慢请求 5s 窗口占比 60% 时,按纯错误率算只有 8%,熔断根本不开。第三,半开探测要带标记。探测请求在 metadata 里标 breaker_probe=1,4.2 的链路里一眼能认出,探测打出的日志与正常流量分开统计,否则探测失败污染错误率曲线。第四,熔断与限流的方向区别:熔断保护的是调用方自己(下游坏了别跟死),限流保护的是自己(流量太大别被打穿),方向反了参数就全错了。

还有一类坑在运维侧:熔断器打开后忘查下游。熔断是止血不是治病,打开状态只进告警不进看板置顶时,会出现「熔断一直在开、没人去修下游」的 40 分钟空窗。我们的纪律是熔断打开告警必须带一句「请确认下游 X 健康度」,告警文案模板里写死这个动作,把止血与治病分开排班。

参数速查:五条调用链的实测值

默认值统一只是起点,五条核心调用链各自调过参,结果留在这:

调用链 窗口 错误率 最小样本 冷却 调参原因
订单 → 库存 5s 50% 20 30s 默认值,未调
订单 → 支付 5s 40% 30 45s 支付下游稳,收紧防误断
网关 → 订单 10s 60% 50 30s 入口容忍度高,放宽
订单 → 用户 5s 50% 20 30s 默认值,未调
对账 → 订单 60s 80% 10 10s 低频批处理,窗口拉长
调参纪律 内容
只改不删 每条链的参数变更留 diff 记录
回放先行 参数变更必须过 5.1 的注入回放
单变量 一次只动一个参数,防归因失败

调参最容易翻车的场景是把冷却调太短:订单→支付 曾经把冷却从 45s 调到 15s 想「快点恢复」,结果支付侧一次 40s 的抖动里被反复半开-失败,探测请求把支付又打抖了一次,抖动周期自我延长。冷却的下限由下游恢复时间定,不由焦虑定。

小结

熔断器三句话:5s 窗口、50% 加 20 样本开断、30s 半开 3 探测且失败翻倍冷却。粒度到方法级,超时计入错误,探测带标记,参数按调用链微调并全程回放。熔断器不修故障,只负责让故障停在它这一层——修故障的是下游,降级(3.2)负责在熔断期间把用户体验接住。下一篇 3.2 讲打开熔断后流量落地的地方,熔断与降级是一对,缺一不可。

延伸阅读