草草聊事

线上 CPU 100% 排查:正则表达式导致的性能问题

2026/06/19
3
0

线上 CPU 100% 排查:正则表达式导致的性能问题

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


问题现象

2026 年 6 月 18 日下午 3 点 15 分,生产监控告警群突然炸了:

  • CPU 100%:身份验证服务 auth-service CPU 使用率持续 100%,已持续 12 分钟
  • 接口超时/api/v1/auth/validate p99 响应时间从 50ms 飙升到 2.3 秒
  • 错误率飙升:错误率从 0.1% 上升到 3.2%,部分请求直接 504
  • 进程不稳定:Pod 已重启 3 次(OOMKilled)

告警群讨论

排查过程

第一步:定位高 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 基准测试

📖 全文带可复现 Demo 和排查截图 🔗 个人博客:https://opencao.cn 📺 公众号:Ai拆代码的曹操 🌟 知识星球:源阅会 (82877104)