1. 项目概述:FastAPI 性能优化实战背景
去年接手公司电商平台的商品查询接口重构时,我遇到了典型的性能瓶颈问题。这个每天处理千万级请求的核心接口,在使用 FastAPI 重构后 QPS 始终卡在 1000 左右,完全达不到我们预期的性能指标。经过两周的深度优化,最终实现了从 1000 QPS 到 1 万 QPS 的跨越式提升。本文将完整还原这次性能优化实战的全过程。
这个商品查询接口的主要功能是根据商品 ID 返回商品详情,包括名称、价格、库存等核心信息。技术栈采用 FastAPI + MySQL + Redis 的标准组合,部署在 4 核 8G 的云服务器上。初期版本直接移植了原来 Flask 项目的同步写法,导致完全没能发挥 FastAPI 的异步性能优势。
2. 初始性能瓶颈分析
2.1 原始代码实现
初始版本的代码采用了典型的同步编程模式:
python复制@app.get("/api/goods/{goods_id}")
def get_goods(goods_id: int):
# 同步Redis查询
goods_info = redis_client.get(f"goods:{goods_id}")
if goods_info:
return json.loads(goods_info)
# 同步MySQL查询
conn = pymysql.connect(...)
cursor = conn.cursor()
cursor.execute("SELECT ... WHERE id = %s", (goods_id,))
goods = cursor.fetchone()
conn.close()
# 同步Redis更新
redis_client.setex(f"goods:{goods_id}", 3600, json.dumps(goods))
return goods
2.2 性能测试结果
使用 JMeter 进行压测(100 并发):
- QPS:980
- 平均响应时间:102ms
- 错误率:0.5%
2.3 瓶颈定位与分析
通过火焰图分析和代码审查,发现主要存在五大性能瓶颈:
- 同步数据库驱动:使用 pymysql 同步查询 MySQL,每个请求都需要建立新的 TCP 连接
- 阻塞式 Redis 操作:虽然 redis-py 使用了连接池,但同步操作会阻塞事件循环
- 线程池切换开销:使用 def 定义的同步路由,FastAPI 会启用额外线程池处理
- 连接创建开销:每次 MySQL 查询都新建连接,没有复用机制
- 缓存效率低下:热点商品每次都要访问 Redis,没有本地缓存
3. 五大核心优化策略实施
3.1 异步化改造基础
3.1.1 异步路由定义
将路由函数改为 async def 声明:
python复制@app.get("/api/goods/{goods_id}")
async def get_goods(goods_id: int): # 关键修改
...
3.1.2 异步驱动替换
- MySQL:pymysql → asyncmy
- Redis:redis-py → redis-py[asyncio]
3.1.3 异步操作关键字
所有 IO 操作添加 await:
python复制goods_info = await redis_client.get(cache_key)
conn = await asyncmy.connect(...)
await cursor.execute(sql)
优化效果:
- QPS 提升 89% → 1850
- 响应时间降低 47% → 54ms
3.2 连接池优化实践
3.2.1 MySQL 连接池配置
python复制async def create_mysql_pool():
return await asyncmy.create_pool(
host="localhost",
maxsize=10, # 重要参数
...
)
3.2.2 Redis 连接池配置
python复制redis_client = Redis(
host="localhost",
max_connections=10, # 连接池大小
...
)
3.2.3 连接池使用技巧
- 通过 FastAPI 的 Depends 管理生命周期
- 使用 async with 确保连接正确释放
- 根据服务器核心数设置 pool size(建议 CPU 核数×2)
优化效果:
- QPS 提升 73% → 3200
- 响应时间降低 43% → 31ms
3.3 多级缓存架构实现
3.3.1 本地缓存选型
采用 cachetools 的 TTLCache:
python复制from cachetools import TTLCache
local_cache = TTLCache(maxsize=10000, ttl=300)
3.3.2 缓存查询顺序
- 先查本地内存
- 再查 Redis
- 最后查 MySQL
3.3.3 缓存更新策略
- 本地缓存:同步更新
- Redis 缓存:异步更新
- 缓存失效:TTL 自动失效
优化效果:
- QPS 提升 81% → 5800
- 响应时间降低 45% → 17ms
3.4 数据库层深度优化
3.4.1 索引优化方案
- 主键索引:id(已存在)
- 新增联合索引:(category, price)
- 覆盖索引优化查询
3.4.2 SQL 优化要点
- 避免 SELECT *
- 只查询必要字段
- 使用预编译语句
3.4.3 执行计划分析
使用 EXPLAIN 验证索引效果:
sql复制EXPLAIN SELECT id,name FROM goods WHERE id=123
优化效果:
- QPS 提升 12% → 6500
- 响应时间降低 12% → 15ms
3.5 异步任务解耦
3.5.1 BackgroundTasks 使用
python复制async def update_cache(goods_id, data):
await redis_client.setex(...)
@app.get("...")
async def get_goods(..., background_tasks: BackgroundTasks):
background_tasks.add_task(update_cache, goods_id, data)
3.5.2 任务拆分原则
- 主流程:核心数据获取
- 后台任务:日志记录、缓存更新、消息通知
3.5.3 注意事项
- 任务函数必须是 async
- 避免在任务中执行耗时操作
- 需要错误处理机制
优化效果:
- QPS 提升 57% → 10200
- 响应时间降低 35% → 9.8ms
4. 性能优化效果对比
| 优化阶段 | QPS | 响应时间(ms) | 错误率 | 核心措施 |
|---|---|---|---|---|
| 初始版本 | 980 | 102 | 0.5% | 同步阻塞 |
| 异步改造 | 1850 | 54 | 0.1% | 异步路由+驱动 |
| 连接池 | 3200 | 31 | 0% | 连接复用 |
| 本地缓存 | 5800 | 17 | 0% | 多级缓存 |
| DB优化 | 6500 | 15 | 0% | 索引优化 |
| 任务解耦 | 10200 | 9.8 | 0% | 异步任务 |
5. 典型问题与解决方案
5.1 连接池参数调优
问题现象:高并发时出现"Too many connections"错误
解决方案:
- 计算公式:max_connections = (核心数 × 2) + 有效磁盘数
- 监控指标:SHOW STATUS LIKE 'Threads_connected'
5.2 缓存一致性问题
问题场景:商品价格更新后缓存未失效
解决方案:
- 双写策略:DB更新后立即删除缓存
- 设置合理的TTL
- 考虑使用Redis发布/订阅
5.3 异步上下文管理
常见错误:async with 使用不当导致连接泄漏
正确写法:
python复制async with pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(...)
6. 进阶优化方向
对于需要更高性能的场景,还可以考虑:
- 使用uvicorn的--workers参数启动多进程
- 采用JIT编译的Python实现(如PyPy)
- 热点数据预加载到内存
- 考虑使用更快的序列化方案(如MessagePack)
- 分布式缓存架构升级
在实际项目中,我们通过这五个阶段的优化,不仅显著提升了接口性能,还大幅降低了服务器资源消耗。从最初的4台服务器缩减到1台即可承载相同流量,硬件成本降低75%。这个案例充分证明了FastAPI在高并发场景下的卓越潜力,关键在于正确使用其异步特性。