3.2 熔断降级:后端挂了怎么办

场景:后端慢了一秒,网关跟着一起死

限流治的是外部流量,熔断治的是内部故障。我们踩的坑很典型:库存服务的数据库在晚高峰慢了一秒,网关的 PHP-FPM worker 全部阻塞在等后端响应上,队列排满,新进来包括正常接口的请求也全超时——一个后端的局部故障,拖垮了整个网关。这在分布式系统里叫级联失败,是接入层最危险的死法。

事故复盘定下熔断的目标:后端故障时网关必须活下来,且能给客户端有意义的降级响应,而不是一片 502。熔断器要做的就是实时统计后端健康度,失败到阈值就切断,等后端恢复了再试探性放行。

方案:三态熔断加三级降级

熔断器经典三态我们照搬:Closed(正常放行)、Open(熔断,请求直接失败)、Half-Open(半开试探)。关键是阈值的制定,我们按错误率与慢调用双维度判定,任一达标即熔断,条件比单一维度更敏感:

状态 判定条件 行为
Closed 错误率低于阈值 正常转发
Open 错误率超阈值持续 30 秒 直接返回降级响应
Half-Open 熔断满 30 秒 放行 1 个试探请求
回 Closed 试探成功 恢复且计数清零

降级响应是熔断的另一半,熔断只是切开故障,降级决定切开后客户看到什么。我们对接口分级设计降级出口:

接口类型 降级方式 示例
查询类 返回缓存旧数据 库存查最近有效缓存
写入类 返回排队受理,异步重试 下单先落 MQ 延后处理
强一致类 直接 503 加明白说明 支付类不降级
非核心类 直接返回空壳结构 推荐列表返回兜底数据

配置:熔断状态机骨架

熔断器的实现是一个纯状态机,输入是调用结果,输出是放行与否。所有状态存 Redis 滑动窗口(与限流复用同一实例),多机共享一份熔断判断。为什么放 Redis 而不是进程内:多机对同一后端的健康判断必须一致,否则一半机器熔断一半机器继续打,后端压力根本降不下来,熔断效果减半。

<?php
// 熔断判定(骨架):滑窗错误率
public function shouldTrip(string $service): bool
{
    $key = "cb:{$service}";
    $window = $this->redis->zrange($key, time() - 30, time(), 'BYSCORE');
    if (count($window) < 20) {
        return false;             // 样本太少,不熔断
    }
    $fails = 0;
    foreach ($window as $r) {
        if ($r[0] === '0') $fails++;   // 0=失败
    }
    return ($fails / count($window)) >= 0.5;  // 错误率五成
}

超过百分之五十持续三十秒即熔断,样本不足二十不判定,避免小流量接口误熔断。慢调用记为失败是因为慢调用往往就是故障的前兆,提前切比事后切损失小。写入类接口的降级落地具体做法:熔断时网关把请求落本地队列(Redis List),json 里带原始请求与重试计数,后台 worker 在后端恢复后重放,失败的次数上限三次,超限投递告警人工处理,这套异步重试的核心原则是幂等:重放的请求若已处理不得重复执行,靠的是每个请求带的请求 ID(见第 4.2 节)做去重键,与签名校验的防重放机制异曲同工。

熔断状态的可观测也专门做了看板:每个服务的当前状态、持续时长、最近一次熔断原因都在值班大屏上,红黄绿三色一目了然。有一次大客户问为啥接口慢,值班同学打开看板解释库存服务处于 Half-Open 恢复期,对方秒懂并表示感谢。熔断系统不只是后台保命符,它是你向客户解释问题的官方语言。

熔断与限流的边界

团队里常有人混这两个概念。一句话区分:限流防外部冲击(流量太大),熔断防内部故障(后端坏了)。它们协作的方式是限流先兜住外部洪水,熔断再切断内部出血点,顺序上限流在前熔断在后(先入口再链路)。监控面板也按这个顺序排版:QPS 限流曲线在左侧,熔断状态在右侧,值班同学一眼就能分辨事故类型。

还有一个常见误区是把重试当作熔断的替代。客户端遇错重试如果不加退避,等于给故障中的后端二次打击,我们见过客户 SDK 用固定一秒间隔重试,后端刚要恢复又被压回去。网关的熔断在服务端切流量,客户端的退避在调用端减压,两端配合才能真正止血:网关的降级响应里带上 Retry-After,客户端按这个值退避,比各自猜要准。

还有一个易误解的点:熔断不是负载均衡的健康检查。负载均衡摘除节点是按健康度摘机器,熔断是按错误率切流量方向,一个治机器一个治链路,两者互补。我们的后端服务也搭了 CLB,机器级摘除交给 CLB,链路级熔断留在网关,各司其职,从没因为职责打架出过事。

踩坑:抖动、饥饿与雪崩

第一个坑是 Half-Open 抖动。试探请求万一正好落在恢复瞬间失败,熔断器会立即重新 Open,随后再试再失败,接口在恢复期反复抖动。修复是 Half-Open 期间允许连续两个失败才回 Open,给恢复期一点容忍。第二个坑是单点饥饿:熔断器按服务维度统计,某服务里一个慢接口把整个服务的错误率拖高,导致同服务的快接口被连坐熔断。修复是熔断粒度细化到服务加接口两段 key,代价是 Redis 键数量多一些,值得。

第三个坑最惊险:熔断引发的雪崩。某次后端恢复瞬间,Half-Open 放开瞬间积压的请求洪水般涌入,刚苏醒的后端又被压垮,熔断再开,循环了三轮。修复是恢复期必须渐进放量:Half-Open 每 3 秒翻倍放行比例,从百分之一开始爬,三十秒内回到正常——这个爬坡逻辑后来也用在灰度发布上,见 3.4 节。

第四个坑是熔断阈值本身需要随规模校准。早期用户少时设的错误率百分之五十对应每秒几十个请求的样本,流量涨了十倍后,熔断判定从统计上更敏感(样本大了小波动更显著也更容易达阈值),反而出现了几次误熔断。修复是把阈值从绝对错误率改为带置信区间的滑动统计,并给慢调用与错误分别设阈值窗口。参数不是定一次管三年,规模变了规则也要跟着校准,校准的依据是压测与历史数据,不是直觉。

再分享熔断测试的方式。熔断逻辑必须在压测环境真实演练过,否则它的第一次触发就是线上直播事故。我们每月做一次故障注入:在压测环境给某个后端服务注入 500 错误或两秒延迟,观察熔断器是否按预期 Open、Half-Open 是否按预期恢复、降级响应是否如约返回。演练报告归档,任何不符合预期的行为都是下个月的修复项。

顺带一个跨团队的协调经验。熔断切走流量时,后端同学其实也需要知道——他们要判断是自己的服务坏了还是网关误判。我们的熔断事件通过 CLS 推送一份给后端服务的值班群(带请求 ID 与错误样本),后端同学一秒进入状态,排查再不用跨组扯皮。工具的价值一半在自身功能,一半在它让多少人协同得更快。

再聊聊写入类接口降级的代价。排队受理意味着客户看到的成功其实要等下一刻才真正生效,这要求客户端配合查询结果接口确认最终状态。我们在文档里把这称为异步受理模式,并给出三行轮询示例。必须承认,这套降级并非无代价:它把同步问题异步化,调试与排错的复杂度上升了。因此熔断恢复后第一件事是检查重放队列有没有积压,并在大屏上标出积压深度。任何优雅的降级方案背后,都有运维同学盯着它的仪表盘。

还有一个与管理层沟通的经验值得单说。熔断降级的价值很难在周报里量化——它避免了多少损失?没有事故就没有证据。我们的做法是定期把故障注入演练的截图与数据指标整理成季度报告,包括恢复时间、影响范围、客户满意度三个维度的对比,让管理层直观看到这套系统在假想敌面前的表现。技术基础设施团队要学会讲自己的价值故事,否则永远是被砍预算的第一候选。报告这件事本身也倒逼我们把演练做好做实,形成正循环。

也提醒一句熔断器的上限。熔断救的是后端短暂故障的场面,它治不了彻底报废:后端机器全灭时熔断只是把 502 换成降级响应,撑不了太久。真正兜底的是多机房容灾(第 5.1 节)与后端服务自身的冗余。网关的熔断是链路的免疫系统,不是整条命。分清每个机制的边界,系统设计才不会错配期待。

小结

熔断保的是网关自己,降级保的是客户体验,两者是一体两面。三态状态机加双维度判定加三级降级出口,配上恢复期渐进放量,就是我们在生产环境兜完全套的方案。记住一条:熔断器自己必须简单到不可能坏,它是最外层的那道保险丝,保险丝若也脆弱,整栋楼就没有安全的地方了。

再给一套上线节奏的建议。熔断模块落地分四步走而不是一次全量:第一步只做统计不切断,先观察窗口数据准不准;第二步开告警,让大家习惯看熔断看板;第三步对非核心接口开切断,验证降级链路;第四步才全量启用。我们在第三步时发现降级返回的内容格式客户 SDK 解析不了,幸亏提前发现。分步上线让每个错误只在小范围暴露,这是治理类系统通用的落地方法论。

顺带一句提醒:熔断器的配置参数也要纳入变更评审。窗口时长、错误率阈值、慢调用阈值,任何一个随手一改都可能让整套保护形同虚设或频繁误断。参数即策略,策略即安全,别让熔断配置成为代码库里最自由的那个角落。

延伸阅读