记录一下那天晚上的排查过程。uvicorn 0.30 配了 limit_concurrency=100,压测到 120 并发时多出来的请求直接返回 503,响应体是空的,没有 Retry-After,客户端 SDK 把 503 当可重试,重试把流量又放大了一轮。

刚开始盯着「worker 数只有 1,并发上限按进程算」,折腾了半个多小时,排掉了。

排查方向其实有限,先把可能性排一排。limit_concurrency 超限时直接 503 且不带 Retry-After。还有一种是前置网关把 503 当可重试,放大了流量。

还有一种情况,请求在队列里等待的时间没计入超时。

复现并看响应头

curl -s -o /dev/null -D - -w '%{http_code}\n' http://localhost:8000/health

然后用并发工具压到上限以上

python -c "import concurrent.futures,urllib.request;f=lambda _:urllib.request.urlopen('http://localhost:8000/health').status;print(set(concurrent.futures.ThreadPoolExecutor(120).map(f,range(120))))"

再往下在网关层对 503 加 Retry-After

python -c "print({'status':503,'headers':{'Retry-After':'2'}})"

调大 worker 数与 limit 后复测

uvicorn app:app --workers 4 --limit-concurrency 400

改完先别撤,确认这几项。超过上限的请求收到带 Retry-After 的 503,客户端按 2 秒退避,重试总量可控,正常请求的 P99 不再被拖高。

limit_concurrency 是保护进程的硬闸,返回体很简陋;要在网关补 Retry-After 并让客户端区分可重试与不可重试,否则退避会变成放大器。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。

作者 9823189