周二下午收到告警,订单服务 CPU 持续 100%,接口全部超时。SSH 上去一看,4 个核全跑满了。
第一步:找到最耗 CPU 的线程
# 找到 Java 进程 PID
jps -l
# 12345 order-service.jar
# 查看该进程下各线程的 CPU 占用
top -Hp 12345
# 找到 CPU 最高的线程 ID(假设是 12380)
# 转换成 16 进制
printf '%x\n' 12380
# 305c
第二步:导出线程堆栈
jstack 12345 > /tmp/thread_dump.txt
在 dump 文件里搜 nid=0x305c:
"order-processing-thread-7" #45 prio=5 os_prio=0 tid=0x00007f... nid=0x305c runnable [0x00007f...]
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$BmpCharProperty.match(Pattern.java:3825)
at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
...
at com.example.util.OrderParser.parse(OrderParser.java:42)
at com.example.service.OrderService.processOrder(OrderService.java:89)
第三步:定位代码
// OrderParser.java 第 42 行
public static Order parse(String input) {
// 灾难性回溯!
Pattern pattern = Pattern.compile("(.+)*order_(\\d+)_detail(.*)");
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
// ...
}
}
问题找到了:正则表达式灾难性回溯。
正则 (.+)* 是嵌套量词,当输入字符串不匹配时,引擎会尝试所有可能的组合,导致指数级回溯。
// 正常输入:毫秒级完成
parse("abc_order_123_detail_xyz")
// 恶意输入:CPU 直接打满
parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaa!") // 不匹配,引擎疯狂回溯
第四步:修复
// 方案一:避免嵌套量词
Pattern pattern = Pattern.compile("(.+?)_order_(\\d+)_detail(.*)");
// 用 .+? 非贪婪代替 (.+)*
// 方案二:预编译正则(不要每次 new)
private static final Pattern ORDER_PATTERN =
Pattern.compile("(.+?)_order_(\\d+)_detail(.*)");
public static Order parse(String input) {
Matcher matcher = ORDER_PATTERN.matcher(input);
if (matcher.find()) {
// ...
}
}
两个修复点:
- 正则表达式改为非贪婪,避免灾难性回溯
- Pattern 改为静态常量,不要每次调用都编译
第五步:验证
修复后重新部署,CPU 曲线:
修复前:100% → 100% → 100%(持续)
修复后:35% → 28% → 30%(正常水平)
顺便发现了另一个问题
排查过程中 jstack 看了几次,发现有个线程一直在 BLOCKED 状态:
"task-scheduler-3" #50 prio=5 os_prio=0 tid=0x00007f... nid=0x3060 blocked [0x00007f...]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.service.CacheService.refresh(CacheService.java:67)
- waiting to lock <0x00000000c01234> (a com.example.service.CacheService)
查了一下,refresh 方法加了 synchronized,但里面调了一个很慢的远程接口:
// 问题代码
public synchronized void refresh() {
List<Data> data = slowRemoteApi.fetchAll(); // 这个接口要 30 秒
cache.putAll(data);
}
synchronized 锁了整个方法,远程接口慢的时候,所有其他线程都在等锁。改成只锁写缓存的部分:
public void refresh() {
// 远程调用不加锁
List<Data> data = slowRemoteApi.fetchAll();
// 只锁写操作
synchronized (this) {
cache.putAll(data);
}
}
总结
CPU 100% 排查四步:top -Hp 找线程 → printf '%x' 转 16 进制 → jstack 导堆栈 → 搜 nid=0x 定位代码。这次的问题是正则灾难性回溯,另外顺手发现了一个 synchronized 范围过大的问题。正则表达式要注意避免嵌套量词,synchronized 范围要尽可能小。