FastAPI 异步并发实现原理详解

4 阅读10分钟

目录

  1. 核心架构概览
  2. Python 异步基础:从协程到事件循环
  3. ASGI 协议:异步 Web 的基石
  4. FastAPI 的请求处理流程
  5. 同步函数 vs 异步函数的处理
  6. Uvicorn 与事件循环
  7. 并发模型详解
  8. 依赖注入的异步处理
  9. 性能对比与最佳实践
  10. 常见陷阱与避坑指南

一、核心架构概览

FastAPI 的高并发能力源于以下几层技术栈的组合:

┌─────────────────────────────────────────┐
│         用户业务代码 (FastAPI App)       │
├─────────────────────────────────────────┤
│      FastAPI (基于 Starlette)            │
├─────────────────────────────────────────┤
│      Starlette (ASGI 框架/工具集)        │
├─────────────────────────────────────────┤
│   ASGI 协议 (异步服务器网关接口规范)      │
├─────────────────────────────────────────┤
│   Uvicorn (ASGI 服务器, 基于 uvloop)     │
├─────────────────────────────────────────┤
│   uvloop (基于 libuv 的事件循环)         │
├─────────────────────────────────────────┤
│   asyncio (Python 异步标准库)            │
├─────────────────────────────────────────┤
│   操作系统 I/O 多路复用 (epoll/kqueue/   │
│   IOCP)                                  │
└─────────────────────────────────────────┘

关键结论:FastAPI 本身不实现并发,它通过 单线程 + 事件循环 + 协程切换 的模型,在 I/O 等待期间切换任务,从而实现高并发。


二、Python 异步基础:从协程到事件循环

2.1 协程 (Coroutine)

协程是可以暂停和恢复执行的函数。在 Python 中通过 async def 定义:

async def fetch_data():
    result = await some_io_operation()  # 暂停点
    return result
  • async def 定义的函数调用后不会立即执行,而是返回一个协程对象。
  • await主动让出控制权的关键字,告诉事件循环:"我在等 I/O,先去处理别人吧"。

2.2 事件循环 (Event Loop)

事件循环是异步编程的"调度器",核心职责:

  1. 维护一个任务队列(待执行的协程)。
  2. 监听 I/O 事件(通过操作系统的 epoll/kqueue/IOCP)。
  3. 当某个协程 await 一个 I/O 操作时,把它挂起,去执行其他就绪任务。
  4. I/O 完成后,把对应协程重新放回队列继续执行。
import asyncio

async def task(name, delay):
    print(f"{name} 开始")
    await asyncio.sleep(delay)  # I/O 等待,让出控制权
    print(f"{name} 完成")

async def main():
    # 三个任务并发执行(注意:是并发,不是并行)
    await asyncio.gather(
        task("A", 2),
        task("B", 1),
        task("C", 3),
    )

asyncio.run(main())
# 总耗时约 3 秒(不是 6 秒)

2.3 关键概念:并发 ≠ 并行

概念含义实现方式
并发 (Concurrency)多个任务交替执行(看起来同时)单线程 + 协程切换
并行 (Parallelism)多个任务真正同时执行多核 CPU + 多进程

FastAPI 单进程下是并发,不是并行。要利用多核,需要启动多个 worker 进程(如 uvicorn --workers 4)。


三、ASGI 协议:异步 Web 的基石

3.1 从 WSGI 到 ASGI

  • WSGI (Web Server Gateway Interface):传统的同步协议(Flask/Django 默认使用)。每个请求占用一个线程,I/O 阻塞期间线程被闲置。
  • ASGI (Asynchronous Server Gateway Interface):异步协议,原生支持 async/await,能在单线程内处理大量并发连接,并支持 WebSocket、HTTP/2、Server-Sent Events 等长连接场景。

3.2 ASGI 应用的标准签名

每个 ASGI 应用都是一个可调用对象(callable),签名如下:

async def app(scope, receive, send):
    """
    scope: dict, 包含连接信息(type, path, headers 等)
    receive: 异步函数, 用于接收消息(如请求体)
    send: 异步函数, 用于发送消息(如响应)
    """
    assert scope['type'] == 'http'
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [(b'content-type', b'text/plain')],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, ASGI!',
    })

FastAPI 应用本质上就是一个复杂封装过的 ASGI 应用


四、FastAPI 的请求处理流程

一个请求从到达到响应,经过的完整路径:

客户端请求
    │
    ▼
[Uvicorn] 接收 TCP 连接 → 解析 HTTP → 构造 ASGI scope
    │
    ▼
[Starlette] 路由匹配 → 中间件链 → 找到对应的 endpoint
    │
    ▼
[FastAPI] 解析路径/查询/请求体参数 → Pydantic 验证 → 依赖注入
    │
    ▼
[用户函数] async def endpoint(...) 被调度到事件循环
    │
    ├─ 如果是 async def: 直接在事件循环中执行
    │
    └─ 如果是 def: 提交到线程池 (run_in_threadpool) 执行
    │
    ▼
[FastAPI] 处理返回值 → Pydantic 序列化 → JSON 编码
    │
    ▼
[Starlette] 构造 Response → 通过 send() 发送
    │
    ▼
[Uvicorn] 写入 socket → 返回客户端

五、同步函数 vs 异步函数的处理

这是 FastAPI 设计中最重要也最容易踩坑的地方。

5.1 处理逻辑

FastAPI 通过检测端点函数是 def 还是 async def,采用不同的执行策略:

# 简化的伪代码
if asyncio.iscoroutinefunction(endpoint):
    # async def: 在事件循环中直接 await
    result = await endpoint(**kwargs)
else:
    # def: 放到线程池执行,避免阻塞事件循环
    result = await run_in_threadpool(endpoint, **kwargs)

5.2 三种写法的对比

from fastapi import FastAPI
import time
import asyncio
import httpx

app = FastAPI()

# 写法 1: 异步端点 + 异步 I/O (推荐 ✓)
@app.get("/async-good")
async def async_good():
    async with httpx.AsyncClient() as client:
        r = await client.get("https://api.example.com")
    return r.json()

# 写法 2: 同步端点 + 同步 I/O (可接受 ✓)
@app.get("/sync-ok")
def sync_ok():
    import requests
    r = requests.get("https://api.example.com")  # 阻塞,但在线程池
    return r.json()

# 写法 3: 异步端点 + 同步阻塞 I/O (灾难 ✗)
@app.get("/async-bad")
async def async_bad():
    import requests
    r = requests.get("https://api.example.com")  # 阻塞了整个事件循环!
    return r.json()

5.3 为什么写法 3 是灾难?

事件循环是单线程的。当 async def 端点中执行同步阻塞代码时:

  • 整个事件循环被卡住
  • 其他所有正在处理的请求全部停滞
  • 1000 个并发瞬间退化成串行

核心原则:在 async def 函数里,I/O 操作必须用 await 调用异步版本。如果只能用同步库,请把整个端点改成 def,让 FastAPI 调度到线程池。

5.4 线程池机制

FastAPI 默认使用 Starlette 提供的 AnyIO 线程池,默认 40 个线程 (可配置):

# Starlette 内部 (简化)
from anyio import to_thread
await to_thread.run_sync(sync_function, *args)

意味着:同步端点最多并发 40 个,超过的请求要排队。


六、Uvicorn 与事件循环

6.1 Uvicorn 的角色

Uvicorn 是一个ASGI 服务器,负责:

  1. 监听 TCP 端口,接受连接。
  2. 解析 HTTP/WebSocket 协议。
  3. 将请求转化为 ASGI scope/receive/send
  4. 调用 ASGI 应用(FastAPI)。
  5. 把响应写回 socket。

6.2 uvloop:性能加速器

Uvicorn 默认会在可用时使用 uvloop 替代 asyncio 的默认事件循环:

  • uvloop 基于 Cython 封装的 libuv(Node.js 同款)。
  • 比标准 asyncio 事件循环快 2-4 倍
  • Windows 不支持 uvloop(会回退到 SelectorEventLoopProactorEventLoop)。
pip install uvloop  # Linux/Mac
uvicorn main:app --loop uvloop

6.3 Worker 进程模型

单个事件循环只能利用一个 CPU 核心。生产环境通常起多个 worker:

uvicorn main:app --workers 4
# 或用 gunicorn 管理
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker

模型如下:

        Master Process
        ┌──────────────┐
        │   Gunicorn   │ (负责管理 worker)
        └──────┬───────┘
               │ fork
   ┌───────────┼───────────┬───────────┐
   ▼           ▼           ▼           ▼
Worker 1   Worker 2   Worker 3   Worker 4
(事件循环) (事件循环) (事件循环) (事件循环)
   │           │           │           │
   └─ 每个进程独立处理 ~10K 并发连接 ─┘

经验公式:worker 数 ≈ CPU 核数 × 2 + 1(I/O 密集场景可更多)。


七、并发模型详解

7.1 单线程为什么能高并发?

传统多线程模型:每个连接 = 一个线程,1万连接 = 1万线程,内存爆炸(每线程约 8MB 栈)。

FastAPI 异步模型:

事件循环 (1个线程)
    │
    ├── 监听 epoll/kqueue
    │
    ├── 任务队列: [task1, task2, task3, ...]
    │
    └── 调度逻辑:
        while True:
            ready_tasks = epoll.wait()  # 等待 I/O 事件
            for task in ready_tasks:
                run_until_await(task)    # 执行到下一个 await

核心:当一个协程 await 网络/磁盘 I/O 时,它被挂起,CPU 立即去执行其他就绪的协程。CPU 几乎不闲置。

7.2 一个具体的并发示例

import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/slow")
async def slow():
    await asyncio.sleep(2)  # 模拟 I/O
    return {"done": True}

假设 100 个客户端同时请求 /slow

模型总耗时资源占用
同步 + 单线程200 秒1 线程
同步 + 多线程2 秒100 线程 (~800MB)
异步 + 单线程2 秒1 线程 (~50MB)

7.3 CPU 密集型任务的处理

异步模型的弱点:CPU 密集任务会阻塞事件循环

@app.get("/cpu-bad")
async def cpu_bad():
    result = sum(i*i for i in range(10**8))  # 卡死事件循环!
    return {"result": result}

正确做法:

from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool
from concurrent.futures import ProcessPoolExecutor

executor = ProcessPoolExecutor()

@app.get("/cpu-good")
async def cpu_good():
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(executor, heavy_computation)
    return {"result": result}

八、依赖注入的异步处理

FastAPI 的 Depends 系统同样支持异步:

from fastapi import Depends, FastAPI

async def get_db():
    db = await create_async_session()
    try:
        yield db
    finally:
        await db.close()

@app.get("/users")
async def list_users(db = Depends(get_db)):
    return await db.fetch_all("SELECT * FROM users")

关键机制

  • 依赖项可以是 defasync def,FastAPI 自动适配(同上:sync 走线程池,async 走事件循环)。
  • yield 形式的依赖会在响应发送后执行清理代码(类似 finally)。
  • 多个依赖的解析是串行的(按声明顺序),但每个依赖内部仍享受异步优势。

九、性能对比与最佳实践

9.1 基准对比 (TechEmpower Benchmarks 大致水平)

框架请求/秒 (RPS)模型
Flask (sync)~5,000WSGI 多线程
Django (sync)~7,000WSGI 多线程
FastAPI~60,000ASGI + asyncio
Node.js (Express)~50,000事件循环
Go (net/http)~150,000Goroutine

9.2 最佳实践

  1. 优先使用 async def,配合异步库:

    • 数据库:asyncpgdatabasesSQLAlchemy 2.0 asyncTortoise ORM
    • HTTP:httpx.AsyncClientaiohttp
    • 缓存:aioredis
    • 文件:aiofiles
  2. 同步代码必须放在 def 端点中,让 FastAPI 自动调度到线程池。

  3. CPU 密集任务:用 ProcessPoolExecutor 或独立服务(Celery、消息队列)。

  4. 生产部署

    gunicorn main:app \
      -w 4 \
      -k uvicorn.workers.UvicornWorker \
      --bind 0.0.0.0:8000 \
      --timeout 60
    
  5. 调整线程池大小(处理同步端点较多时):

    from anyio.lowlevel import RunVar
    from anyio import CapacityLimiter
    
    @app.on_event("startup")
    def startup():
        RunVar("_default_thread_limiter").set(CapacityLimiter(100))
    

十、常见陷阱与避坑指南

陷阱 1:在 async 函数里用同步库

# ✗ 错误
@app.get("/bad")
async def bad():
    time.sleep(1)         # 阻塞事件循环
    requests.get(url)     # 阻塞事件循环

# ✓ 正确
@app.get("/good")
async def good():
    await asyncio.sleep(1)
    async with httpx.AsyncClient() as c:
        await c.get(url)

陷阱 2:忘记 await 协程

# ✗ 错误:返回的是协程对象,不是结果
@app.get("/bug")
async def bug():
    result = some_async_func()      # 没 await
    return result                    # 返回 <coroutine>

# ✓ 正确
@app.get("/fixed")
async def fixed():
    result = await some_async_func()
    return result

陷阱 3:在异步上下文中使用全局可变状态

# 因为单线程事件循环,以下代码看似线程安全
counter = 0

@app.get("/inc")
async def inc():
    global counter
    current = counter
    await asyncio.sleep(0)   # 这里发生了切换!
    counter = current + 1    # 竞态条件

虽然单线程不会有真正的数据竞争,但 await 是切换点,多个协程交错执行仍会破坏不变量。需要时使用 asyncio.Lock

陷阱 4:滥用 BackgroundTasks 做长任务

BackgroundTasks 仍运行在同一个事件循环(或线程池)中,长时间任务会拖累服务。长任务应使用 Celery / RQ / Arq 等专用队列。

陷阱 5:数据库连接池配置过小

异步框架可以瞬间发起几千个并发查询,但连接池只有 10 个,多余请求会排队。需要根据并发量调整 pool_sizemax_overflow


总结

FastAPI 的高并发能力来源于:

  1. 协程 提供了轻量级的"暂停/恢复"机制。
  2. 事件循环 在 I/O 等待时切换任务,让 CPU 不空转。
  3. uvloop 用 C 实现的事件循环带来 2-4 倍的性能。
  4. ASGI 协议 让请求处理全程异步化。
  5. 线程池兜底 让同步代码也能不阻塞事件循环。
  6. 多 worker 进程 突破 GIL 限制,利用多核。

理解这套机制后,关键的实践原则只有一条:

async def 端点中的所有 I/O 必须是异步的;做不到就用 def 端点。

遵守这条原则,FastAPI 才能真正发挥它的并发威力。