07 · 服务端性能与容量:定位瓶颈的方法
性能优化的第一步不是"快",是"找到慢在哪"。本篇讲后端定位瓶颈的通用方法论:从经验直觉到实测。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 后端性能问题怎么定位:先分层测,再对症下药 |
| 核心内容 | 性能优化的正确顺序、连接池/线程模型、数据库慢查询、缓存、压测、容量估算 |
| 读完能回答 | 服务变慢先查哪?数据库慢查询怎么定位?压测怎么看结果? |
| 相关板块 | C10 · 数据建模 | C13 · 缓存与 Redis | C08 · 可观测性 |
一、优化的正确顺序
先测后优化,一次只改一个变量。 90% 的"性能差"不是语言慢,而是:
| 常见瓶颈 | 优先级 |
|---|---|
| 数据库慢查询 / 缺索引 | 最先查 |
| N+1 查询 | 高 |
| 无缓存的热点查询 | 高 |
| 连接池耗尽 | 中 |
| 阻塞调用卡事件循环 | 中 |
| 语言本身 | 最低(几乎不是瓶颈) |
铁律:没有压测/数据支撑的"优化"是玄学。先量化,再动手。
二、连接池与线程模型
2.1 连接池耗尽
症状:请求超时、日志出现"获取连接超时"。
排查:连接池大小 vs 数据库 max_connections,是否泄漏(连接未归还)。
python
# 连接池参数(SQLAlchemy)
create_engine(url, pool_size=10, max_overflow=20, pool_timeout=30)2.2 线程/进程模型
| 运行时 | 模型 | 注意事项 |
|---|---|---|
| Node | 单线程事件循环 | 阻塞调用全卡 |
| Python (asyncio) | 事件循环 + 线程池 | async 里别调阻塞库 |
| Go | goroutine | 量大但省资源 |
对 FastAPI:
async def里调阻塞库 = 卡死(见 C09)。普通def路由自动进线程池。
三、数据库慢查询:第一排查对象
sql
-- 1. 看执行计划:走没走索引
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42;
-- 2. 查慢查询日志
-- PostgreSQL: log_min_duration_statement / pg_stat_statements| 现象 | 常见原因 | 解法 |
|---|---|---|
| 全表扫描 | 缺索引 / 写法让索引失效 | 加索引、改写法 |
| N+1 | 循环里逐条查 | 批量查询(IN / join) |
| 排序慢 | 大结果集无索引 | 排序字段加索引 |
| 锁等待 | 事务太长 | 缩短事务、控制并发 |
N+1:查了 100 个用户,每个又查一次订单 = 101 次查询。改成一次
WHERE user_id IN (...)。
四、缓存:性能的第二板斧
先查库再谈缓存。热点读 + 一致性可接受 → 加 缓存。顺序:
text
① 加索引 / 优化 SQL → ② 加缓存(先防穿透/击穿/雪崩) → ③ 再谈架构层五、压测:用数据说话
5.1 工具
| 工具 | 用途 |
|---|---|
wrk / ab | 简单 HTTP 压测 |
k6 | 场景化压测、门槛低 |
| Locust | Python、分布式压测 |
5.2 看什么指标
| 指标 | 含义 | 关注 |
|---|---|---|
| QPS | 每秒请求数 | 上限 |
| RT(P95/P99) | 响应时间分位 | 尾巴有多长 |
| 错误率 | 5xx/超时占比 | 必须低 |
| 资源 | CPU/内存/连接 | 谁先到瓶颈 |
bash
wrk -t4 -c100 -d30s http://localhost:8000/items重点看 P99 而不是平均值:平均好看、尾巴很长的系统线上体验很差。
六、容量估算
text
需要 QPS = 峰值并发 × 单请求耗时 / 目标利用率
实例数 = 需要 QPS / 单实例压测 QPS给"峰值 × 1.5 余量",别按平均算。压测出来的单实例 QPS 是估算的基础,不是拍脑袋。
自检清单
- [ ] 能按"先数据库 → 再缓存 → 再架构"的顺序排查
- [ ] 会用
EXPLAIN ANALYZE定位慢查询,识别 N+1 - [ ] 知道连接池耗尽的症状与排查
- [ ] 会用 wrk/k6 压测,并看 QPS/P99/错误率
- [ ] 会做简单的容量估算
常见误区
- 误区:性能差先换语言/框架。 绝大多数瓶颈在数据库和 IO,不是语言。
- 误区:看平均响应时间。 要看 P99,尾巴才是真实体验。
- 误区:加缓存一定变快。 命中率低、一致性差的缓存是负优化。
- 误区:没压测就"优化"。 没有数据支撑的优化是玄学。
动手练习
练习 A:定位一次慢查询
构造一个无索引的查询,用 EXPLAIN ANALYZE 看到全表扫描,加索引后再对比。
练习 B:修 N+1
写一个循环查订单的接口,改成批量查询,压测对比 QPS 和 P99。
练习 C:压测报告
对你的服务做一次 wrk 压测,记录 QPS、P99、错误率,写一页容量估算。
相关文档
本区
- 10 · 数据建模与数据访问 —— 索引与查询
- 13 · 缓存与 Redis —— 缓存优化
- 08 · 可观测性 —— 怎么监控到慢
其他域
- 10 · Web 性能优化 —— 性能思维的跨端对照