重试与指数退避是什么
对可恢复的错误做重试是必要的,但重试方式决定了它是保护还是伤害:等待时间按指数递增并叠加随机抖动,避免所有实例在同一时刻一起重试。重试只该覆盖可恢复的错误类别,参数错误与鉴权失败重试多少次都没用,反而会把一次小问题放大成持续负载。写操作还要考虑幂等性。
基本信息
| 适用错误 | 限流、服务端临时故障、网络抖动 |
|---|---|
| 不适用错误 | 参数错误、鉴权失败、模型不存在 |
| 退避方式 | 等待时间按指数递增,并叠加随机抖动 |
| 抖动作用 | 打散重试时刻,避免同时冲击上游 |
| 最大次数 | 按业务容忍度设置,不要无限重试 |
| 写操作 | 需要幂等设计,避免重试造成重复下单等后果 |
重试策略速查
| 错误类型 | 是否重试 | 建议 |
|---|---|---|
| 限流类错误 | 是 | 优先使用服务端给出的等待时间 |
| 服务端临时故障 | 是 | 指数退避并限制最大次数 |
| 网络超时 | 视情况 | 确认请求是否可能已生效再决定 |
| 参数错误 | 否 | 直接失败并记录,修代码或提示词 |
| 鉴权与权限 | 否 | 检查密钥与权限配置 |
| 模型或路径不存在 | 否 | 核对模型名与接口地址 |
典型使用场景
- 批量任务:长时间运行中偶发失败自动恢复
- 在线服务:上游抖动时自动重试,用户无感
- 工具调用:外部接口超时后的有限重试
- 多供应商切换:重试用尽后切到备用线路
- 流式请求:断流时的重连策略,注意避免内容重复
带抖动的退避实现
带抖动的退避实现
import time, random
def with_backoff(fn, tries=4, base=1.0):
for i in range(tries):
try:
return fn()
except RetryableError: # 只重试可恢复错误
if i == tries - 1:
raise
delay = base * (2 ** i) # 1 秒、2 秒、4 秒递增
time.sleep(delay + random.random()) # 叠加抖动,打散重试时刻
# 不可恢复的错误应直接抛出,交由上游记录与告警落地注意事项
| 事项 | 做法 | 原因 |
|---|---|---|
| 幂等性 | 给写操作加唯一标识 | 重试可能造成重复执行 |
| 日志 | 记录每次重试的次数与原因 | 便于识别系统性故障 |
| 并发控制 | 重试请求同样受限速约束 | 否则重试会加剧限流 |
| 上限 | 设置最大次数与总耗时 | 避免请求长时间挂起 |
| 降级 | 重试用尽后走备用方案 | 保证核心功能可用 |
重试次数要结合总耗时评估:每次等待叠加起来可能超过上游超时时间,最终表现为请求全都超时但日志里只有重试记录。
对写操作不加幂等就重试是本清单里最危险的一条:重复提交可能造成多次下单、多发消息这类真实业务损失。