def serialize_article(a: Article) -> dict:
    return {
        "id": a.id,
        "title": a.title,
        "view_count": a.view_count,
        "author": {"id": a.author.id, "name": a.author.name},
        "tags": [{"id": t.id, "name": t.name} for t in a.tags],
    }

这样缓存大小从平均8KB降到800B,序列化时间降了70%。

5.3 坑3:连接池配置不当导致连接风暴

现象:压测200并发时,数据库报 FATAL: remaining connection slots are reserved

原因:FastAPI是异步框架,但SQLAlchemy用的是同步session,每个请求占用一个连接,默认 pool_size=5 直接被打爆。

解法(关键配置):

engine = create_engine(
    DATABASE_URL,
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True,
    pool_recycle=3600,
)

6. 效果数据:用数字说话

6.1 压测对比(wrk 8线程 200连接 30秒)

指标 优化前 优化后 提升
P50 延迟 389ms 28ms 13.9x
P95 延迟 789ms 38ms 20.8x
P99 延迟 1.12s 52ms 21.5x
QPS 2,291 10,876 4.7x
平均每秒SQL次数 2,842 1,208 57%↓
Redis缓存命中率 32% 94% 62%↑

6.2 压测输出(优化后)

Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    28.42ms   12.87ms 188.00ms   81.33%
    Req/Sec   1,359.55  132.45   1,842.00   69.10%

Latency Distribution
   50%   28.00ms
   75%   33.00ms
   90%   38.00ms
   99%   52.00ms

6.3 生产环境验证

上线后观察24小时:
- 数据库CPU:85% → 22%
- Redis内存峰值:1.8GB → 450MB(因为只缓存必要字段)
- P95延迟:1.2s → 45ms(网络延迟差异)
- 节省了2个Pod资源(从4副本缩到2副本)

7. 总结与建议

核心收获
1. 先profiling再优化,不要凭感觉加索引或缓存。cProfile + 慢查询日志是最快的定位手段。
2. ORM懒加载是性能杀手,默认 lazy="select" 在列表场景必现N+1。建议默认用 lazy="selectin"lazy="joined"(但要小心多对多笛卡尔积)。
3. 缓存不是银弹,命中率低于50%时先查数据访问层,可能是缓存key设计不合理或数据变动太频繁。
4. 连接池参数必须压测验证pool_size=20, max_overflow=10 的配置更激进,但配合 pool_pre_ping=True 能避免失效连接。

额外建议
- 如果使用Flask,同样的思路适用,但注意Flask同步框架天然并发低,建议先迁移到FastAPI或至少用gunicorn+gevent。
- 生产环境用 py-spy dump 而非 cProfile,避免性能影响。
- Redis缓存建议加一层本地内存缓存(如 cachetools),能再降10ms延迟。

调优是个持续过程。这次优化后,我们建立了性能回归测试(wrk压测脚本集成到CI),每次发版前自动跑一遍,防止N+1问题回潮。希望这篇文章能帮你少走弯路。有任何问题欢迎在评论区交流。