一个支付服务开始给10%的请求返回503错误。不是宕机,可能只是容器重启,负载有点高——生产环境里天天发生的事。

但每个收到503的客户端都在重试。这是设计好的行为。

服务现在收到的流量比原来多了10%。失败率爬到20%。更多客户端重试流量再涨。失败率冲到40%,然后70%。几秒钟内,原本只是"有点咳嗽"的服务彻底失联。依赖它的系统全线超时,告警炸成一片。

没有组件损坏。没有代码bug。每个客户端都按设计运行。整个系统却死了。

重试风暴:分布式系统的经典死法

重试风暴:分布式系统的经典死法

这是工程师最憋屈的故障类型。你写了重试逻辑,加了指数退避,觉得自己很稳健。直到某天流量曲线变成垂直悬崖,你才意识到:重试在单体时代是止痛药,在微服务架构里可能是毒药。

问题藏在链式反应里。A服务调用B,B调用C。C慢了,B重试。A看到B慢了,也重试。原本1个请求变成3个、9个、27个。数学上这叫几何级数,运维夜里接到电话时叫它"那个又来了"。

Netflix在2011年的博客文章里写过类似案例:他们的API网关曾经因为下游延迟,把重试次数设成3次,结果流量放大到足以压垮整个集群。后来他们改了策略——不是减少重试次数,而是给重试加"熔断"和"舱壁"。

为什么工程师总是事后才懂

本地测试时重试永远是对的。开发环境就你一个用户,重试10次也看不出问题。生产环境有10万个并发客户端,每个都"正确"地重试3次,负载就变成了30万。

更隐蔽的是时机。指数退避(Exponential Backoff)能打散重试时间点,但如果所有客户端用同样的算法、同样的初始种子,它们会在"打散"后再次聚成脉冲。AWS的SDK曾经默认用随机化退避,就是为了避免这种同步效应。

Google的SRE手册里有个数字:重试导致的流量放大,在极端情况下能达到原始流量的100倍。这不是理论,是账单上的零。

现在检查你的代码

现在检查你的代码

打开你最近写的HTTP客户端,找到那行`.retry(3)`或者`@Retryable`。它有没有上限?有没有针对429和503的区别处理?有没有在连续失败时主动停止呼叫?

大多数工程师只实现了一半:让失败请求再试一次。但完整的重试策略需要回答另一半:什么时候该承认失败,把压力从系统里卸掉,而不是用"帮助"的名义把它勒死。

那个支付服务后来怎么恢复的?运维手动切断了部分客户端的连接,让服务喘了口气。没有自动化的优雅,只有人在凌晨两点按下的红色按钮。

你的系统里,有多少个"正确"的重试逻辑正在睡觉,等着被某个10%的错误率唤醒?