一、 引言:风平浪静下的警报
一个平静的下午,运维平台突然弹出一条告警:线上某核心应用的某台机器,CPU使用率持续超过200%(8核机器),并且持续了十多分钟尚未恢复。
这是一个基于 Spring Boot + MyBatis 的Java应用,部署在Linux服务器上。告警就是命令,我们必须立即定位问题,避免服务雪崩。传统的“重启大法”虽然能暂时解决问题,但无法根除,我们需要找到问题的根源。
二、 排查工具与思路:为什么是Arthas?
在以往,这种排查流程通常是:
top命令找到CPU占用最高的Java进程PID。top -Hp [pid]找到该进程下占用CPU最高的线程ID。- 将线程ID转换为16进制。
jstack [pid]抓取线程快照。- 在快照文件中查找对应的16进制线程ID,分析线程栈。
这个流程繁琐且一次jstack只能看到瞬间的快照,对于频繁变化的问题可能抓不到现场。
这次,我们决定使用阿里开源的Arthas,一个强大的Java诊断工具。它不仅能简化上述所有步骤,还提供了实时监控、方法执行监控、动态生成火焰图等强大功能。
三、 实战操作步骤
第一步:远程连接与附着
通过SSH登录到目标服务器,下载并启动Arthas,附着到目标Java进程上。
# 1. 找到目标Java进程(我们应用的进程)
ps -ef | grep java
# 2. 下载并启动Arthas
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.
第二步:全局监控,定位异常线程
使用Arthas的dashboard命令,获得一个实时的系统仪表盘。
[dashboard]
在输出的线程列表(Threads)部分,立刻发现了一个名为pool-7-thread-1的线程,其CPU使用率居高不下,持续在90%以上。这就是我们的“罪魁祸首”。
第三步:查看问题线程的详细栈
使用thread命令查看这个异常线程的完整调用栈。
# 查看CPU使用率最高的线程的堆栈信息
thread -n 3
# 或者直接指定问题线程的ID(从dashboard中获取)
thread [id]
输出结果显示,这个线程正处于RUNNABLE状态,调用栈深深地卡在java.util.regex.Pattern$CharProperty.match 方法中,而调用这个正则匹配的,是我们业务代码中的一个日志切面(LogAspect)!
第四步:深入方法监控,定位问题代码
我们使用watch命令来监控这个切面中可疑方法的入参和返回值,试图找出是什么输入导致了疯狂的正则匹配。
# 监控LogAspect中cutMethod方法执行的入参
watch com.example.aop.LogAspect cutMethod '{params}' -x 3
通过观察,我们发现当某个特定的第三方接口回调我们时,会传入一个非常长的JSON字符串(超过100KB),而这个字符串中包含了大量特殊字符。我们的切面为了记录日志,会尝试对这个字符串进行匹配操作。
根源分析:
我们的日志切面里,有一段有性能缺陷的正则表达式,用于简单匹配和过滤。这个正则表达式在普通短文本下运行良好,但遇到了超长且包含复杂结构的字符串时,发生了正则表达式的“回溯爆炸”(Catastrophic Backtracking),导致CPU计算量呈指数级增长,最终耗尽了CPU资源。
有问题的正则示例(简化版):
// 为了匹配并提取内容,但分组和贪婪匹配在复杂文本下会导致大量回溯
String regex = ".*name:(.*)value:(.*)end.*";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(hugeInputString); // hugeInputString是超长字符串
if (matcher.find()) {
// ...
}
四、 解决方案与优化
短期解决方案(紧急止血):
- 立即在Arthas中使用
jad命令反编译线上类确认代码。 - 因为无法立即发布,我们使用了Arthas的
mc(Memory Compiler)和retransform命令,在线热更新了修复后的Class文件,将正则匹配替换为简单的字符串分割split()操作。
# 1. 在本地IDE修改LogAspect.java源码并编译成Class文件
# 2. 用Arthas的mc命令编译(或直接上传.class文件)
mc -c <ClassLoaderHash> /tmp/LogAspect.java -d /tmp
# 3. 用retransform命令热加载新的字节码
retransform /tmp/com/example/aop/LogAspect.class
CPU使用率在命令执行后几分钟内迅速下降至正常水平。
长期解决方案(根本修复):
- 重构日志切面:移除复杂的正则表达式,对于超长内容(如
length() > 1000)进行截断或跳过详细匹配,只做简单日志记录。 - 添加防护代码:
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)"; } // 使用更安全、更简单的字符串操作方法替代复杂正则 // ... } } // ... } - 代码审查:对全项目代码进行正则表达式扫描,避免类似性能陷阱。
五、 总结与思考
这次线上排查给我带来了几点深刻体会:
- 工具赋能:Arthas是Java开发者不可或缺的“瑞士军刀”,它将复杂的JVM排查工作变得简单直观,极大提升了问题定位效率。掌握它,就等于拥有了线上应用的“上帝视角”。
- 性能意识:正则表达式是一把双刃剑,编写时必须考虑其最坏情况下的时间复杂度,避免“回溯爆炸”。对于用户输入或外部传入的数据,尤其要谨慎。
- 防御式编程:对于日志记录、参数解析等环节,一定要对输入数据的规模和格式做必要的判断和限制,防止被异常数据“击穿”。
- 热更新能力:在紧急情况下,Arthas的热更新能力可以作为线上问题的救命稻草,但它毕竟是“外科手术”,最终还是要依靠完整的代码修复和发布流程。
通过这次从告警到定位再到修复的完整过程,不仅解决了一个线上故障,更对JVM应用的性能调优和问题排查建立了更深的理解。希望这次实录也能为你带来启发。