轻量而不轻浮FastAPI 生产实践中的取舍一、Demo 能跑生产就崩那些文档没说的坑FastAPI 上手确实快。Hello World五行代码搞定OpenAPI 文档自动生成类型提示和 Pydantic 配合起来数据校验也顺手。但真要把项目往生产环境推框架文档里没写的问题就来了异步数据库连接池泄漏、中间件执行顺序踩坑、依赖注入生命周期管理、ASGI 服务器配置不当导致的并发瓶颈。这些问题不是 FastAPI 的设计缺陷而是表面极简和运行时复杂之间的认知差。框架把复杂性藏在默认值后面了一旦你要偏离默认行为就得自己搞清楚底层在干什么。二、请求是怎么被处理的中间件、依赖注入、路由FastAPI 基于 Starlette 的 ASGI 协议每个请求走一条明确的处理链路。理解这条链路才能正确使用中间件和依赖注入。sequenceDiagram participant Client as 客户端 participant ASGI as Uvicorn participant MW as 中间件栈 participant DI as 依赖注入系统 participant Endpoint as 路由处理函数 Client-ASGI: HTTP Request ASGI-MW: 请求进入中间件栈洋葱模型 Note over MW: Middleware A → B → Cbr/按注册顺序正向执行 MW-DI: 解析路径参数与依赖 DI-DI: 解析 Depends() 依赖树 DI-DI: 执行依赖函数获取资源 Note over DI: 数据库连接、认证信息、配置项等 DI-Endpoint: 注入依赖执行业务逻辑 Endpoint--DI: 返回 Response 或抛出异常 DI--MW: 依赖清理yield 依赖的 finally 块 Note over DI: 释放数据库连接、关闭文件句柄等 MW--ASGI: 响应穿过中间件栈反向执行 Note over MW: Middleware C → B → Abr/按注册顺序反向执行 ASGI--Client: HTTP Response两个容易被忽视的细节第一中间件是洋葱模型。注册在前的中间件先处理请求、后处理响应。如果你有一个认证中间件和一个日志中间件注册顺序决定了日志里能不能拿到认证信息——认证中间件必须排在日志前面否则日志里拿不到用户 ID。第二yield依赖比try/finally更可靠。yield之后的代码无论路由函数是否抛异常都会执行这保证了数据库连接、文件句柄等资源一定会被释放。我自己写代码时经常忘记finally但yield之后写清理逻辑几乎不会漏。三、项目结构按业务领域拆别按技术层拆下面是一个生产环境里验证过的结构。核心思路很简单按业务领域拆分而不是按技术层拆分。# # app/main.py —— 应用入口 # from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import settings from app.core.database import engine, Base from app.api.v1 import api_router from app.core.exceptions import register_exception_handlers asynccontextmanager async def lifespan(app: FastAPI): # ---- 启动阶段 ---- async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) from app.core.redis import redis_pool await redis_pool.initialize() yield # ---- 关闭阶段 ---- await redis_pool.close() await engine.dispose() app FastAPI( titlesettings.APP_NAME, versionsettings.VERSION, docs_url/docs if settings.DEBUG else None, lifespanlifespan, ) app.add_middleware( CORSMiddleware, allow_originssettings.ALLOWED_ORIGINS, allow_credentialsTrue, allow_methods[*], allow_headers[*], ) register_exception_handlers(app) app.include_router(api_router, prefix/api/v1)几个设计决策说明一下lifespan替代on_event后者已废弃asynccontextmanager更清晰CORS 必须最外层否则预检请求可能被内部中间件拦截生产环境关闭 Swaggerdocs_urlNone避免暴露接口文档# # app/core/database.py —— 数据库连接池 # from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker, DeclarativeBase engine create_async_engine( settings.DATABASE_URL, pool_size10, max_overflow10, pool_recycle3600, echosettings.DEBUG, ) AsyncSessionLocal sessionmaker( engine, class_AsyncSession, expire_on_commitFalse, ) class Base(DeclarativeBase): pass连接池参数调优建议pool_size设为 CPU 核心数 × 2pool_recycle设为 3600 秒避免 MySQL 8 小时断连。expire_on_commitFalse很重要否则提交后对象过期懒加载会触发额外查询。# # app/core/deps.py —— 依赖注入 # from typing import Annotated, AsyncGenerator from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import jwt, JWTError from app.core.database import AsyncSessionLocal oauth2_scheme OAuth2PasswordBearer(tokenUrl/api/v1/auth/login) async def get_db() - AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: try: yield session await session.commit() except Exception: await session.rollback() raise async def get_current_user( token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[AsyncSession, Depends(get_db)], ) - dict: credentials_exception HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail无法验证凭据, headers{WWW-Authenticate: Bearer}, ) try: payload jwt.decode( token, settings.SECRET_KEY, algorithms[settings.ALGORITHM], ) user_id: str | None payload.get(sub) if user_id is None: raise credentials_exception except JWTError: raise credentials_exception from app.services.user import UserService user await UserService.get_by_id(db, user_id) if user is None: raise credentials_exception return {id: user.id, plan: user.plan} DB Annotated[AsyncSession, Depends(get_db)] CurrentUser Annotated[dict, Depends(get_current_user)]依赖注入类型别名DB、CurrentUser的好处是路由函数签名可以压缩到一行同时保留完整的类型提示和 IDE 补全。认证依赖失败时直接抛 401而不是返回None——避免下游代码出现空指针。# # app/core/exceptions.py —— 全局异常处理 # from fastapi import Request from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError class AppException(Exception): def __init__(self, code: int, message: str): self.code code self.message message def register_exception_handlers(app: FastAPI): app.exception_handler(RequestValidationError) async def validation_handler(request: Request, exc: RequestValidationError): errors [] for err in exc.errors(): field ..join(str(loc) for loc in err[loc][1:]) errors.append({field: field, message: err[msg]}) return JSONResponse( status_code422, content{code: 422, message: 参数校验失败, details: errors}, ) app.exception_handler(AppException) async def app_exception_handler(request: Request, exc: AppException): return JSONResponse( status_codeexc.code // 100 * 100, content{code: exc.code, message: exc.message}, ) app.exception_handler(Exception) async def fallback_handler(request: Request, exc: Exception): import logging logging.getLogger(app).exception(未处理异常) return JSONResponse( status_code500, content{code: 500, message: 服务内部错误}, )全局异常处理器统一了错误响应格式前端不用针对不同接口解析不同的错误结构。兜底处理器只记录日志、不暴露堆栈信息。# # app/api/v1/users.py —— 路由示例 # from fastapi import APIRouter from pydantic import BaseModel, Field from app.core.deps import DB, CurrentUser from app.services.user import UserService router APIRouter(prefix/users, tags[users]) class UpdateProfileRequest(BaseModel): name: str | None Field(None, min_length1, max_length50) avatar: str | None Field(None, patternr^https?://) class ProfileResponse(BaseModel): id: str name: str avatar: str plan: str router.patch(/me, response_modelProfileResponse) async def update_profile( body: UpdateProfileRequest, db: DB, user: CurrentUser, ): if body.name is None and body.avatar is None: from app.core.exceptions import AppException raise AppException(code400400, message至少需要更新一个字段) updated await UserService.update_profile( dbdb, user_iduser[id], namebody.name, avatarbody.avatar, ) return updated路由函数只关注业务逻辑认证和数据库连接由依赖注入自动完成。四、生产部署的几个坑异步阻塞是最容易踩的坑。FastAPI 基于 ASGI 的异步模型所有 I/O 必须用 async 版本asyncpg 而非 psycopg2、httpx 而非 requests。在异步路由里调用同步阻塞函数整个事件循环会被卡住所有并发请求排队等待。一个time.sleep(1)在异步路由里100 并发下的响应时间会从 1 秒变成 100 秒。解决方案是用asyncio.to_thread()把阻塞调用推入线程池但更好的做法是源头避免阻塞。数据库连接池耗尽也很常见。默认pool_size5在并发量稍高时就会耗尽表现为请求超时和TimeoutError。合理的池大小应该基于数据库最大连接数和 ASGI worker 数量计算pool_size db_max_connections / worker_count。同时必须设置pool_recycle否则长时间空闲的连接会被数据库服务端主动断开。Pydantic v2 性能提升明显但迁移有成本。v2 基于 Rust 重写校验速度提升 5-50 倍但部分 v1 API 已废弃validator换成field_validatorclass Config换成model_config。迁移建议启用兼容模式逐步替换不要一次性重写。维度开发环境生产环境原因pool_size5CPU 核心数 × 2避免连接池耗尽pool_recycle-13600 秒避免 MySQL 8 小时断连workers1CPU 核心数充分利用多核keep_alive5 秒75 秒减少连接重建开销docs_url/docsNone生产环境关闭 Swagger五、几点经验FastAPI 的极简 API 确实降低了入门门槛但生产部署有几件事必须关注所有 I/O 必须异步连接池参数必须根据并发量调优全局异常处理器必须覆盖所有错误类型。框架的轻量不是忽视运行时行为的理由——API 表面越简洁越需要理解底层机制偏离默认行为时才能做出正确的决策。