async def 中调用 requests.get() 会导致事件循环阻塞,qps 下降、延迟毛刺、cpu 使用率低;应改用 aiohttp 或 httpx.asyncclient 并 await,同步 sdk 需用 asyncio.to_thread 包装。

它根本不是“并发下阻塞”,而是你混进了同步操作,事件循环被卡死了。
async def 里调用了 requests.get() 会怎样
表面上函数标了 async,但内部用 requests.get() 这种同步阻塞调用,整个协程就停在那儿不动了——事件循环无法切走,其他所有待处理的请求全得排队等它。这不是并发低,是单个协程把轮子焊死了。
- 现象:QPS 上不去、延迟毛刺大、
uvicorn日志里出现大量超时,但 CPU 使用率很低 - 检查方法:在可疑函数里加
print("start"); time.sleep(0.1); print("end"),如果“start”和“end”之间隔了几秒,基本就是同步 IO 卡住了 - 替换方案:一律改用
aiohttp.ClientSession或httpx.AsyncClient,确保每个 HTTP 调用都带await - 特别注意:第三方 SDK(比如某些云厂商的 Python SDK)默认仍是同步的,得查文档确认有没有
async版本,没有就用asyncio.to_thread()包一层
长时间计算没丢到线程池
Python 的 async/await 不解决 CPU 密集型任务。你在 async def 里写了个 for 循环算斐波那契或解析大 JSON,事件循环照样被霸占。
- 判断依据:
top显示 Python 进程 CPU 持续 90%+,但并发请求数不高 - 正确做法:用
await asyncio.to_thread()把计算逻辑扔进线程池,例如await asyncio.to_thread(json.loads, raw_data) - 别用
loop.run_in_executor手动管理 executor,to_thread更简洁、错误传播更可靠 - 极端情况(如图像处理、科学计算)考虑用
ProcessPoolExecutor,但要注意进程间通信开销
未 await 的 close() 或未释放的资源
你以为写了 conn.close() 就完事了?错。WebSocket、数据库连接、HTTP client session 都要 await conn.close()。漏掉 await,连接对象还在内存里强引用着,FD 不释放,系统级句柄耗尽后,新连接连 accept 都失败,看起来就像“服务卡住”。
- 典型场景:
except WebSocketDisconnect:块里只写conn.close(),没加await;或者用async with但提前return跳出了作用域 - 验证方式:运行
lsof -p $(pgrep -f uvicorn) | wc -l,对比ulimit -n,接近阈值就说明是泄漏,不是负载高 - 硬性规范:所有异步资源必须走
async with,哪怕只是临时用一下;广播循环中每个send都包try/except WebSocketDisconnect,失败立刻active_connections.discard(conn) - 心跳必须由服务端主动发:靠客户端 ping 不可靠,得自己起一个
while True: await websocket.ping(); await asyncio.sleep(20)的 task
最常被忽略的是:asyncio 的清理动作本身也是协程,不 await 就等于没执行。哪怕你写了十行 close(),只要其中任意一个没 await,FD 就卡在 OS 层不还回去——这和并发量无关,纯属代码漏写。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











