排查 Spring Boot 线上项目 CPU 100% 问题是一个系统性的工作,需要结合多种工具和方法。以下是一个详细的排查指南:
核心原则:
- 安全第一: 优先使用对线上服务影响最小的诊断工具(如
top
,jstack
)。避免在业务高峰或没有预案的情况下执行可能导致服务暂停的操作(如jmap -dump
)。 - 保留现场: 在采取任何可能改变进程状态的操作(如重启)之前,务必先收集关键诊断信息(线程栈、GC日志、内存快照)。
- 循序渐进: 从宏观到微观,从系统层到应用层,逐步缩小问题范围。
- 结合日志: 应用日志、GC日志是重要的辅助信息源。
排查步骤:
第一阶段:快速定位问题进程和线程 (宏观定位)
-
确认 CPU 使用率:
- 使用
top
命令(Linux)或任务管理器(Windows)查看整体 CPU 使用率,确认确实是你的 Java 进程(通常是java
或包含你应用名的进程)占用了接近 100% 的 CPU。 - 记录进程 PID。
- 使用
-
定位消耗 CPU 的线程:
- Linux:
top -H -p <PID>
: 显示指定进程内所有线程的 CPU 使用情况。按P
(大写) 可以按 CPU 使用率排序。找到持续占用 CPU 最高的几个线程的 PID (此时是线程 ID,通常显示为十进制数)。- 将找到的高 CPU 线程 ID (十进制) 转换为十六进制:
printf "%x\n" <线程ID>
。这个十六进制值后面在jstack
输出中查找线程栈时要用到。
- Windows:
- 使用
Process Explorer
(Sysinternals 工具集) 或jconsole
/VisualVM
连接到目标 JVM 进程,查看线程列表和 CPU 使用情况。
- 使用
- Linux:
第二阶段:分析线程堆栈 (微观分析 - 关键步骤)
-
获取线程堆栈快照:
- 使用
jstack
工具(JDK 自带)获取当前 JVM 进程的线程堆栈信息:jstack <PID> > jstack_dump.txt
- 强烈建议在短时间内(比如间隔 5-10 秒)连续获取 3-5 次堆栈快照。 这有助于区分是某个线程持续高负载,还是多个线程轮番占用 CPU。
- 使用
-
分析堆栈快照:
- 打开
jstack_dump.txt
文件。 - 根据第一阶段找到的高 CPU 线程的 十六进制 ID,在堆栈文件中搜索
nid=0x<十六进制线程ID>
。 - 仔细查看这些高 CPU 线程的堆栈信息 (
"Thread Stack"
部分)。这是定位问题的关键! - 重点关注:
- 线程状态: 是
RUNNABLE
(正在执行) 吗?如果是,它在执行什么代码?如果是BLOCKED
或WAITING
,虽然不直接消耗 CPU,但可能是锁竞争导致其他线程忙等(自旋锁)的根源。 - 执行代码: 堆栈顶部的类和方法是什么?这是线程当前正在执行的操作。
- 重复模式: 在多次堆栈快照中,同一个线程是否总是在执行相同的几个方法?或者多个不同的线程都在执行类似的方法?
- 锁信息: 注意
- waiting to lock <0x000000076bf62200> (a java.lang.Object)
和- locked <0x000000076bf62200> (a java.lang.Object)
这样的信息,可能指示死锁或严重的锁竞争。
- 线程状态: 是
- 常见问题模式:
- 死循环: 线程堆栈显示一直在某个循环方法内部(例如
while(true)
,for(;;)
),或者在频繁轮询(如sleep
时间极短或没有sleep
)。 - 锁竞争/死锁: 多个线程在
BLOCKED
状态等待同一个锁,或者存在循环等待锁的情况(死锁)。虽然BLOCKED
线程本身不消耗 CPU,但持有锁的线程如果执行慢,或者等待线程采用忙等策略(自旋锁),都会导致 CPU 高。jstack
通常能直接检测并报告死锁。 - 密集计算: 线程在执行非常耗 CPU 的算法(如复杂数学计算、大量数据处理)。
- 无限递归: 堆栈深度异常深,且方法调用重复。
- 低效的 I/O 或网络操作: 虽然通常 I/O 等待不消耗 CPU,但如果使用的是非阻塞 I/O 且处理逻辑不当(如忙等数据就绪),或者在循环中频繁进行大量小数据量的网络/磁盘操作(上下文切换开销大),也可能导致 CPU 高。查看堆栈中是否涉及
Selector
,Channel
,InputStream/OutputStream
等操作。 - 频繁的 GC: 如果线程堆栈中有大量
GC task
线程(如GC Thread#0
,G1 Main Marker
等)处于RUNNABLE
状态,并且占比很高,说明垃圾回收非常频繁,可能是内存问题(内存泄漏或配置不当)导致的。这需要结合 GC 日志分析。
- 死循环: 线程堆栈显示一直在某个循环方法内部(例如
- 打开
第三阶段:分析内存和垃圾回收 (GC)
-
检查 GC 状态:
- 使用
jstat
工具监控 GC 情况:jstat -gcutil <PID> 1000 10
: 每 1 秒输出一次 GC 统计信息,共 10 次。关注O
(老年代使用率),E
(Eden 区使用率),YGC
(Young GC 次数),YGCT
(Young GC 时间),FGC
(Full GC 次数),FGCT
(Full GC 时间)。- 关键指标:
- 频繁的 Young GC (
YGC
快速增加): 可能对象分配过快或 Survivor 区过小。 - 频繁的 Full GC (
FGC
快速增加): 非常危险! 通常意味着老年代空间不足,可能存在内存泄漏。伴随长时间的FGCT
。 - 老年代使用率 (
O
) 持续很高或接近 100%: 是内存泄漏的强烈信号。 - GC 时间占比高 (
YGCT
+FGCT
占总运行时间的比例大): 说明 GC 是 CPU 消耗的主要来源。
- 频繁的 Young GC (
- 使用
-
检查 GC 日志 (如果配置了):
- 如果应用启动时配置了 GC 日志参数(强烈建议线上环境配置!),直接查看 GC 日志文件。
- 参数示例 (G1 GC):
-Xloggc:/path/to/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M
- 分析日志: 关注 Full GC 发生的频率、持续时间、触发原因(
Allocation Failure
,Metadata GC Threshold
,System.gc()
等)、GC 前后各代内存变化。寻找内存无法回收(内存泄漏)或 GC 效率低下的证据。
-
生成和分析堆转储 (Heap Dump - 谨慎使用):
- 如果怀疑是内存泄漏导致频繁 Full GC 进而消耗 CPU,需要分析内存中的对象。
- 生成 Heap Dump:
jmap
(可能引起短暂停顿):jmap -dump:format=b,file=heap_dump.hprof <PID>
- JVM 参数触发 (推荐): 如果配置了
-XX:+HeapDumpOnOutOfMemoryError
,在 OOM 时会自动生成 dump。也可以配置-XX:+HeapDumpBeforeFullGC
/-XX:+HeapDumpAfterFullGC
。 - 通过
jcmd
(JDK7+,比 jmap 更推荐):jcmd <PID> GC.heap_dump filename=heap_dump.hprof
- 分析 Heap Dump: 使用强大的内存分析工具:
- Eclipse MAT: 最常用,功能强大,擅长查找内存泄漏(Leak Suspects Report, Dominator Tree)。
- VisualVM: JDK 自带,基础分析。
- JProfiler / YourKit: 商业工具,功能更全面,分析效率高。
- 分析重点: 查找哪些对象占用了最多内存?是否存在预期之外的大量对象?是否存在因错误引用导致无法回收的对象(内存泄漏)?大对象?
第四阶段:系统级和外部因素排查
-
系统负载:
- 使用
vmstat 1
或mpstat -P ALL 1
查看整体 CPU 使用、上下文切换 (cs
)、中断 (in
) 等情况。高上下文切换也可能导致 CPU 利用率显示高。 - 使用
iostat -xz 1
查看磁盘 I/O 是否成为瓶颈(虽然通常 I/O 等待不直接消耗 CPU,但可能影响应用行为)。 - 使用
netstat -antp | grep <PID>
或ss -antp | grep <PID>
查看网络连接数、状态。大量连接或特定状态(如CLOSE_WAIT
)可能指示问题。
- 使用
-
外部依赖:
- 高 CPU 是否发生在调用特定外部服务(数据库、Redis、RPC)之后?检查这些服务的状态和响应时间。
- 分析应用日志,是否有大量超时、错误?是否有循环重试逻辑?
-
配置与资源:
- 检查应用 JVM 参数(
-Xmx
,-Xms
, GC 选择等)是否合理?堆是否设置过小导致频繁 GC? - 检查服务器资源(CPU 核数、内存)是否足够?是否被其他进程抢占资源?
- 检查应用 JVM 参数(
第五阶段:结合应用日志和代码分析
-
审查应用日志:
- 在 CPU 飙升的时间段前后,仔细检查应用日志(业务日志、框架日志如 Spring、Tomcat)。寻找错误、异常、警告信息,或者大量重复的操作日志。
- 是否有特定的请求模式(如某个接口被疯狂调用)?
-
代码审查:
- 根据线程堆栈分析指向的代码位置,结合日志信息,审查相关代码逻辑。
- 重点检查:
- 高 CPU 线程执行的方法。
- 循环逻辑(是否有退出条件?循环体内的操作是否很重?)。
- 同步锁(
synchronized
,ReentrantLock
)的使用范围和时间。是否锁住了大块代码或耗时操作? - 算法复杂度(是否存在 O(n^2) 或更糟的算法处理大量数据?)。
- 第三方库或框架的使用是否存在已知的性能问题或不当使用。
总结与解决
- 综合以上步骤收集的信息,确定根本原因:
- 代码 Bug (死循环、低效算法、锁竞争)。
- 内存问题 (内存泄漏导致频繁 Full GC)。
- 资源不足 (JVM 堆大小、服务器 CPU)。
- 外部依赖故障。
- 配置不当 (GC 策略、线程池大小)。
- 根据原因制定解决方案:
- 修复代码 Bug。
- 优化内存使用,修复内存泄漏。
- 调整 JVM 参数(增大堆、选择合适的 GC)。
- 扩容服务器资源。
- 优化外部依赖调用或解决依赖方问题。
- 优化配置。
预防措施:
- 监控告警: 部署完善的监控系统(如 Prometheus + Grafana, Zabbix, 商业 APM),监控 CPU、内存、GC、线程池、关键接口响应时间等指标,设置阈值告警。
- GC 日志: 务必 在线上环境开启 GC 日志。
- Heap Dump 配置: 配置
-XX:+HeapDumpOnOutOfMemoryError
。 - 性能测试: 上线前进行充分的压力测试和性能测试。
- 代码审查: 关注性能敏感代码。
- 限流熔断: 对核心接口实施限流和熔断机制,防止雪崩效应。
工具链总结:
- 系统监控:
top
,vmstat
,mpstat
,iostat
,netstat
/ss
- JVM 线程:
jstack
,jcmd Thread.print
- JVM 内存/GC:
jstat
,jmap
,jcmd GC.heap_info
(基础信息) - Heap Dump 分析: Eclipse MAT, VisualVM, JProfiler, YourKit
- 综合监控/分析:
jconsole
,VisualVM
(GUI), 商业 APM (Application Performance Monitoring) 工具(如 Dynatrace, AppDynamics, New Relic, SkyWalking, Pinpoint)
遵循这个指南,结合耐心和细致的分析,通常能够定位并解决 Spring Boot 应用 CPU 100% 的问题。记住,保留现场信息是第一要务!