前言:线上血雨腥风,记一次惊心动魄的 CPU 爆表 💥
对于我们这些 Java 后端攻城狮来说,线上的世界从来都不是风平浪静。各种妖魔鬼怪层出不穷,CPU 突然像脱缰的野马一样狂飙,内存一不小心就给你 OutOfMemoryError 的“惊喜”,还有那频繁得让人怀疑人生的 GC… 相信各位或多或少都经历过线上事故的“洗礼”。
今天,我想跟大家聊聊线上问题排查的那些“基本操作”。你可能会觉得“基本”嘛,谁还不会 top 和 jstack?但就像武侠小说里的基本功一样,往往最基础的东西才是解决复杂问题的基石。别问我为什么这么说,因为我前几天就刚经历了一场因为“基本功”扎实而化险为夷的战斗!
话说上周五,我刚泡好一杯枸杞菊花茶,准备摸鱼…哦不,是认真 review 代码的时候,突然监控群里 красные报警像雨点一样砸过来:“XX服务 CPU 使用率持续超过 90%!”
我的心头一紧,菊花茶瞬间不香了。线上 CPU 爆高,这可不是闹着玩的。赶紧放下手里的“摸鱼神器”,我开始了我的“线上救援”行动。
(接下来就可以直接切入正文的第一点,并结合故事来讲解)
1. CPU 飚高:抽丝剥茧,揪出那个“疯狂”的线程 🧵
1.1 定位“罪魁祸首”:找到高 CPU 进程
如同警察办案一样,我们首先要找到“案发现场”。我的第一反应就是登录服务器,祭出我的“老伙计”:top 命令。
在 top 的输出结果中,我迅速锁定了那个 CPU 使用率高居不下的 Java 进程,记下了它的 PID (Process ID)。
1.2 揪出“真凶”:定位高 CPU 线程
找到了“嫌疑人”进程,接下来就要找到这个进程里哪个线程在“搞事情”。 这时候就需要 top 的另一个利器:top -Hp [进程 ID]。
这个命令会列出指定 PID 下所有线程的 CPU 使用情况。很快,我就发现了一个 CPU 占用率极高的线程,再次记下它的 线程 ID (LWP)。
1.3 “审问”嫌疑人:查看线程堆栈
现在我们已经锁定了“嫌疑线程”,是时候看看它到底在干什么了。JDK 提供的 jstack 工具就是我们的“审讯利器”。
我执行了命令:jstack -l [进程 ID] > jstack.log,将该进程的所有线程堆栈信息都导到了 jstack.log 文件中。
(故事继续)
由于 top -Hp 看到的线程 ID是十进制的,而 jstack 日志中的线程 ID 是十六进制的,所以我们需要进行一个简单的转换。使用 printf "%x\n" [十进制线程 ID] 命令,将十进制的线程 ID 转换为十六进制。
拿到十六进制的线程 ID,我打开 jstack.log 文件,通过搜索(/ 命令在 vim 中)找到了对应的线程堆栈信息。
(在这里可以贴一段你实际遇到的线程堆栈的简化版,并进行分析)
例如,我看到这个线程一直在执行一个看似没有出口的循环:
"Thread-X" #23 prio=5 os_prio=0 tid=0x00007f8c12345678 nid=0xabcd runnable [0x00007f8c98765432]
java.lang.Thread.State: RUNNABLE at
com.example.demo.service.impl.SomeServiceImpl.process(SomeServiceImpl.java:88) at
com.example.demo.task.MyTask.run(MyTask.java:35) at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at
java.lang.Thread.run(Thread.java:748)
(故事高潮)
看到 SomeServiceImpl.java:88 这行代码,我瞬间明白了!这不就是我昨天刚上线的一个功能吗?由于在处理某种特殊数据时,我遗漏了一个判断条件,导致程序进入了死循环,一直在疯狂地消耗 CPU 资源!
(解决方案)
定位到问题代码,解决起来就简单多了。我立即修改了代码,增加了缺失的判断逻辑,然后紧急发布上线。几分钟后,监控告警消失,服务器的 CPU 使用率也恢复了正常。
(总结经验)
这次 CPU 爆高事件虽然惊险,但也给我提了个醒:再简单的代码也需要仔细 review,任何疏忽都可能导致线上事故。
1.4 其他 CPU 飚高原因
除了业务逻辑死循环,CPU 飚高还可能是以下原因导致的:
- C2 编译器优化: JVM 的 C2 编译器在将热点代码编译成本地机器码时会消耗 CPU。解决方案: 提前进行充分的压测预热,让 C2 编译器在上线前完成编译。
- 频繁 Full GC: Full GC 会 Stop-The-World,如果过于频繁也会导致 CPU 升高。解决方案: 需要进行 GC 优化。
2. 内存问题排查:与 GC “斗智斗勇” 🧠
2.1 内存溢出(OOM):最后的“遗言”
内存溢出是 Java 应用最致命的错误之一。当 JVM 堆内存不足以创建新对象时,就会抛出 OutOfMemoryError。
排查方法:
- 添加 JVM 参数: 在启动参数中加上
-XX:+HeapDumpOnOutOfMemoryError,这样在发生 OOM 时,JVM 会生成一个 Heap Dump 文件(.hprof文件),记录了当时堆内存的详细信息。 - 使用内存分析工具: 常用的工具有 MAT (Memory Analyzer Tool),JProfiler,JVisualVM 等。这些工具可以分析 Heap Dump 文件,找出内存泄漏的原因,例如哪些对象占用了大量内存,对象的引用链是什么等等。
2.2 GC 不健康:小心“温水煮青蛙” 🐸
有时候,内存并没有溢出,但是 GC 的行为并不理想,例如 Young GC(YGC)过于频繁或耗时过长,Full GC(FGC)过于频繁等等。这也会影响应用的性能。
如何判断 GC 是否健康?
根据我的经验,一个相对健康的 GC 状态是:
- YGC: 大约 5 秒一次左右,每次耗时不超过 50 毫秒。
- FGC: 最好没有,如果使用 CMS GC,一天一次左右是可以接受的。
GC 优化的两个维度:
-
频率:
- YGC 频率过低(间隔过长): 可能说明堆内存过大,可以适当缩小堆内存。
- YGC 频率过高: 可能说明 Eden 区过小,可以适当增大 Eden 区,但要保证新生代占整个堆的 30%-40%。Eden、From Survivor、To Survivor 的比例建议在 8:1:1 左右,可以根据实际情况调整。
-
时长(主要针对 YGC):
-
YGC 耗时过长:
可能原因包括:
- 大量对象需要复制: 检查是否有大量短生命周期的大对象。
- StringTable 过大: 如果大量使用
String.intern()且没有 Full GC 清理,可能导致 StringTable 很大,YGC 扫描耗时增加。 - 操作系统 Swap: GC 时如果操作系统正在进行内存交换,也会增加 STW 时间。
-
FGC 优化(主要优化频率):
FGC 的常见原因和优化思路:
- Old 区内存不够: 扩大 Old 区容量。如果 FGC 后 Old 区仍然有很多存活对象,说明 Old 区确实需要增大;如果 FGC 后效果很好,可能是大量短命对象进入了 Old 区,需要调整新生代大小或晋升年龄。
- 元数据区内存不够: 调整元数据区大小(
-XX:MaxMetaspaceSize)。 - System.gc() 调用: 避免在生产环境显式调用
System.gc()。 - jmap 或 jcmd 执行 Full GC 操作: 在执行 dump 等操作时会触发 FGC,需要在合适的时间进行。
- CMS Promotion failed 或 concurrent mode failure: 这是 CMS GC 特有的问题,通常需要调整 CMS 相关参数,例如
-XX:CMSInitiatingOccupancyFraction。 - JVM 悲观策略: JVM 认为 YGC 后 Old 区无法容纳晋升对象而提前触发 FGC。可以尝试调整新生代和 Old 区的大小比例。
2.3 GC 监控利器
- jstat: 查看 JVM 各种内存区域的使用情况和 GC 的详细统计信息,例如 YGC 次数、YGC 总耗时、FGC 次数、FGC 总耗时等。
- jmap 和 jcmd: 可以查看堆内存的对象分布情况,也可以生成 Heap Dump 文件。Oracle 推荐使用
jcmd代替jmap。注意:dump 文件时会触发 FGC,谨慎使用。 - jinfo: 查看 JVM 启动参数,也可以在运行时修改部分参数。
- 可视化 GC 日志分析工具: 例如 GCeasy,HeapHero 等,可以将 GC 日志可视化,更直观地分析 GC 行为。
最重要的:线上环境务必开启 GC 日志! 这对于分析 GC 问题至关重要。
总结:基本功是线上排查的“倚天剑” 🗡️
正如本文的标题,我们今天聊的都是线上问题排查的“基本操作”。线上故障千变万化,解决之道也远不止这些。但掌握这些基本工具和思路,能帮助我们快速定位问题的方向,为后续更深入的分析打下坚实的基础。
后续的文章中,我还会继续分享关于 IO、网络、TCP 连接等方面的基本排查方法,希望能和大家一起在“线上战场”上不断成长!敬请期待! 😉
14万+

被折叠的 条评论
为什么被折叠?



