requests 如何实现带 jitter 的指数退避重试(不依赖 backoff 库)

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_factorbackoff_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 Retry

def 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 已足够。