目录
- 核心架构概览
- Python 异步基础:从协程到事件循环
- ASGI 协议:异步 Web 的基石
- FastAPI 的请求处理流程
- 同步函数 vs 异步函数的处理
- Uvicorn 与事件循环
- 并发模型详解
- 依赖注入的异步处理
- 性能对比与最佳实践
- 常见陷阱与避坑指南
一、核心架构概览
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)
事件循环是异步编程的"调度器",核心职责:
- 维护一个任务队列(待执行的协程)。
- 监听 I/O 事件(通过操作系统的 epoll/kqueue/IOCP)。
- 当某个协程
await一个 I/O 操作时,把它挂起,去执行其他就绪任务。 - 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 服务器,负责:
- 监听 TCP 端口,接受连接。
- 解析 HTTP/WebSocket 协议。
- 将请求转化为 ASGI
scope/receive/send。 - 调用 ASGI 应用(FastAPI)。
- 把响应写回 socket。
6.2 uvloop:性能加速器
Uvicorn 默认会在可用时使用 uvloop 替代 asyncio 的默认事件循环:
- uvloop 基于 Cython 封装的 libuv(Node.js 同款)。
- 比标准
asyncio事件循环快 2-4 倍。 - Windows 不支持 uvloop(会回退到
SelectorEventLoop或ProactorEventLoop)。
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")
关键机制:
- 依赖项可以是
def或async def,FastAPI 自动适配(同上:sync 走线程池,async 走事件循环)。 yield形式的依赖会在响应发送后执行清理代码(类似finally)。- 多个依赖的解析是串行的(按声明顺序),但每个依赖内部仍享受异步优势。
九、性能对比与最佳实践
9.1 基准对比 (TechEmpower Benchmarks 大致水平)
| 框架 | 请求/秒 (RPS) | 模型 |
|---|---|---|
| Flask (sync) | ~5,000 | WSGI 多线程 |
| Django (sync) | ~7,000 | WSGI 多线程 |
| FastAPI | ~60,000 | ASGI + asyncio |
| Node.js (Express) | ~50,000 | 事件循环 |
| Go (net/http) | ~150,000 | Goroutine |
9.2 最佳实践
-
优先使用
async def,配合异步库:- 数据库:
asyncpg、databases、SQLAlchemy 2.0 async、Tortoise ORM - HTTP:
httpx.AsyncClient、aiohttp - 缓存:
aioredis - 文件:
aiofiles
- 数据库:
-
同步代码必须放在
def端点中,让 FastAPI 自动调度到线程池。 -
CPU 密集任务:用
ProcessPoolExecutor或独立服务(Celery、消息队列)。 -
生产部署:
gunicorn main:app \ -w 4 \ -k uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 \ --timeout 60 -
调整线程池大小(处理同步端点较多时):
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_size 和 max_overflow。
总结
FastAPI 的高并发能力来源于:
- 协程 提供了轻量级的"暂停/恢复"机制。
- 事件循环 在 I/O 等待时切换任务,让 CPU 不空转。
- uvloop 用 C 实现的事件循环带来 2-4 倍的性能。
- ASGI 协议 让请求处理全程异步化。
- 线程池兜底 让同步代码也能不阻塞事件循环。
- 多 worker 进程 突破 GIL 限制,利用多核。
理解这套机制后,关键的实践原则只有一条:
async def端点中的所有 I/O 必须是异步的;做不到就用def端点。
遵守这条原则,FastAPI 才能真正发挥它的并发威力。