重试风暴的放大效应有个简单公式。每跳重试次数为 R、调用链深度为 d,故障节点承受的请求量是 R 的 d 次方乘以正常量。加个 10% 的重试预算之后,公式变成 1.1 的 d 次方,深度 3 之后就趋于平坦。Uber 在服务网格里落地了一套错误归属机制,一次真实故障中拦下约 950 万次无效请求,重试风暴半径从平均 20 降到 2。
放大效应是怎么发生的
错误发生之后,调用链上的每一层都会看到失败。问题在于大家分不清这个错误是自己产生的,还是下游传上来的。既然分不清,就都按统一策略重试。重试本身没问题,问题在于重试的时机没有上下文感知,能控制次数,控制不了哪些请求值得重试。
对已经降级的下游继续重试,负载被推高,故障被加速,上游依赖也被卷进来。故障范围随深度指数扩大,这就是所谓风暴。
公式给出的两个杠杆
第一个杠杆是重试预算。把每跳的重试比例限制为 B,承载量变成 1 加 B 的 d 次方。B 取 10% 时,各跳为 1 倍、1.1 倍、1.21 倍、1.33 倍。深度 3 之后曲线就平了,深度再大也不会指数爆炸。
可用性对照更能说明预算的价值。下游可用性 99.9% 时,加预算后综合可用性到 99.9999%。可用性 90% 时到 99%。可用性跌到 70%,也只能拉到 77%。预算救不回一个已经烂掉的服务,只能防止错误被放大。
第二个杠杆是错误归属。回到错误产生的位置,让错误的所有者那一跳决定后果,重试才能发生在正确的层级。
错误归属怎么实现
判定逻辑要把症状和病因分开。如果错误由出站调用失败引发,返回的就是症状,病因在下游。如果自身还没有出站失败就已经报错,这个错误归自己。
三个组件配合。服务依赖分析把入站失败和出站失败关联起来,形成故障模式记忆。重试中间件在请求路径上决定是否重试,并标记重试条件是否满足。决策矩阵按四个维度判定,被调方是否出错、被调方是否声明错误、调用方是否应该重试、调用方是否上报传播错误。
有一条容易忽略的规则。被调方确实出错但没有声明错误时,调用方不应该重试。如果缺少归属声明,调用方虽然会重试,但要按传播错误上报,让上层看到真实的错误位置。
实测数据
2025 年 11 月的一次故障里,核心实体服务底层基础设施异常,该服务在调用链五层以下。如果只做简单的重试预算、没有错误归属,对已降级服务的流量会增长 46% 到 135%。
实际运行下来,机制阻止直接调用方最多多打 20 万次请求,在整个服务网格里拦下约 950 万次无效请求。重试风暴的最大半径从 25 降到 3,平均从 20 降到 2。
机制本身有误判。100 次请求、某节点出现 10 次内部错误时,有约 1.9 次被错误地判定为未归属。最坏情况下,约 2% 的调用方失败会被巧合性地归错。合理的取舍是接受这个误差,换来整体放大效应的收敛。
落地时的顺序
- 先加重试预算,把每跳比例压到 10% 上下,这一步不需要改业务代码,收益立竿见影。
- 再实现错误归属,从调用链最深处、最容易出故障的服务开始。
- 保留至少一次重试保证,用标志位确保限制重试不会误伤需要重试的场景,前提是调用链上至少有一个服务配置了重试。
- 把机制做成共享基础设施,各服务继承防护,不要各自实现。
常见问题
- 重试预算设多少。10% 是常见起点,按业务容忍度调整,重点是所有跳统一认知。
- 与熔断是什么关系。熔断保护下游,重试预算抑制放大器,两者互补。
- 中小团队值得做吗。调用链超过三层就值得,先做预算这一步。
- 错误归属的误判怎么处理。控制误判率,接受个别归错,别为追求精确放弃整体防护。