上游返回 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% 以下,重试请求占比与初始失败率相符。
固定退避适合单客户端,多客户端同时失败时必须有抖动,否则重试本身就成了新的攻击源。这类问题最好一次改到位,反复小改反而容易把原因搅混。
