先看现象。用 tenacity 8.5.0 的 @retry(wait=wait_exponential(multiplier=1, max=20) + wait_random(0, 1)) 包 gpt-4o 调用,日志里第 3 次重试只等了 0.3 秒,429 还没恢复就又打过去,十分钟刷了 300 次。

真正麻烦的地方在于,几个原因的现象几乎一样。两个 wait 策略用加号组合是叠加计算,随机项会把指数结果往下拉。还有一种是multiplier 设成 0.5 且 exp_base 默认 2,前几次的间隔只有几百毫秒。

少数情况下是没有配 retry_if_exception_type,参数错误也会进重试。

不容易想到的是,stop_after_attempt 与 stop_after_delay 同时启用,先到哪个算哪个。

上来先把前五次等待秒数打印出来核对

python -c "from tenacity import wait_exponential,wait_random;w=wait_exponential(multiplier=1,max=20)+wait_random(0,1);print([round(w(i),2) for i in range(1,6)])"

接着改成带固定抖动的指数退避再对比

python -c "from tenacity import wait_exponential_jitter;w=wait_exponential_jitter(initial=2,max=30,jitter=3);print([round(w(i),2) for i in range(1,6)])"

只对超时和 429 重试,其余异常直接抛

python -c "from tenacity import retry,retry_if_exception_type;import httpx;print(retry_if_exception_type((httpx.TimeoutException,httpx.HTTPStatusError)))"

等前面都确认过,再打开 tenacity 的调试日志看真实间隔

python -c "import logging;logging.basicConfig(level=logging.DEBUG);print('retry log on')"

怎么算真的好了,看的是同一批现象。第 3 到第 5 次重试的间隔稳定落在 8 到 12 秒,一次 429 最多重试 4 次就成功,十分钟的请求量从 300 降到 12。

容易忽略的是,多个 wait 用加号不会取最大值,抖动叠在指数之上仍会被 exp_base 拉平,直接用 jitter 参数比手写随机项可控。线上能稳定复现的问题都不算难,难的是只在大流量下才出现的那种。

作者 9823189