必须加指数退避和随机抖动,避免重试风暴;异步场景用 asyncio.sleep() 而非 time.sleep();优先选 tenacity 替代 retrying;重试需配熔断、结构化日志与成功率监控。

重试逻辑没加退避,请求直接打满下游
Python 里用 requests 或 aiohttp 自己写重试,最容易犯的错就是重试间隔为 0 —— 第一次失败立刻重试,三次重试全挤在 100ms 内发出去。下游服务本来卡在 99% CPU,你再叠个 3× 并发,它直接 503,然后你的重试又触发,形成正向反馈循环。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 必须用指数退避(exponential backoff),比如第一次等 0.1s、第二次 0.2s、第三次 0.4s,上限设成 2s 就够用
- 加随机抖动(jitter),避免所有客户端在同一时刻重试,
time.sleep(base * (2 ** attempt) + random.uniform(0, 0.1)) - 别用
time.sleep()硬等阻塞线程,异步场景下改用asyncio.sleep(),否则整个 event loop 被拖住
retrying 库默认不控制并发,多个请求同时触发重试风暴
retrying 这个老库(虽已停更)还在不少项目里跑着,它只管单个函数重试,完全不管调用上下文。如果上游是高并发服务(比如 FastAPI 每秒接 500 请求),每个请求都独立走一遍重试逻辑,那下游看到的就是 500 × 3 = 1500 QPS 的脉冲流量。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 换成
tenacity,它支持wait_exponential()+stop_after_attempt(3)组合,语义清晰且默认不共享状态 - 如果必须用
retrying,手动加全局限流器(比如threading.Semaphore(5)),但注意这会串行化重试,得权衡延迟和压垮风险 - 检查重试装饰器是否作用在了最外层视图函数上——应该只包核心 I/O 函数,比如
fetch_user_data(),而不是整个get_user()HTTP handler
异步重试中未 cancel 已超时的 pending 请求
用 asyncio.wait_for() 包一层重试逻辑,但没在 timeout 后主动 cancel 掉底层 task,结果是:请求已超时返回 504,但那个 await requests.get(...) 还在后台默默跑着,可能 2 秒后才真正失败,继续触发下一轮重试。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 每次重试前生成新
asyncio.Task,并在超时或成功后显式task.cancel() - 用
asyncio.create_task()+asyncio.shield()控制取消边界,避免取消传播到不该中断的子协程 - 日志里加 trace_id 和 attempt_id,方便确认“这个 504 对应的底层请求到底有没有被 cancel”
重试掩盖了真正的稳定性短板
重试不是容错,是兜底。如果某个接口平均失败率从 0.1% 突然涨到 5%,重试机制会让业务层感知不到——用户照样能用,但下游数据库连接池早被打爆了,监控里只看到慢查询和连接拒绝。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 重试必须配熔断(circuit breaker),比如用
tenacity的circuit_breaker,连续 5 次失败就开路 30 秒,强制降级或报错 - 所有重试行为必须打结构化日志:
retry_attempt=2, original_error="ConnectionResetError", upstream="payment-api" - 把重试成功率单独做成指标(比如
retry_success_rate{service="order"}),低于 95% 就告警——说明重试本身正在失效
重试配置不是写一次就完事的事。下游接口变更、网络拓扑调整、甚至 DNS 缓存时间变化,都会让原来安全的重试策略变成雪崩开关。上线后盯三天 metrics,比看十遍代码有用。










