上游返回 429 之后,客户端统一用 1 秒、2 秒、4 秒的固定退避,结果每次重试都在同一秒涌上来,上游的 rps 曲线成了整齐的方波,第三次重试时 429 反而更多,错误率从 3% 涨到 18%。

一开始怀疑是「重试落在同一个连接池上,瞬时并发把连接数也打满」,查了一圈,方向不对。

排查方向其实有限,先把可能性排一排。所有客户端从同一个起点开始算退避,重试时间完全同步。也可能是退避只按次数算,没有把响应里的 Retry-After 一起考虑。

少数情况下是失败请求没有标记来源,重试流量和正常流量混在一起统计。

上来先在退避里加一层随机抖动再发重试

python -c "import random;print([round((2**i)*random.uniform(0.5,1.5),2) for i in range(4)])"

优先读响应头里的 Retry-After 并作为下限

curl -s -D - -o /dev/null http://127.0.0.1:8000/v1/chat/completions | grep -i retry-after

给重试请求单独打标签方便看曲线

python -c "import uuid;print('X-Retry-Id:',uuid.uuid4())"

压测对比加抖动前后同一时间窗的 rps

python soak.py --rps 30 --fail-rate 0.2 --backoff jitter --duration 180

恢复的判定其实很直接。上游 rps 曲线变得平滑不再出现方波,第三次重试的 429 比例降到 5% 以下,重试请求占比与初始失败率相符。

固定退避适合单客户端,多客户端同时失败时必须有抖动,否则重试本身就成了新的攻击源。这类问题最好一次改到位,反复小改反而容易把原因搅混。

作者 9823189