一步步排查:从 CPU 100% 告警到正则回溯根因

本文是线上问题实战录系列的第 3 篇
叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防


问题现象

前两天遇到一个线上问题,排查过程比较典型,记录一下。某日下午,收到告警:auth-service CPU 持续 100%,连续 12 分钟没有恢复。伴随症状包括接口响应变慢(p99 从 50ms 到 2.3s)、错误率升高(0.1% 到 3.2%)、Pod 重启 3 次。初步排查先排除了流量突增的可能——请求量和平时差不多。那就说明问题出在应用内部。首先登录服务器执行 top 命令,确认 Java 进程 CPU 占用异常。然后使用 top -Hp 查看具体线程,发现多个线程处于高 CPU 状态。jstack 导出线程栈后,发现这些线程都卡在 UserValidator.isValidPhone 方法里。这是一段手机号校验逻辑,通过正则表达式实现。进一步用 Arthas 的 profiler 命令生成火焰图,确认耗时集中在正则匹配的 Pattern.matcher 上。

排查过程

第一步:定位高 CPU 进程

登录生产服务器,执行 top,发现 PID=24589 的 Java 进程占满了 1 个核,RSS 4.5GB:

top 命令输出

第二步:定位消耗 CPU 的线程

top -Hp 24589

TID=24650 占 99% CPU,其他 186 个线程几乎空闲。转十六进制:printf '%x\n' 246500x605a

top -Hp 定位线程

第三步:分析 Java 线程栈

jstack 24589 > /tmp/jstack.txt
grep -A 40 'nid=0x605a' /tmp/jstack.txt

输出显示线程栈卡死在正则表达式匹配中,89 帧都在 Pattern$BmpCharProperty.match 之间来回跳——典型的正则回溯

jstack 线程栈

第四步:用 Arthas 快速验证

一秒就查出来了,和 jstack 结果一致,但快得多:

Arthas 快速定位

第五步:找到问题代码

String.matches(pattern) 等价于 Pattern.compile(pattern).matcher(input).matches()。每次调用都重新编译同一个正则表达式,在高并发下 CPU 直接打满:

有问题的代码

根因分析

今天上午 10 点上线的代码变更引入了 isValidPhoneV1。该方法在每次收到验证请求时都通过 String.matches() 编译正则表达式:

String.matches() → Pattern.compile() → 编译正则 → 创建 Pattern 对象 → 创建 Matcher 对象 → 执行匹配
                                 ↑ 每次请求重复执行整个过程

在高并发下(100 TPS),不断重复的 Pattern.compile() 导致 CPU 完全被正则编译和匹配占据。

修复方案

将正则表达式预编译为 static final 常量:

修复后的代码

Git Diff 对比

验证结果

JMH 基准测试

JMH 基准测试结果

HTTP 压测对比

Apache Benchmark 压测对比

指标 V1(修复前) V2(修复后) 提升倍数
TPS 6,067 28,340 4.67x
平均响应时间 16.48ms 3.53ms ↓78%
P99 响应时间 196ms 28ms ↓86%
CPU 使用率 99.2% 12.3% ↓86%

避坑建议

1. 正则表达式使用规范

  • 禁止在循环/高并发路径中使用 String.matches()Pattern.matches()String.split() 等会触发隐式编译的方法
  • 所有正则表达式必须声明为 static final Pattern 常量
  • 复杂正则表达式的回溯性能需要使用工具验证(如 regex101.com)

2. 上线流程优化

  • 修改高并发路径的代码必须附带性能压测数据
  • 代码审查 checklist 中增加正则表达式使用检查项

3. 监控告警优化

  • 增加对线程级别的监控,快速定位 CPU 消耗源
  • 配置 arthas 到基础镜像中,方便即时诊断

4. 诊断工具推荐

  • 快速定位top -Hpprintf '%x\n'jstack → grep nid
  • 更快的定位arthas thread -n 3
  • 性能分析:JMH 基准测试、async-profiler 火焰图
  • 压测工具:Apache Benchmark (ab)、JMeter

附:完整命令清单

进程与线程级 CPU 排查

top -b -n 1 | head -20                                      # 查看进程 CPU 排行
top -b -H -p <pid> -n 1 | head -25                          # 查看线程 CPU 排行
printf '%x\n' <tid>                                          # TID 转十六进制

Java 诊断

jstack <pid> > /tmp/jstack.txt                              # 导出线程栈
grep -A 40 'nid=0x<nid>' /tmp/jstack.txt                    # 查看指定线程栈帧
thread -n 3                                                  # Arthas 查看最忙线程
stack <class> <method>                                       # Arthas 查看方法调用链
grep -n 'isValidPhone\\|String.matches\\|Pattern' src/main/java/com/opencao/demo/UserValidator.java  # 定位问题代码

压测验证

ab -n 10000 -c 100 -T 'application/json' -p request.json http://auth-service:8080/api/v1/auth/validate/v1  # 修复前
ab -n 10000 -c 100 -T 'application/json' -p request.json http://auth-service:8080/api/v1/auth/validate/v2  # 修复后
mvn test -Dtest=RegexBenchmark -pl .                       # JMH 基准测试
posted @ 2026-06-23 19:23  Ai拆代码的曹操  阅读(0)  评论(0)    收藏  举报