真实场景:被QPS碾过的Flask
去年我们上线了一个实时数据聚合API,峰值QPS大约3000。Flask + Gunicorn(4 worker,1个主进程)跑在8核机器上,某个周五晚高峰P99延迟飙到800ms,直接触发上游熔断。我连夜把接口用FastAPI重写了,单进程(Uvicorn + 4 worker)扛到3500 QPS,P99稳定在50ms以下。但后来我发现:如果不理解FastAPI的源码,随便写个同步阻塞函数进去,性能立刻打回原形。所以我把FastAPI 0.111.0(依赖Starlette 0.37.2、Pydantic 2.7.4)的源码翻了一遍。
方案对比:Flask vs FastAPI处理模型
两个框架核心差异:
| 维度 | Flask 3.0.2 | FastAPI 0.111.0 |
|---|---|---|
| 协议 | WSGI(同步) | ASGI(异步) |
| 请求解析 | 每个请求分配新线程(Werkzeug) | 单线程异步事件循环 |
| 路由查找 | 字典映射+正则(O(N)) | 预编译Trie树(O(1)) |
| 参数验证 | 手动类型转换与校验 | Pydantic模型自动验证 |
| 依赖注入 | 无,需手动封装 | 基于类型注解的DI系统 |
下面我从源码角度拆解FastAPI最核心的三个部分:路由注册-请求解析-依赖注入。
完整代码实现:一个可压测的FastAPI应用
# app.py – FastAPI 示例
from fastapi import FastAPI, Depends, Query
from pydantic import BaseModel
import time, asyncio
app = FastAPI(title="demo")
# ---------- 依赖注入 ----------
class Database:
def __init__(self):
self.conn = "connected"
async def query(self, sql: str):
await asyncio.sleep(0.001) # 模拟异步DB查询
return f"result of {sql}"
db = Database()
async def get_db():
return db
# ---------- 请求体模型 ----------
class Item(BaseModel):
name: str
price: float
# ---------- 路由 ----------
@app.get("/items")
async def read_items(page: int = Query(1, ge=1), db=Depends(get_db)):
result = await db.query(f"SELECT * FROM items LIMIT 10 OFFSET {(page-1)*10}")
return {"data": result}
@app.post("/items")
async def create_item(item: Item):
# 这里只演示验证
return {"received": item.model_dump()}
Flask对比版本
# flask_app.py
from flask import Flask, request, jsonify
import time, concurrent.futures, threading
app = Flask(__name__)
_db_lock = threading.Lock()
def query_db(sql):
time.sleep(0.001) # 模拟同步DB查询
return f"result of {sql}"
@app.route("/items", methods=["GET"])
def read_items():
page = request.args.get("page", 1, type=int)
with _db_lock:
result = query_db(f"SELECT * FROM items LIMIT 10 OFFSET {(page-1)*10}")
return jsonify({"data": result})
@app.route("/items", methods=["POST"])
def create_item():
data = request.get_json()
# 手动验证
if not isinstance(data.get("name"), str) or not isinstance(data.get("price"), (int, float)):
return jsonify({"error": "invalid"}), 400
return jsonify({"received": data})
源码深入:路由注册与查找
1. 路由装饰器如何工作
当我们写 @app.get("/items") 时,FastAPI实际上调用 app.router.add_api_route()。查看 fastapi/routing.py 第 385-405 行:
# fastapi/routing.py (简化)
class APIRouter(routing.Router):
def add_api_route(
self,
path: str,
endpoint: Callable,
methods: Optional[Union[Set[str], List[str]]] = None,
...
) -> None:
# 关键:生成一个 APIRoute 实例并注册到Starlette的Router
route = APIRoute(
path,
endpoint=endpoint,
methods=methods,
# ... 传递依赖, 响应模型等
)
self.routes.append(route)
而Starlette的 Router 在 starlette/routing.py 中维护一个 _routes 列表,但查找时使用前缀树(Trie)优化。以版本0.37.2为例,它的 __call__ 方法里遍历 routes,但并不是O(N)扫描:
# starlette/routing.py (核心)
class Router:
def __init__(self, routes=None):
self.routes = [] # 实际上存储预处理后的Trie节点
# 通过 _is_asgi3 等内部方法,在add_route时构建字典树
def add_route(self, path, endpoint, methods=None):
route = Route(path, endpoint, methods=methods)
self.routes.append(route)
# 在内部,每次请求到来时,Route 的 path 被编译成正则,但匹配是预编译的
实际上Starlette的 Route 类使用 compile_path 把 /items/{id} 转为正则表达式,并缓存编译好的 re.compile 对象。当路由数量少时,遍历匹配;路由数量多时,通过 _routes 列表但每个路由的匹配是O(1)级别的正则。但更重要的是 – FastAPI 和 Starlette 的底层 Router 有一个优化:把静态路径(不含参数)存入一个字典,实现直接哈希查找。
# starlette/routing.py (约800行)
class Router:
def __call__(self, scope, receive, send):
# 对于静态路径,先查字典
path = scope["path"]
if path in self._plain_routes:
# 直接获取对应Route对象
route = self._plain_routes[path]
...
else:
# 动态路由,遍历所有正则路由
for route in self._parametrized_routes:
if route.match(path):
...
这就是O(1)查找的来源。FastAPI在注册路由时,add_api_route 会调用Starlette的 add_route,最终把静态路径塞入 _plain_routes 字典。
2. 请求解析与依赖注入的源码
当一个请求抵达,FastAPI的 app.__call__ 实际上是Starlette的 ASGIApp。关键在于请求如何组装出路径参数、查询参数、请求体,并自动注入到函数。这由 run_endpoint_function 和 solve_dependencies 完成。
# fastapi/routing.py (约500行)
async def run_endpoint_function(
*, dependant: Dependant, values: Dict[str, Any], is_coroutine: bool, ...
) -> Any:
# 重点:调用 solve_dependencies 获取所有需要的参数
values, errors, *_ = await solve_dependencies(
request=request, dependant=dependant, ...
)
if is_coroutine:
return await dependant.call(**values)
else:
return await run_in_threadpool(dependant.call, **values)
solve_dependencies 函数在 fastapi/dependencies/utils.py 中。它递归地遍历 Dependant 对象树(这棵树是在应用启动时通过 inspect.signature 分析函数签名生成的),对于每个参数:
- 如果是
Depends,递归调用solve_dependencies获取依赖的值; - 如果是
Query、Path等,从请求对象中提取对应数据; - 如果是Pydantic模型,使用
model_validate进行自动类型转换与校验。
整个过程完全异步,不会阻塞事件循环。
效果数据:压测对比
测试环境:MacBook Pro M3 Pro (12核), Python 3.11.5, Uvicorn 0.29.0, Gunicorn 22.0.0, 4 workers, 同一台机器内压测。压测工具 wrk2,持续30秒,并发256连接,GET /items?page=1。
# FastAPI 启动
uvicorn app:app --workers 4 --port 8001
# Flask 启动
gunicorn flask_app:app --workers 4 --worker-class sync --timeout 30 --port 8002
# 压测命令
wrk -t16 -c256 -d30s --latency http://localhost:8001/items\?page=1
wrk -t16 -c256 -d30s --latency http://localhost:8002/items\?page=1
结果:
FastAPI:
Requests/sec: 3521.2
Latency (avg): 2.33ms
P99 Latency: 12.45ms
Transfer/sec: 1.48MB
Flask:
Requests/sec: 1287.6
Latency (avg): 12.86ms
P99 Latency: 200.12ms
Transfer/sec: 0.52MB
FastAPI在4 worker下QPS是Flask的2.7倍,P99延迟仅为Flask的1/16。同样的查询业务,Flask的多线程上下文切换和GIL锁消耗显著。
内存占用(RSS):FastAPI启动后约120MB,Flask约85MB,但Flask每个worker独立内存,4个worker合计约340MB。FastAPI的worker共享内存(通过进程fork),实际4个worker仅180MB。整体内存节省47%。
避坑指南
坑1:全局可变对象作为依赖
我在一个业务中用 Depends(get_redis) 注入了 redis_client 实例,但该实例在应用高并发时频繁被修改。FastAPI的依赖注入默认在同一个请求内会缓存,但跨请求并不会互斥。我犯的错误是:在依赖中创建了全局 dict 作为缓存,没加锁,导致数据错乱。
解决方案:依赖函数应该返回无副作用的对象,或使用 typing 里的 ContextVar 隔离请求上下文。如果必须用全局缓存,使用 asyncio.Lock 或使用线程安全的库如 aioredis 自带的连接池。
坑2:同步阻塞调用导致事件循环卡死
在某个依赖里我直接调用了 time.sleep(0.1) 模拟耗时,结果整个worker的QPS从3000掉到300。源码里 solve_dependencies 是在异步上下文中运行的,任何同步阻塞都会阻塞事件循环。
解决方案:用 asyncio.sleep 或者 await asyncio.to_thread(blocking_func)。阅读FastAPI源码会发现,对于非协程的路径函数,它自动用 run_in_threadpool 调度,但依赖函数必须手动处理。
坑3:Pydantic模型验证失败时不返回422
新版本Pydantic v2中,model_validate 对某些非法输入会抛出 ValidationError,但FastAPI内部已经捕获并转化为422响应。但有次我自定义了一个 @validator 装饰器(Pydantic v1遗留代码),结果在 model_validate 之前就抛出了异常,未被框架捕获,导致500错误。
解决方案:升级项目到Pydantic v2后,改用 @field_validator 和 model_validator。阅读 fastapi/dependencies/utils.py 中 request_body_to_args 函数可以看到,它用的是 await request.json() 后的 model = field_model.model_validate(data),所有的验证异常都会被包在 RequestValidationError 中处理。自定义钩子要确保符合
总结
FastAPI的高性能建立在几个关键设计上:Starlette的ASGI路由查找(静态路径O(1))、Pydantic的异步验证、以及基于协程的事件循环。理解这些源码细节,能帮你避免90%的性能坑。下次如果你遇到请求卡顿,先检查依赖注入里有没有阻塞调用。
最后提醒:源码版本很重要。本文基于FastAPI 0.111.0,如果你用的0.109以下或1.x(尚未发布但计划中)可能有差异。读源码时用 git checkout tags/0.111.0 锁定版本。