一次线上 CPU 100% 排查记录

2 阅读2分钟

周二下午收到告警,订单服务 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()) {
        // ...
    }
}

两个修复点:

  1. 正则表达式改为非贪婪,避免灾难性回溯
  2. 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 范围要尽可能小。