做API开发的这些年,我一直有个执念:Python后端写起来是真爽,但每回讨论“高性能”时总有点抬不起头。FastAPI出来之后,情况其实已经变了,但很多人对它的认知还停留在“自动生成文档很方便”这个层面。我这段时间正好把一个内部任务协同工具的后端从零搭起来,要让Web端和小程序共用一套API,要处理JWT鉴权、任务增删改查、标签筛选、热点数据缓存,还得扛住整个部门上午集中建任务、下班前集中查状态的流量。选型时没有任何犹豫,直接用FastAPI。这篇文章就是这次实践从开发到压测、再到部署的完整记录。文中涉及的所有性能结论,不是背参数,而是在一台4核8G的Linux云主机上反复跑wrk跑出来的,环境不同数据会有浮动,但趋势和排查思路是通用的。
要说清楚FastAPI的“高性能”,我们先别急着写接口,先把它的底裤看清楚。否则后面遇到瓶颈,你根本不知道问题出在哪一层。
1. FastAPI 的性能不是玄学:先看三个底层机制
1.1 ASGI和事件循环:Python并发的翻身仗
很多人对比框架时还在比较“Flask跑一个空路由要多少毫秒”,这个方向就错了。Flask这类WSGI框架,每个请求进来后,是交给一个工作线程去处理的。线程在处理数据库查询或外部HTTP调用时,只能干等着,这段时间线程被白白占住。而FastAPI跑在ASGI标准上,底层是Starlette,整个服务跑在一个事件循环里。遇到await,解释器就会去处理其他连接,等数据库响应回来了,再回来继续往下走。
打个比方,Flask的并发模型像银行柜台,每个客户进门就分配一个柜员,柜员办业务时其他人都等着;FastAPI更像医院叫号系统,一个分诊台同时接待几百个号,你去拍片、抽血的间隙,分诊台已经在处理下一个病人了。只要业务是IO密集型——查数据库、调Redis、请求第三方接口——这种模型就能用很小的线程开销扛住很大的并发连接。
1.2 Pydantic v2的Rust核心:校验不再白给
FastAPI另一个容易被人忽略的提速点是Pydantic。老项目里如果直接从0.9x版本升级上来会明显感觉到:现在这套Pydantic v2不再是纯Python实现,核心校验逻辑已经用Rust重写了,性能比v1时代提升5到50倍。
为什么要关心这个?因为FastAPI的请求解析、参数校验、响应序列化,每一步都在Pydantic里进出。一个业务接口如果请求体有20个字段,响应又是一个嵌套列表,这些工作在旧版里是纯Python逐个字段判断,放在高并发下就是实打实的CPU开销。我自己有个感受:老项目从Pydantic v1迁到v2之后,接口延迟没怎么变,但压力测试时CPU占用率掉下去一大截。FastAPI从某个版本开始就默认搭配Pydantic v2了,它是在帮你把序列化这层隐藏成本填平。
1.3 别把async def用成普通函数
FastAPI有个很贴心的设计:如果你把端点定义成普通def而不是async def,它不会让你报错,而是自动把这个函数扔进线程池去执行。这个设计初衷是好的,让你可以在端点里跑同步代码,但它有个副作用——很多新手因此把所有路由都写成普通def,等于把FastAPI的异步优势主动废掉了。
线程池默认大小是40,一旦有几十个请求同时卡在同步数据库查询或同步HTTP请求上,后面的同步请求就得排队。我的经验是:能用异步库的地方,路由就定义成async def。数据库用asyncpg或aiomysql,HTTP调用用httpx.AsyncClient,Redis用redis.asyncio;实在跑不了异步的老代码,才用同步def,而且最好让FastAPI明确知道这是一个会阻塞线程池的任务。压测时最容易翻车的点就在这里,不是框架慢,是你把异步框架当同步框架用了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工程底座:目录划分、配置管理与环境隔离
2.1 目录结构直接决定后面能不能撑住
我见过太多FastAPI项目的代码是“单文件膨胀型”:main.py从200行写到2000行,路由、模型、业务逻辑全黏在一起。一旦接口超过20个,这种项目基本就失去维护价值了。这次我用的目录结构经过好几轮项目验证,大家可以直接照搬:
text复制app/
├── main.py # 应用入口,注册路由、中间件
├── core/
│ ├── config.py # pydantic-settings 配置
│ └── security.py # JWT工具、密码散列
├── db.py # 异步引擎、Session工厂
├── models/ # SQLAlchemy ORM模型
│ └── task.py
├── schemas/ # Pydantic模型(请求/响应)
│ └── task.py
├── api/
│ ├── router.py # 总路由
│ └── v1/
│ ├── tasks.py # 任务模块路由
│ └── auth.py # 登录/令牌路由
└── services/ # 业务逻辑层
└── task_service.py
按模块切分,而不是按“models.py、schemas.py”这种类型切分,这是关键。一个任务模块涉及路由、模型、Schema、Service,它们应该聚在一起;项目大了之后,按类型堆文件会让你在五个目录之间来回跳。
2.2 配置用 pydantic-settings 管起来
配置管理是我早期项目里最随意的地方——DATABASE_URL直接写在代码里,密钥也存在同一个文件里。现在有pydantic-settings,配合.env文件,一劳永逸:
python复制from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "task-api"
debug: bool = False
api_prefix: str = "/api/v1"
