这个坑值得记一笔。FastAPI 0.111 的异步接口里用 queue.Queue 做本地缓冲,生产者 put 之后由消费者协程去取。压到 200 并发时接口整体卡住,连 /healthz 都超时,日志没有异常,CPU 占用只有 3%。把 handler 改成同步 def 就不卡了,worker 加到 8 个也没用。
刚开始盯着「uvicorn 单 worker 只有一个事件循环,一处阻塞就是全局停摆」,折腾了半个多小时,排掉了。
这类报错背后常见的原因有几处,逐个排除就行。queue.Queue.put 在队列满时阻塞线程,而它跑在事件循环线程上;消费者是协程,被阻塞的线程里根本轮不到它执行;队列没有设 maxsize 时不会阻塞,真正卡住的是别处的 join。
第一件事是确认阻塞发生在哪个调用上
python -c "import queue,time;q=queue.Queue(maxsize=1);q.put(1);t=time.time();q.put(2,timeout=1);print('blocked', time.time()-t)"
随后改用 asyncio.Queue 并统一成 await
python -c "import asyncio;q=asyncio.Queue(maxsize=100);print(type(q))"
再往下把阻塞调用挪进线程池执行
python -c "import asyncio;print(asyncio.to_thread)"
压测确认健康检查不再超时
curl -s -o /dev/null -w '%{time_total}' http://127.0.0.1:8000/healthz
改完先别撤,确认这几项。200 并发下健康检查响应时间稳定在 5ms 以内,队列积压时接口返回 429 而不是整体卡死。
异步框架里只能放非阻塞调用,queue.Queue、requests、time.sleep 任意一个都会拖住整个循环,CPU 很低而请求全挂就是这个特征。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。
