一、问题背景:一个拖垮业务的“简单”接口

上个月接手一个内部数据中台服务,其中一个/api/v1/batch_query接口被多个业务方调用。逻辑不复杂:前端传入一批商品ID(最多50个),后端需要去5个不同的上游服务拉取价格、库存、物流、评价、促销数据,聚合后返回。

当时的实现是同步串行的:for循环里挨个请求上游,每个请求平均200ms,5个就是1秒,加上JSON解析和聚合,接口平均响应时间1.2秒。在峰值流量下,Tomcat线程池被打满,CPU idle为0,但都在等IO。

压测数据(wrk -t8 -c200 -d30s):
- QPS: 87
- RT Avg: 1150ms
- RT P99: 2300ms

这个接口成了系统的瓶颈。业务方吐槽,领导施压,我决定用asyncio重构。

二、环境与版本:别用旧版本踩坑

先说环境,踩坑记录在后面,版本很关键:

  • Python: 3.11.4(必须3.10+,我要用TaskGroup,3.9及以下没有)
  • Web框架: 原本是Flask 2.3.2 + gunicorn 20.1.0。注意:Flask本身不支持异步,我改成aiohttp 3.8.5作为入口,或者用quart。我选了aiohttp,因为文档更熟。
  • HTTP客户端: aiohttp 3.8.5(自带连接池,比requests快一个数量级)
  • 压测工具: wrk 4.2.0
  • 机器: 4核8G,CentOS 7.9,网络内网延迟 50ms,那asyncio是刚需,收益是数量级的。但如果你的接口是纯CPU计算(比如加密、图像处理),asyncio没用,应该用多进程。

最后给三点实用建议:

  1. 别用requests + asyncio.to_thread,那是假异步,线程池照样阻塞。要用真正的异步客户端(aiohttp/httpx)。
  2. 信号量必须加,否则上游服务会被你打死。我见过不加限流直接压测,把整个测试环境搞挂的。
  3. Python 3.11+ 的 asyncio.TaskGroup 比 gather 更好用,它会在任务失败时自动取消其他任务,语义更清晰。我上面的代码用了gather是因为兼容性,新项目建议直接用async with asyncio.TaskGroup() as tg

这次改造让我在组里“封神”了,但说实话,只是把正确的工具用在了正确的地方。希望这篇能帮到正在被同步IO折磨的你。有问题评论区见。