跳转至

性能瓶颈定位:从现象到命令的排查手册

压测出现「TPS 不涨 / RT 抖动 / 错误率升高」,不要先改配置。按下面顺序排除,每一步都有明确的判据。

0. 排查顺序

压测机 → 网络 → 应用资源(CPU/内存/IO)→ JVM(GC/线程)→ 数据库 → 缓存/MQ → 下游

先排除压测机,否则会把压测机瓶颈当成服务端瓶颈,白改一通。

1. 先确认不是压测机的问题

# 压测机 CPU / 内存 / 网络
top -b -n1 | head -15
sar -n DEV 1 5 | grep -E "eth0|IFACE"
ss -s | head -5

判据:

  • us 高且 JMeter 进程占大头 → 压测机瓶颈
  • 出口带宽接近 1Gbps(约 115MB/s)→ 带宽瓶颈
  • TIME-WAIT 超过 2 万 → 端口耗尽

2. CPU:先分清 us、sy、wa

top -Hp 12345          # 进程内哪个线程吃 CPU
pidstat -u -t 1 5      # 线程级 CPU
vmstat 1 5             # r 列 > CPU核数 → 排队
指标 含义 典型原因
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 也可以直接出火焰图:

./profiler.sh -d 30 -e cpu -f /tmp/cpu.html 12345

锁竞争

sy 高且 CPU 上不去,查锁:

jstack 12345 | grep -A 20 "BLOCKED"
jstack 12345 | grep -c "java.lang.Thread.State: BLOCKED"

如果大量线程卡在同一把锁(- waiting to lock <0x...>),看持有者线程在做什么。常见于同步单例、全局计数器、synchronized 方法包了整个业务。

3. 内存:区分堆内、堆外与泄漏

free -m
jstat -gcutil 12345 1000 10
jcmd 12345 VM.native_memory summary    # 需要启动参数 -XX:NativeMemoryTracking=summary

jstat 输出关注:

  S0     S1     E      O      M     CCS    YGC   YGCT    FGC    FGCT     GCT
  0.00  62.50  78.20  45.10  92.30  88.10   120   2.10     3    0.85    2.95

判据:

  • 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

iostat -x 1 5
iotop -o -P
pidstat -d 1 5

判据:

指标 阈值 含义
%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. 网络

sar -n DEV 1 5           # 带宽
sar -n TCP,ETCP 1 5      # 重传、连接数
ss -lnt                  # 监听队列
ss -s                    # 状态汇总

判据:

  • retrans/s 持续 > 0.5% → 丢包/链路问题
  • ListenOverflows / ListenDrops 增长 → accept 队列满,需要调 somaxconn 或加实例
  • 大量 CLOSE_WAIT → 应用没关连接(代码问题)
  • 大量 TIME_WAIT → 短连接,启用连接复用

6. GC:RT 周期性尖刺的常见元凶

启动参数带 GC 日志:

-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=50M

分析:

# 每次 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

慢日志:

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;
pt-query-digest /var/log/mysql/slow.log | head -40

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 积压:

kafka-consumer-groups.sh --bootstrap-server broker:9092 \
  --describe --group order-consumer

LAG 持续增长说明消费能力不足,压测时会被误判为“接口慢”。

9. 真实案例:RT 每 60 秒尖刺一次

现象:下单接口 P95 300ms,但每分钟有 3-5 秒 RT 涨到 3s。

排查路径:

  1. iostat 无异常 → 排除磁盘
  2. GC 日志无 Full GC → 排除 GC
  3. SHOW PROCESSLIST 在尖刺时刻抓到一条 UPDATE inventory SET ... WHERE sku_id IN (...) 大批量更新
  4. 定时任务每分钟执行一次,锁住了库存行
  5. 解法:定时任务改为分批(每批 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

相关阅读