应显式限制重试次数并组合使用停止条件,如stop_after_attempt(3) & stop_after_delay(10),避免因无上限重试或固定等待导致请求风暴、资源耗尽或长时间卡顿。

重试时没设最大次数,requests.exceptions.ConnectionError 会拖垮整个服务
不加限制的重试在下游宕机时会指数级放大请求量,尤其用 urllib3 或 requests 默认重试策略时,默认可能重试 3 次且带退避,但若配合业务层循环重试,实际调用次数可能远超预期。
实操建议:
- 显式禁用
requests内置重试:session.mount("https://", requests.adapters.HTTPAdapter(max_retries=0)) - 统一用
tenacity替代手写 while 循环,它能精确控制重试边界 - 对同一目标 URL 的并发重试必须加共享限流(如
asyncio.Semaphore(2)),否则 100 个协程同时重试一个挂掉的 API,等于发起 100×N 次无效请求
tenacity 中 stop_after_attempt 和 stop_after_delay 必须同时配用
只设 stop_after_attempt(3) 但没设总耗时上限,当每次重试间隔被设为 wait_exponential(multiplier=1, min=1, max=60) 时,第三次重试可能在 60 秒后才触发,导致单次调用卡住一分多钟,线程/协程资源被长期占用。
实操建议:
- 始终组合使用:
@retry(stop=(stop_after_attempt(3) & stop_after_delay(10))) - 避免用
wait_fixed(5)这类固定等待——下游恢复前所有重试都在“齐步走”,容易形成请求尖峰 - 对幂等性不确定的接口,重试前检查是否已产生副作用(比如查
order_id是否已存在),否则重复支付、重复发消息就不是雪崩,是资损
异步重试中忘记取消 pending task,asyncio.TimeoutError 不会自动清理资源
用 asyncio.wait_for(task, timeout=5) 包裹一个带重试的协程时,超时只抛异常,原 task 仍在后台运行。若该 task 内部又发起 HTTP 请求,这些请求不会被 cancel,连接池持续被占满,最终触发 asyncio.LimitOverrunError 或连接耗尽。
实操建议:
- 必须用
asyncio.create_task()启动重试任务,并在超时后显式调用task.cancel() - HTTP 客户端要用支持 cancellation 的库,如
aiohttp(ClientSession默认响应 cancel)或httpx.AsyncClient(需传timeout=...并捕获httpx.TimeoutException) - 重试函数内部避免用
time.sleep(),改用await asyncio.sleep(),否则会阻塞整个 event loop
重试 + 降级 + 熔断没分层,circuits.CircuitBreaker 被绕过
常见错误是把熔断器放在重试装饰器外层,结果重试逻辑在熔断器关闭状态下反复冲击下游;或者把熔断器嵌在重试内层,导致每次重试都单独触发熔断判断,失去统计意义。
实操建议:
- 熔断器必须包裹「完整的一次尝试」:包括首次请求 + 所有重试子过程,即
@circuit(failure_threshold=5, recovery_timeout=60)要包住整个@retry(...)函数 - 降级逻辑不能只返回 mock 数据,要区分场景:读接口可返回缓存旧值,写接口必须返回失败并记录告警,不可静默吞掉错误
- 监控指标必须分离:重试率(
retry_count / total_requests)、熔断触发次数、降级命中数——三者趋势背离时,说明某层策略失效
真正难的不是写对重试代码,而是确认每个重试动作背后有没有对应的可观测性埋点,以及下游故障恢复后,你的重试间隔和并发数会不会瞬间把刚起来的服务再次压垮。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











