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 讲打开熔断后流量落地的地方,熔断与降级是一对,缺一不可。
延伸阅读
- 熔断算法的更多变体与对照,见 go-micro-cps-guide。
- 熔断事件与限流事件的日志检索,见 rust-logpipe-guide。
- 状态机参数与压测拐点的对应,见 k8s-pipeline-guide。
