重试、退避、抖动与重试风暴
重试适合处理短暂且可能在下一次尝试中消失的故障。下游已经过载或操作无法安全重复时,重试会增加请求量、延长尾延迟,并把局部故障扩大成重试风暴。
1. 先区分可重试与永久错误
可重试情况可能包括连接被重置、临时无健康实例、明确的限流响应和部分服务端错误。参数错误、权限不足、资源不存在和业务规则拒绝通常不会因立即再试而改变。
不能只按异常类名判断。一次读超时可能是网络抖动,也可能是下游已经饱和;服务端应通过稳定错误码和 Retry-After 等信息表达是否建议重试。
2. 写操作需要先解决重复执行
客户端超时只说明没有收到结果,服务端可能已经提交。GET 等安全读取通常可重试;创建订单、扣款和发消息需要幂等键、条件状态转换或结果查询。
HTTP 方法的幂等语义是协议约定,仍要由服务实现保持。POST 也可以通过业务幂等键成为可安全重试操作。
3. 指数退避给下游恢复时间
固定间隔重试会让大量客户端保持同步。指数退避让后续尝试逐渐拉开:
delay = min(cap, base × 2^attempt)
设置上限避免等待无限增长。退避仍需服从总 deadline;如果睡眠后没有足够时间完成下一次尝试,就应直接结束。
4. 抖动打散同步客户端
服务短暂恢复后,所有客户端若在相同的 100 ms、200 ms、400 ms 重试,会形成新的流量尖峰。Full jitter 可以在 [0, 当前退避上限] 中随机选择等待时间,把请求分散开。
随机数不是装饰。多个实例使用相同种子、固定定时器或按整秒调度,仍可能出现同步。
5. 重试次数按总尝试数计算
“重试 3 次”容易被理解为总共 4 次调用。接口和指标应明确 attempt 上限,并记录每次原因与耗时。
多层链路中只选择一层承担主要业务重试。假设客户端、网关和两个服务都各尝试 3 次,最深下游理论请求数可达到 3⁴。即使不全部展开,也会在过载时显著放大。
6. 重试预算限制额外流量
除了单请求次数,还要限制系统级重试比例或并发数。例如正常成功请求补充 retry token,重试消耗 token;错误持续升高、预算耗尽后直接失败。
重试预算让少量瞬时错误获得恢复机会,同时避免故障期间重试量与原始流量同规模增长。应按下游、路由和优先级分别统计。
7. Hedged request 只适合受控读取
尾延迟敏感的幂等读取可以在首个请求超过某个分位延迟后,向另一个副本发送一次并行备份请求,先成功者返回并取消另一份。
它会主动增加负载,只适合低比例长尾、多个独立副本且有额外容量的场景。下游整体过载时,hedging 会进一步恶化问题;写操作也不能直接套用。
8. 用故障注入验证重试策略
测试连接 reset、固定延迟、限流、部分实例故障和全体过载,观察总请求放大倍数、成功率、P99、重试预算和取消后的在途请求。
若关闭重试后下游反而恢复,说明策略正在放大故障。监控应区分原始请求和每次 attempt,不能把重试后的最终成功掩盖为“没有异常”。
9. 常见问题
9.1 所有 5xx 都应该重试吗
不应该。部分 5xx 可能来自确定性代码错误或下游持续过载。需要按接口语义、错误类型、剩余预算和重试预算共同决定,并限制到其他健康实例也可能改善的场景。
9.2 重试切换实例就一定安全吗
不一定。原实例可能已经完成写入,只是响应丢失;新实例再次执行仍会重复。实例切换只能绕过局部故障,不能替代业务幂等。
10. 面试题
10.1 为什么重试会形成流量放大,如何控制
出现公司:字节跳动
考察重点
- 瞬时与永久错误、幂等和结果未知。
- 指数退避、full jitter 与端到端 deadline。
- 多层重试乘法和系统级重试预算。
相关内容:第 1 节“先区分可重试与永久错误”至第 8 节“用故障注入验证重试策略”。
参考回答
重试只用于可能短暂恢复的错误,并且操作要幂等或带幂等键。每次尝试都消耗下游容量;如果客户端、网关和服务各自重试,最深下游流量会乘法放大。下游已经过载时,这会延迟恢复并占满更多线程和连接。
控制方法是明确一个主要重试层,限制总 attempts,让全部尝试服从端到端 deadline,并使用指数退避和随机抖动。还要设置按下游计算的重试预算或并发上限,预算耗尽就快速失败。指标分别统计原始请求、attempt、重试原因和放大倍数,并通过过载故障注入验证。