urllib3.Retry 通过自定义 backoff_func 实现带 jitter 的指数退避,公式为 min(backoff_max, (2 retry_count) backoff_factor random.uniform(0.5, 1.5)),需设 backoff_factor=0 避免叠加,默认返回值即 sleep 秒数。

requests 自带重试机制不支持 jitter,必须手动封装
requests 的 urllib3.Retry 能做指数退避,但所有重试间隔是确定的(如 1s、2s、4s),没有随机抖动(jitter)。生产环境直接用它容易触发服务端限流或雪崩,必须自己加 jitter —— 也就是在每次计算出的基础等待时间上乘一个 [0.5, 1.5) 之间的随机因子。
用 urllib3.Retry + 自定义 backoff_func 实现 jitter
urllib3.Retry 允许传入 backoff_factor 和 backoff_max,但它默认的退避逻辑是线性的(实际是指数,但无 jitter)。真正可控的方式是传入自定义的 backoff_func 参数,该函数接收重试次数 retry_count,返回应等待的秒数。
- 基础公式:
base = min(backoff_max, (2 ** retry_count) * backoff_factor) - jitter 部分:乘以
random.uniform(0.5, 1.5),避免重试请求扎堆 - 注意:这个函数只在 urllib3 内部调用,不能抛异常,也不能依赖外部状态
import random import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retrydef jittered_backoff(retry_count): base = (2 * retry_count) 0.5 # backoff_factor=0.5 max_wait = 60.0 return min(max_wait, base * random.uniform(0.5, 1.5))
retry_strategy = Retry( total=5, status_forcelist=[429, 500, 502, 503, 504], backoff_factor=0, # 必须设为 0,否则会叠加默认逻辑 backoff_max=60, backoff_func=jittered_backoff, )
adapter = HTTPAdapter(max_retries=retry_strategy) session = requests.Session() session.mount("http://", adapter) session.mount("https://", adapter)
requests.Session 不会自动 sleep,需在 backoff_func 中控制阻塞
很多人误以为 backoff_func 返回值会被 urllib3 自动用于 time.sleep() —— 实际上不是。urllib3 仅用它来决定是否重试(比如超时前还剩多少时间),真正的等待逻辑在它内部实现。但关键点是:**只有当 backoff_func 返回值 ≤ 剩余重试时间时,urllib3 才会 sleep 对应时长**。所以你返回的值就是最终 sleep 秒数,不需要额外 time.sleep()。
- 如果你返回
3.7,urllib3 就会 sleep 约 3.7 秒(精度取决于系统调度) - 返回负数或 None 会导致立即重试(不推荐)
- 若用
Retry(total=3),retry_count取值为0, 1, 2(不是 1~3)
注意 time.monotonic() vs time.time() 与系统时钟漂移
urllib3 内部用 time.monotonic() 计算剩余等待时间,所以你的 backoff_func 返回值不会受系统时间回拨影响。这点不用额外处理,但如果你在自定义重试逻辑里手动 sleep,就得自己用 monotonic() 校验 —— 而用 backoff_func 方式就天然规避了这个问题。
真正容易被忽略的是:jitter 的随机源必须是线程安全的。如果 session 被多线程共用(比如在 FastAPI 的全局 client 里),random.uniform() 默认使用全局 random.Random 实例,在 CPython 中是线程安全的,但不保证跨平台;更稳妥的做法是每个重试策略绑定独立的 random.Random 实例,不过对绝大多数场景,直接用 random.uniform 已足够。










