<p style="text-align:center"><img src="https://platform-outputs.agnes-ai.space/images/t2i/task_egT5kpvXJKNPPJvFfr7slkB4hCRbsl97/output_056ce58b9ed045b0a7c4b4697b45b04a.png" alt="封面图" style="max-width:100%;height:auto" /></p>
凌晨 3 点接口超时,排查发现 asyncio 协程里混用了同步 requests 库
凌晨 3 点,报警群炸了。接口 P99 延迟飙到 2 秒,但 CPU 利用率才 20%。第一反应是数据库慢,查了慢查询日志却干干净净。最后定位到 Python 服务里,几个老代码片段的 asyncio 协程里居然直接调了 requests。这种"伪异步"直接阻塞了事件循环,导致所有协程排队等待。单线程模型下,一个阻塞,全员挂起,这就是 CPU 低但响应慢的根本原因。
故障现场还原
出问题的代码长这样,看着是 async 函数,里面却是同步 IO:
import asyncio
import requests
async def fetch_data(url):
# 错误示范:在协程中使用同步阻塞库
resp = requests.get(url, timeout=5)
return resp.text
async def main():
tasks = [fetch_data(f'http://api.example.com/{i}') for i in range(10)]
await asyncio.gather(*tasks)
asyncio.run(main())
**效果说明**:这段代码执行时,`requests.get` 会阻塞整个线程。虽然写了 async,但 10 个请求是串行执行的,总耗时接近 50 秒,完全失去并发优势,事件循环被卡死。asyncio 核心是单线程事件循环,任何阻塞操作都会阻止后续任务调度。
正确异步姿势
必须使用异步 HTTP 库,如 aiohttp,让出控制权给事件循环:
import asyncio
import aiohttp
async def fetch_data(session, url):
# 正确示范:使用异步 IO 库
async with session.get(url) as resp:
return await resp.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch_data(session, f'http://api.example.com/{i}') for i in range(10)]
await asyncio.gather(*tasks)
asyncio.run(main())
**效果说明**:`aiohttp` 在等待 IO 时会挂起协程,切换执行其他任务。10 个请求并发发出,总耗时接近单个请求耗时(约 0.5 秒),吞吐量提升百倍。注意 Session 复用,避免频繁 TCP 握手开销,这是生产环境必备优化。
如何监控阻塞
线上不能全靠肉眼,要开启事件循环慢回调日志,自动捕捉阻塞点:
import asyncio
import logging
logging.basicConfig(level=logging.DEBUG)
async def main():
loop = asyncio.get_running_loop()
# 设置慢回调阈值为 0.1 秒
loop.slow_callback_duration = 0.1
await asyncio.sleep(0.2) # 模拟耗时操作
asyncio.run(main())
**效果说明**:当协程执行超过 0.1 秒,日志会打印 "Executing callback took longer than 0.1 seconds"。这是定位线上阻塞最直接的证据,比看 CPU 更有用。建议阈值设为 0.05 秒敏感些,配合 ELK 收集日志告警。
老兵检查清单
-
**严禁同步 IO**:协程内禁止出现 requests, pymysql, time.sleep 等同步库。
-
**依赖审查**:引入第三方库前,确认是否支持 async/await,不要想当然。
-
**开启监控**:生产环境务必设置 `slow_callback_duration`,配合日志告警。
-
**CPU 误导**:异步阻塞时 CPU 往往不高,别被监控指标骗了,要看延迟分布。
-
**计算密集型**:CPU 密集任务别用 asyncio,该上多进程就上多进程,别混用。
你们遇到过协程阻塞最难查的案例是啥?