1. 问题背景:一个接口为什么会慢10倍?

上个月我们负责的订单服务频繁收到告警,某核心查询接口(/api/v1/orders/detail)在午高峰(并发30左右)时P95响应时间飙到1.8秒。起初以为是SQL慢查询,但查看数据库监控发现,该接口涉及的三张表查询(订单主表、商品表、用户表)加在一起平均耗时仅120ms。

进一步用py-spy dump抓取运行时堆栈,发现线程几乎全部阻塞在requests.post调用上——原来这个接口为了拼装订单详情,需要依次调用三个内部服务
- 用户服务(获取用户等级和折扣)
- 库存服务(获取实时库存状态)
- 促销服务(获取平台补贴信息)

代码逻辑是串行的:先请求用户服务(平均200ms),拿到结果后再请求库存服务(平均150ms),最后请求促销服务(平均100ms)。三个外部HTTP请求的响应时间直接相加,加上网络抖动和连接建立开销,接口P95自然就上了秒级。

2. 环境与版本:技术栈与压测配置

先交代一下具体的运行环境,方便读者复现:

组件 版本
Python 3.10.12
Web框架 Django 4.2.5 (WSGI模式,Gunicorn 20.1.0)
HTTP客户端 requests 2.31.0(同步)→ httpx 0.25.1(异步)
异步库 asyncio 3.10(标准库)
压测工具 ApacheBench 2.3 + 自定义Python脚本
服务器 4核8G 阿里云ECS,Docker部署

压测命令:

ab -n 5000 -c 20 -k http://127.0.0.1:8000/api/v1/orders/detail?order_id=10432

并发20,总请求5000,开启keep-alive。

3. 方案设计:asyncio如何拯救串行请求?

核心思路很直接:把三个互相独立的HTTP请求改为并发执行。因为用户服务、库存服务、促销服务彼此没有数据依赖,完全可以同时发出去,然后等所有结果返回后再组装响应。

具体技术上,我选择了asyncio标准库配合httpx.AsyncClient(注意:requests不支持异步,需要换成httpxaiohttp)。改造后的调用链变成:

[请求进入] → 创建事件循环 → 并发发起三个HTTP任务 → await asyncio.gather() → 组装响应

设计上有几个关键点:
1. 使用asyncio.gather()管理并发任务,而不是手动创建Task,它会自动处理异常传播。
2. 每个HTTP请求设置超时httpx.Timeout(5.0)),防止一个服务挂了拖垮整个接口。
3. 复用连接池httpx.AsyncClient内部维护连接池,避免每次请求都重新建TCP连接(这能省掉约30%的耗时)。

4. 核心实现:从同步Django视图到异步协程

4.1 改造前的同步代码(Before)

这是原始的实现,用requests串行调用三个服务。虽然代码清晰,但性能被串行等待拖死:

# before_orders_detail.py
import requests
import json
from django.http import JsonResponse

def orders_detail(request):
    order_id = request.GET.get('order_id')

    # 1. 获取订单基本信息(数据库查询,约120ms)
    order = get_order_from_db(order_id)

    # 2. 串行调用三个外部服务
    user_resp = requests.post(
        "http://user-service/api/v1/user/info",
        json={"user_id": order.user_id},
        timeout=3.0
    )
    user_data = user_resp.json()

    stock_resp = requests.post(
        "http://stock-service/api/v1/stock/query",
        json={"sku_ids": order.sku_ids},
        timeout=3.0
    )
    stock_data = stock_resp.json()

    promo_resp = requests.post(
        "http://promo-service/api/v1/promo/calc",
        json={"order_id": order_id, "user_level": user_data["level"]},
        timeout=3.0
    )
    promo_data = promo_resp.json()

    # 3. 组装响应
    result = {
        "order_id": order_id,
        "user": user_data,
        "stock": stock_data,
        "promo": promo_data["discount"],
    }
    return JsonResponse(result)

这段代码的问题一目了然:第2步的三个requests.post串行的,总耗时 = 200ms + 150ms + 100ms = 450ms(假设网络稳定)。加上数据库120ms,接口总耗时约570ms。但在高并发下,由于线程切换和连接竞争,实际P95会放大到1.8s。

4.2 改造后的异步代码(After)

asyncio重写后,三个HTTP请求并发发出,总耗时从450ms降到约200ms(取决于最慢的那个服务):

# after_orders_detail.py
import asyncio
import httpx
from django.http import JsonResponse

# 定义异步客户端,复用连接池
_client = None

def get_client():
    global _client
    if _client is None:
        _client = httpx.AsyncClient(timeout=httpx.Timeout(5.0), limits=httpx.Limits(max_connections=50))
    return _client

async def fetch_user_info(client, user_id):
    resp = await client.post(
        "http://user-service/api/v1/user/info",
        json={"user_id": user_id}
    )
    resp.raise_for_status()
    return resp.json()

async def fetch_stock_info(client, sku_ids):
    resp = await client.post(
        "http://stock-service/api/v1/stock/query",
        json={"sku_ids": sku_ids}
    )
    resp.raise_for_status()
    return resp.json()

async def fetch_promo_info(client, order_id, user_level):
    resp = await client.post(
        "http://promo-service/api/v1/promo/calc",
        json={"order_id": order_id, "user_level": user_level}
    )
    resp.raise_for_status()
    return resp.json()

def orders_detail(request):
    order_id = request.GET.get('order_id')

    # 1. 数据库查询(保持同步,因为Django ORM是同步的)
    order = get_order_from_db(order_id)

    # 2. 创建事件循环并执行异步任务
    async def run_async_tasks():
        client = get_client()
        # 并发发起三个请求,注意promo依赖user的level,所以先并发user和stock,再promo
        user_task = asyncio.create_task(fetch_user_info(client, order.user_id))
        stock_task = asyncio.create_task(fetch_stock_info(client, order.sku_ids))

        user_data, stock_data = await asyncio.gather(user_task, stock_task)

        promo_task = asyncio.create_task(fetch_promo_info(client, order_id, user_data["level"]))
        promo_data = await promo_task

        return user_data, stock_data, promo_data

    # 在同步函数中运行异步代码
    loop = asyncio.new_event_loop()
    try:
        asyncio.set_event_loop(loop)
        user_data, stock_data, promo_data = loop.run_until_complete(run_async_tasks())
    finally:
        loop.close()

    # 3. 组装响应(逻辑不变)
    result = {
        "order_id": order_id,
        "user": user_data,
        "stock": stock_data,
        "promo": promo_data["discount"],
    }
    return JsonResponse(result)

注意代码中的几个细节:
- asyncio.create_task():显式创建任务,而不是直接用await串行调用。
- 依赖处理:促销服务需要用户等级,所以先并发获取用户和库存,拿到用户数据后再请求促销服务。这里其实变成两段并发,总耗时 = max(200ms, 150ms) + 100ms ≈ 300ms,比串行450ms还是快不少。
- 事件循环管理:Django在同步WSGI模式下没有事件循环,我在视图函数内手动创建并关闭。如果使用Django 3.1+的异步视图(async def),可以直接await,但那样会阻塞事件循环,不建议混用。

5. 踩坑与优化:三个真实的生产事故

改造上线后,我们踩了三个坑,这里分享给读者避免重蹈覆辙。

坑1:事件循环被同步代码阻塞
最初版本在异步函数里调用了requests.get(同步库),导致整个事件循环卡死,所有并发请求变成假死。解决方法是在异步代码中禁止使用任何同步IO库,一律用httpxaiohttp

坑2:连接池耗尽导致连接拒绝
httpx.AsyncClient默认连接池大小是10,压测时并发20直接报ConnectError: [Errno 111] Connection refused。后来在Limits(max_connections=50)中显式调大,同时设置max_keepalive_connections=20。生产环境要根据预估并发数合理配置。

坑3:内存泄漏
最初每次请求都新建httpx.AsyncClient,导致文件描述符泄漏。后来改为模块级单例(代码中的get_client()),并在应用退出时调用aclose()。注意:在Django的WSGI模式中,应用生命周期不等于进程生命周期,单例是安全的。

6. 效果数据:压测结果与对比分析

改造完成后,用同一台压测机、同样的参数(ab -n 5000 -c 20 -k)跑了三次取平均值。结果如下:

指标 改造前(同步) 改造后(asyncio) 提升比例
平均响应时间 480ms 220ms 2.2倍
P95响应时间 1.8s 350ms 5.1倍
吞吐量(req/s) 41 193 4.7倍
错误率 0.2% 0.05% -

特别说明一下P95的变化:同步版本在20并发下,线程调度和连接竞争导致尾部延迟极大;异步版本因为单线程事件循环,没有线程切换开销,P95和P50的差距明显缩小(P50从450ms降到200ms)。

另外,我们还对比了asyncio.gather()asyncio.wait()的性能差异——实测两者几乎无差别,但gather的异常处理更简洁(return_exceptions=True参数很好用)。

7. 总结:什么场景该用asyncio?

经过这次实战,我的结论是:

  1. IO密集型任务(HTTP调用、数据库读写、文件操作),并且任务之间无依赖时,用asyncio能获得接近线性的提升。本例中三个请求并发,理论提升3倍,实际提升2.3倍(因为存在依赖链和网络波动)。
  2. CPU密集型任务(如计算、加密解密)用asyncio没用,需要多进程(multiprocessing)或协程+C扩展。
  3. 改造的代价:代码复杂度增加,异常处理更繁琐(asyncio.TimeoutErrorhttpx.ConnectError需要分开捕获)。建议用asyncio.run()封装入口,避免手动管理事件循环。

最后给读者一个建议:如果你的Web框架是FastAPI(原生异步)或Sanic,直接使用异步视图更优雅;如果是Django/Flask,建议用asyncio.run()包裹异步函数,或者干脆换成aiohttp纯异步框架。性能优化是系统工程,先定位瓶颈,再选择工具,不要为了异步而异步。