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问题回潮。希望这篇文章能帮你少走弯路。有任何问题欢迎在评论区交流。