Python FastAPI性能优化实战:8个让你的API快3倍的技巧

Python FastAPI性能优化实战:8个让你的API快3倍的技巧

FastAPI很火,但很多项目上线后才发现------慢。

不是FastAPI本身慢,而是用的人没注意那些隐形的性能杀手。数据库连接没池化、同步操作阻塞事件循环、N+1查询满天飞......

这篇文章不讲理论,只讲我在线上项目里踩过的坑和对应的解决方案,每个都有压测数据对比。

一、数据库连接池:第一个就该优化的

1.1 问题

最常见的新手错误:每个请求都创建新的数据库连接。

python 复制代码
# ❌ 每个请求创建连接
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # 每次请求都新建连接,TCP三次握手+SSL+认证
    engine = create_engine("postgresql://user:pass@localhost/db")
    with Session(engine) as session:
        user = session.query(User).get(user_id)
    engine.dispose()  # 用完就扔
    return user

压测结果(1000并发,获取单个用户):

makefile 复制代码
平均响应时间: 340ms
P99: 1200ms
错误率: 3.2%(连接超时)

1.2 解决方案:全局连接池

python 复制代码
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

# ✅ 应用启动时创建连接池,所有请求共享
engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/db",
    pool_size=20,          # 常驻连接数
    max_overflow=10,       # 高峰期额外连接
    pool_timeout=30,       # 等待连接超时
    pool_recycle=1800,     # 连接回收时间(秒)
    pool_pre_ping=True,    # 使用前检测连接是否存活
)

AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

# 依赖注入
async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
        finally:
            await session.close()
python 复制代码
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).where(User.id == user_id))
    return result.scalar_one_or_none()

压测对比:

makefile 复制代码
平均响应时间: 12ms(↓96%)
P99: 45ms(↓96%)
错误率: 0%

一个改动,响应时间降了28倍。

二、异步I/O:别在async函数里写同步代码

2.1 问题

FastAPI是异步框架,但很多人在async函数里调用同步阻塞操作:

python 复制代码
# ❌ 在async函数中执行同步I/O
@app.get("/process")
async def process_data():
    # 同步文件读取,阻塞整个事件循环
    with open("large_file.json", "r") as f:
        data = json.load(f)
    
    # 同步HTTP请求,阻塞事件循环
    resp = requests.get("https://api.example.com/data")
    
    # 同步数据库查询,阻塞事件循环
    result = sync_db.query(Model).all()
    
    return {"status": "done"}

问题:一个请求的同步操作阻塞了整个事件循环,所有其他请求都排队等待

2.2 解决方案

方案A:用async版本的库

python 复制代码
# ✅ 使用异步库
import aiofiles
import httpx

@app.get("/process")
async def process_data():
    # 异步文件读取
    async with aiofiles.open("large_file.json", "r") as f:
        data = json.loads(await f.read())
    
    # 异步HTTP请求
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://api.example.com/data")
    
    return {"status": "done"}

方案B:无法异步的操作丢到线程池

python 复制代码
import asyncio
from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=4)

@app.get("/process")
async def process_data():
    loop = asyncio.get_event_loop()
    
    # 把同步操作丢到线程池,不阻塞事件循环
    data = await loop.run_in_executor(
        executor, 
        lambda: json.load(open("large_file.json"))
    )
    
    return {"status": "done", "records": len(data)}

2.3 压测对比

10个并发请求,每个请求内做一次文件读取(100ms):

makefile 复制代码
同步I/O:  总耗时 ~1000ms(串行等待)
异步I/O:  总耗时 ~110ms(并行处理)
线程池:    总耗时 ~300ms(4线程并行+上下文切换)

三、N+1查询:数据库性能的头号杀手

3.1 问题

python 复制代码
# ❌ N+1查询:先查列表,再逐条查关联
@app.get("/articles")
async def list_articles(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Article))
    articles = result.scalars().all()
    
    # 每篇文章都要额外查一次作者信息
    for article in articles:
        # 20篇文章 = 1 + 20 = 21条SQL
        await db.refresh(article, ["author"])
    
    return [
        {"title": a.title, "author": a.author.name}
        for a in articles
    ]

3.2 解决方案:joinedload / selectinload

python 复制代码
from sqlalchemy.orm import selectinload, joinedload

# ✅ 方案1:JOIN查询(适合一对一、数据量小的关联)
@app.get("/articles")
async def list_articles(db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        select(Article).options(joinedload(Article.author))
    )
    articles = result.unique().scalars().all()
    
    return [
        {"title": a.title, "author": a.author.name}
        for a in articles
    ]
    # 只执行1条SQL,JOIN一次搞定

# ✅ 方案2:IN查询(适合一对多、数据量大的关联)
@app.get("/articles")
async def list_articles_with_comments(db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        select(Article).options(
            selectinload(Article.author),
            selectinload(Article.comments)
        )
    )
    articles = result.unique().scalars().all()
    
    return [
        {
            "title": a.title,
            "author": a.author.name,
            "comment_count": len(a.comments)
        }
        for a in articles
    ]
    # 3条SQL(文章+作者IN+评论IN),而非1+N+N

3.3 性能对比

20篇文章,每篇5条评论:

makefile 复制代码
N+1查询:     21条SQL,平均 180ms
joinedload:   1条SQL,平均 12ms(↓93%)
selectinload: 3条SQL,平均 18ms(↓90%)

四、响应序列化:别用dict手动拼

4.1 问题

python 复制代码
# ❌ 手动拼dict,慢且容易漏字段
@app.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User))
    users = result.scalars().all()
    
    return [
        {
            "id": u.id,
            "name": u.name,
            "email": u.email,
            "created_at": u.created_at.isoformat(),  # 容易忘
            # 20个字段就要写20行...
        }
        for u in users
    ]

4.2 解决方案:Pydantic response_model

python 复制代码
from pydantic import BaseModel
from datetime import datetime

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    created_at: datetime
    
    class Config:
        from_attributes = True  # Pydantic v2

@app.get("/users", response_model=list[UserResponse])
async def list_users(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User))
    return result.scalars().all()
    # Pydantic自动序列化,FastAPI自动缓存schema

好处:

  • 序列化更快:Pydantic v2用Rust核心,比手拼dict快2-3倍
  • 自动文档:Swagger UI自动生成响应结构
  • 字段过滤:不会意外暴露password等敏感字段

五、缓存:热数据别反复查库

5.1 简单内存缓存

python 复制代码
from functools import lru_cache
from datetime import datetime

# ✅ 配置项等很少变化的数据,内存缓存即可
@lru_cache(maxsize=128)
def get_system_config(key: str) -> str:
    """系统配置缓存,进程内有效"""
    return load_from_db(key)

# 需要手动失效
def update_system_config(key: str, value: str):
    save_to_db(key, value)
    get_system_config.cache_clear()

5.2 Redis缓存

python 复制代码
import redis.asyncio as redis
import json
from typing import Optional

redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)

async def get_cached(key: str, ttl: int = 300) -> Optional[dict]:
    """从Redis获取缓存"""
    data = await redis_client.get(key)
    return json.loads(data) if data else None

async def set_cached(key: str, value: dict, ttl: int = 300):
    """写入Redis缓存"""
    await redis_client.setex(key, ttl, json.dumps(value, ensure_ascii=False))

# 使用
@app.get("/hot-articles")
async def hot_articles(db: AsyncSession = Depends(get_db)):
    cache_key = "hot_articles:page1"
    
    # 先查缓存
    cached = await get_cached(cache_key)
    if cached:
        return cached
    
    # 缓存没有,查库
    result = await db.execute(
        select(Article)
        .where(Article.is_published == True)
        .order_by(Article.view_count.desc())
        .limit(20)
    )
    articles = [{"title": a.title, "views": a.view_count} for a in result.scalars().all()]
    
    # 写入缓存,5分钟过期
    await set_cached(cache_key, articles, ttl=300)
    
    return articles

5.3 压测对比

热门文章接口(1000并发):

makefile 复制代码
无缓存:    平均 85ms,数据库CPU 80%
Redis缓存: 平均 3ms,数据库CPU 5%(↓96%)

六、分页优化:别用OFFSET

6.1 问题

python 复制代码
# ❌ OFFSET分页:翻到第100页需要扫描前9900条
@app.get("/articles")
async def list_articles(page: int = 1, size: int = 20):
    offset = (page - 1) * size
    result = await db.execute(
        select(Article).offset(offset).limit(size)
    )
    return result.scalars().all()

问题:OFFSET 99000 LIMIT 20 数据库要先扫描99000条再取20条,越往后越慢。

6.2 解决方案:游标分页(Cursor-based Pagination)

python 复制代码
# ✅ 基于ID游标的分页:无论第几页,只扫描20条
@app.get("/articles")
async def list_articles(
    cursor: Optional[int] = None,  # 上一页最后一条的ID
    size: int = 20,
    db: AsyncSession = Depends(get_db)
):
    query = select(Article).order_by(Article.id.desc()).limit(size + 1)
    
    if cursor:
        query = query.where(Article.id < cursor)
    
    result = await db.execute(query)
    articles = result.scalars().all()
    
    has_more = len(articles) > size
    if has_more:
        articles = articles[:size]
    
    return {
        "data": articles,
        "next_cursor": articles[-1].id if articles and has_more else None,
        "has_more": has_more
    }

6.3 性能对比

100万条数据,每页20条:

makefile 复制代码
OFFSET第5000页: 320ms(扫描100000行)
游标第5000页:    2ms(只扫描20行)

七、中间件优化:减少请求处理管道

7.1 问题

加了太多中间件,每个请求都要过一遍:

python 复制代码
# ❌ 每个请求都过所有中间件,即使不需要
app.add_middleware(LoggingMiddleware)       # 日志
app.add_middleware(CORSMiddleware, ...)      # CORS
app.add_middleware(RateLimitMiddleware)      # 限流
app.add_middleware(AuthMiddleware)           # 认证
app.add_middleware(TracingMiddleware)        # 链路追踪

每个中间件增加0.5-2ms,5个就是2.5-10ms的额外延迟。

7.2 解决方案:按路由选择中间件

python 复制代码
from fastapi import FastAPI, Request
from starlette.routing import Match

# 只对需要认证的路由做认证检查
@app.middleware("http")
async def smart_middleware(request: Request, call_next):
    path = request.url.path
    
    # 健康检查和静态资源跳过所有中间件
    if path in ("/health", "/metrics") or path.startswith("/static/"):
        return await call_next(request)
    
    # 只对/api/开头的路由做认证
    if path.startswith("/api/"):
        token = request.headers.get("Authorization")
        if not token or not verify_token(token):
            return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
    
    # 只对写操作做限流
    if request.method in ("POST", "PUT", "DELETE"):
        if await is_rate_limited(request):
            return JSONResponse(status_code=429, content={"detail": "Too many requests"})
    
    return await call_next(request)

八、应用启动优化:别在启动时做重活

8.1 问题

python 复制代码
# ❌ 启动时加载大量数据到内存
@app.on_event("startup")
async def startup():
    global ALL_CONFIGS, ALL_CATEGORIES, ALL_PERMISSIONS
    ALL_CONFIGS = await load_all_configs()         # 2秒
    ALL_CATEGORIES = await load_all_categories()     # 1秒
    ALL_PERMISSIONS = await load_all_permissions()   # 3秒
    # 启动耗时6秒,部署滚动更新时服务中断

8.2 解决方案:延迟加载

python 复制代码
from functools import lru_cache

class LazyLoader:
    """延迟加载器,首次访问时才初始化"""
    
    def __init__(self):
        self._configs = None
        self._categories = None
        self._permissions = None
        self._loaded = False
    
    async def ensure_loaded(self):
        if self._loaded:
            return
        
        # 并行加载
        import asyncio
        self._configs, self._categories, self._permissions = await asyncio.gather(
            load_all_configs(),
            load_all_categories(),
            load_all_permissions(),
        )
        self._loaded = True
    
    @property
    async def configs(self):
        await self.ensure_loaded()
        return self._configs

loader = LazyLoader()

@app.on_event("startup")
async def startup():
    # 启动时什么都不加载
    pass

@app.get("/configs")
async def get_configs():
    # 首次请求时才加载
    return await loader.configs

启动耗时从6秒降到0秒,首次请求耗时略增(但后续请求不受影响)。

总结

优化项 影响程度 实现难度 优先级
数据库连接池 ⭐⭐⭐⭐⭐ 简单 🔴 最高
N+1查询 ⭐⭐⭐⭐⭐ 简单 🔴 最高
异步I/O ⭐⭐⭐⭐ 中等 🟠 高
缓存 ⭐⭐⭐⭐ 简单 🟠 高
游标分页 ⭐⭐⭐ 简单 🟡 中
Pydantic序列化 ⭐⭐ 简单 🟢 低
中间件优化 ⭐⭐ 中等 🟢 低
启动优化 简单 🟢 低

实施建议:先做连接池和N+1查询,立竿见影;再做异步和缓存;其他的按需优化。

有问题欢迎评论区讨论 👇

相关推荐
阿里嘎多学长43 分钟前
2026-07-22 GitHub 热点项目精选
开发语言·程序员·github·代码托管
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端