首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一次线上CPU飙高排查实录:JVM调优之旅

一次线上CPU飙高排查实录:JVM调优之旅

原创
作者头像
大王叫我来巡山、
修改2025-08-29 13:41:48
修改2025-08-29 13:41:48
5391
举报
一、 引言:风平浪静下的警报

一个平静的下午,运维平台突然弹出一条告警:线上某核心应用的某台机器,CPU使用率持续超过200%(8核机器),并且持续了十多分钟尚未恢复。

这是一个基于 Spring Boot + MyBatis 的Java应用,部署在Linux服务器上。告警就是命令,我们必须立即定位问题,避免服务雪崩。传统的“重启大法”虽然能暂时解决问题,但无法根除,我们需要找到问题的根源。

二、 排查工具与思路:为什么是他?

在以往,这种排查流程通常是:

  1. top 命令找到CPU占用最高的Java进程PID。
  2. top -Hp [pid] 找到该进程下占用CPU最高的线程ID。
  3. 将线程ID转换为16进制。
  4. jstack [pid] 抓取线程快照。
  5. 在快照文件中查找对应的16进制线程ID,分析线程栈。

这个流程繁琐且一次jstack只能看到瞬间的快照,对于频繁变化的问题可能抓不到现场。

这次,我们决定使用开源的工具,一个强大的Java诊断工具。它不仅能简化上述所有步骤,还提供了实时监控、方法执行监控、动态生成火焰图等强大功能。

三、 实战操作步骤

第一步:远程连接与附着

通过SSH登录到目标服务器,下载并启动,附着到目标Java进程上。

代码语言:bash
复制
# 1. 找到目标Java进程(我们应用的进程)
ps -ef | grep java

# 2. 下载并启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 3. 控制台会列出所有Java进程,输入序号选择我们要监控的进程
[INFO] arthas-boot version: 3.6.7
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 12345 org.example.MyApplication
...
输入: 1
[INFO] arthas home: /root/.arthas/lib/3.6.7/arthas
[INFO] Try to attach process 12345
...
Attach success.

第二步:全局监控,定位异常线程

使用dashboard命令,获得一个实时的系统仪表盘。

代码语言:bash
复制
[dashboard]

在输出的线程列表(Threads)部分,立刻发现了一个名为pool-7-thread-1的线程,其CPU使用率居高不下,持续在90%以上。这就是我们的“罪魁祸首”。

第三步:查看问题线程的详细栈

使用thread命令查看这个异常线程的完整调用栈。

代码语言:bash
复制
# 查看CPU使用率最高的线程的堆栈信息
thread -n 3

# 或者直接指定问题线程的ID(从dashboard中获取)
thread [id]

输出结果显示,这个线程正处于RUNNABLE状态,调用栈深深地卡在java.util.regex.Pattern$CharProperty.match 方法中,而调用这个正则匹配的,是我们业务代码中的一个日志切面(LogAspect)

第四步:深入方法监控,定位问题代码

我们使用watch命令来监控这个切面中可疑方法的入参和返回值,试图找出是什么输入导致了疯狂的正则匹配。

代码语言:bash
复制
# 监控LogAspect中cutMethod方法执行的入参
watch com.example.aop.LogAspect cutMethod '{params}' -x 3

通过观察,我们发现当某个特定的第三方接口回调我们时,会传入一个非常长的JSON字符串(超过100KB),而这个字符串中包含了大量特殊字符。我们的切面为了记录日志,会尝试对这个字符串进行匹配操作。

根源分析:

我们的日志切面里,有一段有性能缺陷的正则表达式,用于简单匹配和过滤。这个正则表达式在普通短文本下运行良好,但遇到了超长且包含复杂结构的字符串时,发生了正则表达式的“回溯爆炸”(Catastrophic Backtracking),导致CPU计算量呈指数级增长,最终耗尽了CPU资源。

有问题的正则示例(简化版):

代码语言:java
复制
// 为了匹配并提取内容,但分组和贪婪匹配在复杂文本下会导致大量回溯
String regex = ".*name:(.*)value:(.*)end.*";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(hugeInputString); // hugeInputString是超长字符串
if (matcher.find()) {
    // ...
}
四、 解决方案与优化

短期解决方案(紧急止血):

  1. 立即在工具中使用jad命令反编译线上类确认代码。
  2. 因为无法立即发布,我们使用了工具的mc**(Memory Compiler)和**retransform**命令**,在线热更新了修复后的Class文件,将正则匹配替换为简单的字符串分割split()操作。
代码语言:bash
复制
# 1. 在本地IDE修改LogAspect.java源码并编译成Class文件
# 2. 用工具的mc命令编译(或直接上传.class文件)
mc -c <ClassLoaderHash> /tmp/LogAspect.java -d /tmp

# 3. 用retransform命令热加载新的字节码
retransform /tmp/com/example/aop/LogAspect.class

CPU使用率在命令执行后几分钟内迅速下降至正常水平。

长期解决方案(根本修复):

  1. 重构日志切面:移除复杂的正则表达式,对于超长内容(如length() > 1000)进行截断或跳过详细匹配,只做简单日志记录。
  2. 添加防护代码
代码语言:java
复制
public void cutMethod(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof String) {
                String argStr = (String) arg;
                // 对过长参数进行截断,避免耗性能操作
                if (argStr.length() > 2000) {
                    argStr = argStr.substring(0, 2000) + "...(truncated)";
                }
                // 使用更安全、更简单的字符串操作方法替代复杂正则
                // ...
            }
        }
        // ...
    }
  1. 代码审查:对全项目代码进行正则表达式扫描,避免类似性能陷阱。
五、 总结与思考

这次线上排查给我带来了几点深刻体会:

  1. 工具赋能:他是Java开发者不可或缺的“瑞士军刀”,它将复杂的JVM排查工作变得简单直观,极大提升了问题定位效率。掌握它,就等于拥有了线上应用的“上帝视角”。
  2. 性能意识正则表达式是一把双刃剑,编写时必须考虑其最坏情况下的时间复杂度,避免“回溯爆炸”。对于用户输入或外部传入的数据,尤其要谨慎。
  3. 防御式编程:对于日志记录、参数解析等环节,一定要对输入数据的规模和格式做必要的判断和限制,防止被异常数据“击穿”。
  4. 热更新能力:在紧急情况下,他的热更新能力可以作为线上问题的救命稻草,但它毕竟是“外科手术”,最终还是要依靠完整的代码修复和发布流程。

通过这次从告警到定位再到修复的完整过程,不仅解决了一个线上故障,更对JVM应用的性能调优和问题排查建立了更深的理解。希望这次实录也能为你带来启发,工具在评论区!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、 引言:风平浪静下的警报
  • 二、 排查工具与思路:为什么是他?
  • 三、 实战操作步骤
  • 四、 解决方案与优化
  • 五、 总结与思考
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档