性能瓶颈定位:从现象到命令的排查手册¶
压测出现「TPS 不涨 / RT 抖动 / 错误率升高」,不要先改配置。按下面顺序排除,每一步都有明确的判据。
0. 排查顺序¶
先排除压测机,否则会把压测机瓶颈当成服务端瓶颈,白改一通。
1. 先确认不是压测机的问题¶
判据:
us高且 JMeter 进程占大头 → 压测机瓶颈- 出口带宽接近 1Gbps(约 115MB/s)→ 带宽瓶颈
TIME-WAIT超过 2 万 → 端口耗尽
2. CPU:先分清 us、sy、wa¶
| 指标 | 含义 | 典型原因 |
|---|---|---|
us 高 |
用户态计算 | 序列化、正则、加解密、死循环 |
sy 高 |
内核态 | 频繁系统调用、线程切换、锁竞争 |
wa 高 |
等 IO | 磁盘慢、日志同步刷盘 |
si/so 非 0 |
使用 Swap | 内存不足,性能必然崩 |
定位到具体方法¶
# 1. 找出 CPU 最高的线程
top -Hp 12345
# 假设线程 12346
# 2. 转十六进制
printf '%x\n' 12346 # 303a
# 3. 抓栈并定位
jstack 12345 > /tmp/stack.txt
grep -A 40 "nid=0x303a" /tmp/stack.txt
连续采 3 次,如果同一方法反复出现,就是热点。
用 Arthas 更快:
java -jar arthas-boot.jar 12345
# 最忙的 3 个线程
thread -n 3
# 统计某个方法耗时分布
trace com.example.order.OrderService createOrder -n 5
# 采样 30 秒生成火焰图
profiler start
profiler stop --format html --file /tmp/cpu.html
async-profiler 也可以直接出火焰图:
锁竞争¶
sy 高且 CPU 上不去,查锁:
如果大量线程卡在同一把锁(- waiting to lock <0x...>),看持有者线程在做什么。常见于同步单例、全局计数器、synchronized 方法包了整个业务。
3. 内存:区分堆内、堆外与泄漏¶
free -m
jstat -gcutil 12345 1000 10
jcmd 12345 VM.native_memory summary # 需要启动参数 -XX:NativeMemoryTracking=summary
jstat 输出关注:
判据:
O(老年代)GC 后仍 > 80%,且持续上升 → 疑似泄漏FGC次数增长快、FGCT占比高 → 堆不足或有大对象M(Metaspace)持续涨 → 动态生成类(Groovy 脚本未缓存、CGLIB 代理过多)
找大对象:
jmap -histo:live 12345 | head -20
jcmd 12345 GC.heap_dump /tmp/heap.hprof
# 用 MAT 打开,看 Dominator Tree 与 Leak Suspects
常见泄漏源:
| 现象 | 根因 |
|---|---|
| 反复上传后 OOM | 上传流未关闭、ByteArrayOutputStream 累积 |
| 长连接越来越多 | 连接池只借不还 |
| ThreadLocal 不 remove | 线程复用导致对象被线程持有 |
| 本地缓存无上限 | HashMap 当缓存,没有淘汰策略 |
堆外内存:
# 容器内看 cgroup 限制与使用
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
# Java 直接内存
jcmd 12345 VM.native_memory detail | grep -A 5 "Internal"
Netty、gzip、NIO DirectBuffer 泄漏时,堆看起来很健康,但 RSS 一直涨,最终被 OOMKilled(dmesg 里能看到)。
4. 磁盘 IO¶
判据:
| 指标 | 阈值 | 含义 |
|---|---|---|
%util |
> 90% | 磁盘饱和 |
await |
> 20ms(SSD) | 排队严重 |
aqu-sz |
持续 > 2 | 队列积压 |
w/s 突增 |
- | 大量同步写 |
高频原因:
- 日志级别是 DEBUG,每请求写多次磁盘
- 同步刷盘(
log4j2默认immediateFlush=true) - 大文件上传/下载与业务 IO 抢同一块盘
- MySQL 慢查询导致临时表落盘
<!-- log4j2 压测时降低刷盘频率 -->
<RandomAccessFile name="File" fileName="app.log" immediateFlush="false" bufferSize="262144">
5. 网络¶
判据:
retrans/s持续 > 0.5% → 丢包/链路问题ListenOverflows/ListenDrops增长 → accept 队列满,需要调somaxconn或加实例- 大量
CLOSE_WAIT→ 应用没关连接(代码问题) - 大量
TIME_WAIT→ 短连接,启用连接复用
6. GC:RT 周期性尖刺的常见元凶¶
启动参数带 GC 日志:
分析:
# 每次 Young GC 的停顿
grep "Pause Young" gc.log | awk -F'ms' '{print $1}' | tail -20
# Full GC 次数与耗时
grep -c "Pause Full" gc.log
grep "Pause Full" gc.log | tail -5
# Humongous 分配(G1 下大对象)
grep -i "humongous" gc.log | wc -l
判据与解法:
| 现象 | 根因 | 解法 |
|---|---|---|
| Young GC 每秒多次、耗时 20-50ms | 新生代过小/分配率高 | 增大 -Xmn 或 -XX:MaxGCPauseMillis |
| Full GC 每几分钟一次 | 老年代不足或泄漏 | 先查泄漏,再调堆 |
| 单次停顿 > 500ms | 大对象晋升、Humongous | 减小批量、调整 region size |
| GC 后老年代仍 90% | 长期存活对象多 | dump 堆分析 |
RT 曲线呈锯齿、周期与 GC 周期吻合,基本可以锁定 GC。
7. 数据库¶
-- 当前正在执行的慢 SQL
SHOW FULL PROCESSLIST;
-- 锁等待
SELECT * FROM performance_schema.data_locks LIMIT 20;
SELECT * FROM sys.innodb_lock_waits;
-- 引擎状态(含最近死锁)
SHOW ENGINE INNODB STATUS\G
慢日志:
EXPLAIN 看三件事:
EXPLAIN SELECT id, amount FROM orders
WHERE user_id = 1001 AND status = 'PAID'
ORDER BY created_at DESC LIMIT 20;
type为ALL→ 全表扫描rows远大于返回行数 → 索引选择性差Extra出现Using filesort/Using temporary→ 排序或临时表
连接池(HikariCP)指标:
hikaricp_connections_active 当前占用
hikaricp_connections_pending 等待拿连接的线程数
hikaricp_connections_timeout_total 获取连接超时次数
pending 持续 > 0 且 RT 抖动,说明连接池是瓶颈,先看是不是有长事务占着连接。
8. 缓存与 MQ¶
redis-cli --latency -h 10.0.0.5
redis-cli SLOWLOG GET 10
redis-cli --bigkeys
redis-cli MEMORY USAGE user:1001
- 命中率骤降 → 缓存穿透/雪崩,回源打垮数据库
- 大 key(> 10KB 或百万元素)→ 单次操作阻塞
SLOWLOG里有KEYS *、HGETALL大 hash → 改SCAN、拆分
MQ 积压:
LAG 持续增长说明消费能力不足,压测时会被误判为“接口慢”。
9. 真实案例:RT 每 60 秒尖刺一次¶
现象:下单接口 P95 300ms,但每分钟有 3-5 秒 RT 涨到 3s。
排查路径:
iostat无异常 → 排除磁盘- GC 日志无 Full GC → 排除 GC
SHOW PROCESSLIST在尖刺时刻抓到一条UPDATE inventory SET ... WHERE sku_id IN (...)大批量更新- 定时任务每分钟执行一次,锁住了库存行
- 解法:定时任务改为分批(每批 500 行)+ 错峰执行
结论:压测的 RT 异常要先对齐时间轴,把 RT 曲线与 GC、慢 SQL、定时任务的时间点叠在一起看。
10. 一页排查表¶
| 症状 | 第一怀疑 | 第一条命令 |
|---|---|---|
| CPU 100%,TPS 不涨 | 热方法/锁竞争 | top -Hp + jstack |
| CPU 不高,TPS 低 | IO 等待/下游超时 | iostat -x、pidstat -d |
| 内存持续涨 | 泄漏/缓存无界 | jstat -gcutil、jmap -histo |
| RT 周期尖刺 | GC/定时任务 | GC 日志、PROCESSLIST |
| 错误率随并发上升 | 连接池满/限流 | Hikari 指标、网关日志 |
| 偶发超时 | 重传/丢包 | sar -n ETCP、ss -s |