如何避免重试风暴?超时、重试和退避怎么设计?
简化版
重试风暴是指下游变慢或失败后,上游大量请求同时重试,导致请求量被放大,下游更加过载,最终形成雪崩。避免重试风暴的关键不是简单禁止重试,而是给重试加边界:合理超时、有限次数、指数退避、随机抖动、只对可重试错误重试、配合熔断限流和幂等设计。
面试中可以强调:重试一定要有预算。一次用户请求最多重试几次、最多耗时多久、哪些错误允许重试、重试是否会造成重复写,都必须明确。没有边界的重试是放大故障的机器。
详细版
重试的初衷是处理瞬时故障,比如网络抖动、短暂超时、连接复用失败。但当下游已经过载时,重试会把原本的流量成倍放大。例如 1000 QPS 的请求每个都重试 2 次,下游看到的可能变成接近 3000 QPS。更糟的是,多层服务如果都独立重试,放大倍数会叠加。
设计重试时要先设置超时。没有超时,请求会长期占用线程和连接;超时过长,会导致用户等待和资源堆积;超时过短,又会误判慢请求并制造更多重试。重试次数通常要少,常见是 1 到 2 次,并使用指数退避和随机抖动,避免所有请求在同一时间再次打到下游。
另外,不是所有请求都适合重试。读请求、幂等写请求、明确的瞬时错误可以谨慎重试;非幂等写请求、业务校验失败、库存不足、余额不足这类错误不应该盲目重试。重试还要配合熔断、限流、隔离和监控,防止故障扩散。
完整版教学
一、重试为什么会变成风暴
重试本身是一个有用机制。网络调用不可能百分百稳定,偶发超时、连接断开、瞬时抖动都可能通过一次重试恢复。但问题在于:当下游已经慢或过载时,重试会增加额外请求,让下游更慢。
假设上游原本有 1000 QPS,请求下游出现超时。如果每个请求最多重试 2 次,那么理论上最多会产生 3000 QPS 的调用压力。下游本来已经处理不过来,现在还要处理额外的重试流量,错误率进一步升高,上游又继续重试,这就是重试风暴。
如果调用链上有多层重试,放大效应更明显:客户端重试 2 次,网关重试 1 次,服务 A 重试 2 次,服务 B 再重试 1 次,最终落到最底层依赖的请求数可能远超入口请求数。
二、第一条原则是重试必须有预算
重试预算包括四个维度:
- 次数预算:最多重试几次。
- 时间预算:整个调用最多耗时多久。
- 流量预算:重试流量占总流量的比例不能无限增长。
- 链路预算:一条调用链中哪些层允许重试,哪些层不允许重试。
比较稳妥的做法是:靠近用户的一层控制总体超时和体验,内部服务只在非常明确的瞬时错误上做少量重试。不要每一层都默认重试,因为多层重试会让放大倍数失控。
三、超时要先于重试设计
没有合理超时,重试策略通常会失败。超时太长,请求会长时间占用线程、连接和内存;超时太短,正常慢请求会被误判失败,引发无意义重试。
超时设计要考虑:
- 用户可接受等待时间。例如接口整体 SLA 是 300ms,就不能给某个下游 1 秒超时。
- 下游历史延迟分布。可以参考 P95、P99 延迟,而不是只看平均值。
- 调用链深度。上游整体超时要覆盖多个下游调用,不能每个下游都拿完整预算。
- 失败后的恢复动作。如果还有重试,单次超时必须给重试留时间。
例如一个接口总预算 800ms,调用下游最多重试 1 次,就不能把单次超时设成 800ms。可以按 300ms 首次调用、短暂退避、300ms 重试、剩余时间做降级处理。
四、退避和抖动是避免同步冲击的关键
如果所有请求都在超时后立即重试,重试流量会集中在同一时间点打到下游。指数退避可以让重试间隔逐步变长,例如 50ms、100ms、200ms。随机抖动是在退避时间上加入随机变化,避免大量请求同时重试。
常见策略:
第 1 次失败:等待 50ms ~ 100ms
第 2 次失败:等待 100ms ~ 200ms
第 3 次失败:不再重试,走失败或降级
随机抖动非常重要。没有抖动时,许多请求会按照相同时间间隔重新发起,形成周期性尖峰。加入抖动后,重试请求分散到不同时间点,下游更容易恢复。
五、只对可重试错误重试
重试要区分错误类型。可以考虑重试的情况包括:连接断开、临时网络错误、部分 5xx、限流后的短暂等待、明确可恢复的超时。通常不应该重试的情况包括:参数错误、权限失败、余额不足、库存不足、业务规则校验失败、非幂等写入结果未知。
非幂等写请求尤其危险。比如创建订单请求已经在下游成功,但响应在返回途中超时,上游如果再次提交,可能创建重复订单。要支持这类重试,必须先有幂等键、唯一约束或业务去重机制。
因此面试中要把“重试”和“幂等”放在一起讲。没有幂等保障的写请求,不应该随意自动重试。
六、重试要和熔断、限流、隔离配合
重试策略不是孤立存在的。为了防止风暴,需要配合其他稳定性机制:
- 熔断:当下游错误率或超时率过高,短时间停止调用,避免继续重试放大压力。
- 限流:限制重试请求的总量,避免重试流量挤占正常流量。
- 并发隔离:为下游调用设置独立线程池、连接池或信号量,防止拖垮主业务线程。
- 降级:当重试仍失败时,返回兜底数据、默认值或明确失败。
- 监控:单独统计重试次数、重试成功率、重试耗时和重试放大倍数。
一个很实用的指标是重试比例。如果重试请求占比突然升高,说明系统可能已经在故障边缘,即使最终成功率还没有明显下降,也要关注。
七、面试回答可以给出一套落地策略
可以这样回答:
- 设置全链路超时预算,不能每层无限等待。
- 限制重试次数,一般少量重试,避免多层叠加。
- 使用指数退避和随机抖动,避免同步冲击。
- 只对瞬时、可恢复、幂等的错误重试。
- 配合熔断、限流、隔离和降级。
- 建立可观测性,监控重试次数、失败率、耗时和放大倍数。
如果面试官问“重试几次合适”,不要给绝对值。应该说取决于接口 SLA、下游延迟、业务幂等性和错误类型。在线同步接口通常重试次数要少,离线任务和异步消费可以在更长周期内重试,但也要有死信队列和告警。
八、常见误区与追问
这道题要紧扣「重试风暴」本身回答,不能把它混成泛泛的流量控制套话。面试官通常不是只听定义,而是看你能不能把适用场景、关键流程、失败边界和工程取舍串起来,尤其要说明限流位置、算法窗口、阈值来源、拒绝策略、退避降级和观测指标。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 重试风暴是故障时大量客户端同步重试,反而把下游压得更垮,必须用退避、抖动、限流和熔断控制 | 不要停在名词解释 |
| 流程机制 | 请求失败 -> 判断错误类型 -> 按指数退避等待 -> 加入随机抖动 -> 超过次数停止 -> 熔断或降级保护 | 要说清触发点、状态变化、确认点和失败兜底 |
| 工程取舍 | 1000 个客户端每秒失败后立即重试 3 次,下游瞬时压力可能从 1000 QPS 放大到 4000 QPS | 限流保护系统稳定性,但会牺牲一部分请求成功率或响应实时性 |
重试风暴 面试拆解:
1. 请求失败
2. 判断错误类型
3. 按指数退避等待
4. 加入随机抖动
5. 超过次数停止
6. 熔断或降级保护
记忆钩子:先给结论,再拆流程,再讲数字例子和失败边界;回答「重试风暴」时要围绕题目问法收束,不要把相邻概念堆成一段没有重点的名词清单。
- 误区:限流就是简单拒绝请求。 合格答案要区分放行、排队、快速失败、降级、退避和熔断,而不是只说“挡住流量”。
- 误区:全局平均流量低就没有风险。 热点 key、单实例、单下游资源仍可能先被打满,所以要看局部水位。
- 误区:限流阈值可以拍脑袋配置。 阈值要来自压测容量、依赖水位、错误率、P99 延迟和业务优先级。
- 追问:限流应该放在哪里? 入口网关、服务内部、客户端和下游资源侧可以分层配置,分别保护不同边界。
- 追问:触发限流后怎么处理? 核心写请求可排队,非核心读请求可降级,用户侧要给明确失败或稍后重试。
- 追问:如何验证流控策略有效? 看 QPS、并发、延迟、拒绝率、队列长度、下游错误率和恢复时间。
九、加强记忆
重试可以记成“再试一次”,重试风暴就是“所有人一起再试很多次”。下游已经喘不过气时,无边界重试相当于继续加压。
记住四个关键词:超时预算、有限次数、退避抖动、幂等边界。能把这四点讲清楚,重试风暴这类题基本就能答得比较完整。