
一个平静的下午,运维平台突然弹出一条告警:线上某核心应用的某台机器,CPU使用率持续超过200%(8核机器),并且持续了十多分钟尚未恢复。
这是一个基于 Spring Boot + MyBatis 的Java应用,部署在Linux服务器上。告警就是命令,我们必须立即定位问题,避免服务雪崩。传统的“重启大法”虽然能暂时解决问题,但无法根除,我们需要找到问题的根源。
在以往,这种排查流程通常是:
top 命令找到CPU占用最高的Java进程PID。top -Hp [pid] 找到该进程下占用CPU最高的线程ID。jstack [pid] 抓取线程快照。这个流程繁琐且一次jstack只能看到瞬间的快照,对于频繁变化的问题可能抓不到现场。
这次,我们决定使用开源的工具,一个强大的Java诊断工具。它不仅能简化上述所有步骤,还提供了实时监控、方法执行监控、动态生成火焰图等强大功能。
第一步:远程连接与附着
通过SSH登录到目标服务器,下载并启动,附着到目标Java进程上。
# 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命令,获得一个实时的系统仪表盘。
[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()) {
// ...
}短期解决方案(紧急止血):
jad命令反编译线上类确认代码。mc**(Memory Compiler)和**retransform**命令**,在线热更新了修复后的Class文件,将正则匹配替换为简单的字符串分割split()操作。# 1. 在本地IDE修改LogAspect.java源码并编译成Class文件
# 2. 用工具的mc命令编译(或直接上传.class文件)
mc -c <ClassLoaderHash> /tmp/LogAspect.java -d /tmp
# 3. 用retransform命令热加载新的字节码
retransform /tmp/com/example/aop/LogAspect.classCPU使用率在命令执行后几分钟内迅速下降至正常水平。
长期解决方案(根本修复):
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)";
}
// 使用更安全、更简单的字符串操作方法替代复杂正则
// ...
}
}
// ...
}这次线上排查给我带来了几点深刻体会:
通过这次从告警到定位再到修复的完整过程,不仅解决了一个线上故障,更对JVM应用的性能调优和问题排查建立了更深的理解。希望这次实录也能为你带来启发,工具在评论区!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。