线上 CPU 飙升排查

2022年双十一凌晨2点,监控大屏突然报警:订单服务的CPU使用率从30%飙升到95%,持续了5分钟。

技术团队紧急拉起线上会议,10分钟后才定位到问题:某个开发同学在代码里加了一个性能测试脚本,每秒打印10万次日志。

这个小脚本在测试环境没问题,但上了生产环境,日志输出直接占满了CPU。

5分钟CPU打满,影响了约2000个订单的创建,直接损失约5万元。

这是一个典型的"代码问题导致线上故障"的案例。

【面试官手记】

CPU飙升是最常见的线上问题之一。我面试过很多候选人,能完整描述排查路径的不超过30%。大多数候选人只能说"用top看看",但问到"top里看到的是什么"、"怎么定位到具体代码"就卡壳了。CPU排查的关键是层层定位:先定位进程,再定位线程,最后定位代码。

一、CPU飙升的常见原因 🔴

1.1 五大原因

CPU飙升的五大原因:

1. 死循环/无限递归
   - 代码写错,while(true)没有退出条件
   - 递归调用没有终止条件
   - 常见场景:定时任务、数据处理

2. 频繁GC
   - 对象创建太多,触发频繁Minor GC
   - 内存设置不合理,触发Full GC
   - 常见场景:大数据处理、缓存未命中

3. 正则表达式灾难
   - 使用了"灾难性回溯"的正则
   - 输入数据匹配复杂导致CPU飙升
   - 常见场景:日志解析、文本处理

4. 序列化/反序列化
   - 大对象序列化消耗CPU
   - 低效的序列化方式
   - 常见场景:RPC调用、缓存序列化

5. 加密解密
   - 大量数据加密解密
   - 复杂的加密算法
   - 常见场景:接口验签、数据加密

1.2 量化指标

CPU使用率的含义:
- user%:用户态CPU,执行应用程序代码
- sys%:系统态CPU,执行内核代码(系统调用、IO操作)
- nice%:被调整过优先级的进程的CPU
- idle%:空闲CPU

排查阈值:
- CPU > 80%:需要关注
- CPU > 90%:需要立即处理
- CPU持续 > 95%:肯定会影响服务

1.3 面试追问

面试官:线上CPU打满了,怎么排查?

候选人:首先用top命令看看是哪个进程CPU高。

面试官:top里显示的PID是什么?怎么进一步定位?

候选人:用top -Hp pid,看看是哪个线程CPU高。

面试官:找到了高CPU的线程后,怎么定位到代码?

候选人:把线程ID转成16进制,然后用jstack打印线程栈,找到对应线程。

【面试官心理】

CPU排查的追问通常很深入。能回答出"top -Hp"的候选人,说明有过实际排查经验;能回答出"转16进制"的候选人,说明知道Java线程ID和系统线程ID的对应关系;能回答出"用arthas trace"的候选人,说明知道更高级的排查工具。

二、排查流程 🔴

2.1 第一步:top定位进程

# 查看系统整体CPU使用情况
top

# 按CPU使用率排序
top -c

# 查看某个进程的CPU使用情况
top -p <pid>

# 关键看:
# - %CPU:进程CPU使用率
# - COMMAND:进程名
# - TIME:进程累计CPU时间
top输出示例:
top - 02:15:30 up 45 days,  5:23,  2 users,  load average: 8.56, 5.32, 4.12
Tasks: 120 total,   1 running, 119 sleeping,   0 stopped,   0 zombie
%Cpu(s): 95.2 us,  3.1 sy,  0.0 ni,  1.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 65536000 total, 12345678 free, 45678901 used,  7514121 buff/cache
KiB Swap:        0 total,        0 free,        0 available

  PID USER      PR  NI  %CPU    %MEM     TIME+   COMMAND
12345 java      20   0  85.3   12.5  12:34.56   java -jar app.jar

2.2 第二步:top定位线程

# 查看进程内各线程的CPU使用情况
top -Hp 12345

# 关键看:
# - PID:线程ID
# - %CPU:线程CPU使用率
# - TIME:线程累计CPU时间
top -Hp输出:
  PID USER      PR  NI  %CPU    %MEM     TIME+   COMMAND
12346 java      20   0  45.2    0.0   5:23.45   java
12347 java      20   0  35.1    0.0   4:12.34   java
12348 java      20   0   5.0    0.0   0:45.67   java

2.3 第三步:jstack定位代码

# 将线程ID转成16进制
printf '%x\n' 12346
# 输出:303a

# 打印线程栈,查找对应线程
jstack 12345 | grep -A 20 '0x303a'
// jstack输出示例:
"pool-1-thread-10" #12346 prio=5 os_prio=0 tid=0x00007f8a4c02a800 nid=0x303a runnable [0x00007f8a3c2a8000]
   java.lang.Thread.State: RUNNABLE
    at java.lang.String.hashCode(String.java:69)
    at java.util.HashMap.put(HashMap.java:426)
    at com.example.service.UserService.getUserById(UserService.java:45)
    at com.example.controller.UserController.get(UserController.java:32)
    at sun.reflect.GeneratedMethodAccessor45.invoke(Unknown Source)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)

2.4 第四步:arthas精确定位

# 启动arthas
java -jar arthas-boot.jar

# 监控CPU占用最高的类和方法
trace com.example.service.UserService getUserById '#cost > 100'

# 持续监控10秒
monitor -c 10 com.example.service.UserService getUserById

# 查看方法调用耗时
stack com.example.service.UserService getUserById

三、常见问题定位 🟡

3.1 死循环

// 典型死循环代码
while (true) {
    // 某个条件应该退出,但逻辑写错了
    if (count > 100) {
        // 漏了 break
        count--;
    }
    process();
}

// 正确写法
while (true) {
    if (count > 100) {
        break;  // 要有break
    }
    process();
    count--;
}

3.2 正则表达式灾难

// 灾难性回溯的正则
String pattern = "(a+)+b";

// 优化后的正则
String pattern = "a+b";

// 或者使用 possessive quantifier
String pattern = "(a+)++b";

3.3 频繁GC

排查步骤:
1. jstat -gcutil <pid> 1000
   - 查看GC频率和耗时
   - FGC > 1 说明有 Full GC

2. jmap -heap <pid>
   - 查看堆内存设置
   - 查看对象分布

3. jmap -histo <pid> | head -30
   - 查看内存中的大对象
   - 排名前30的对象可能有问题

四、生产避坑 🟡

4.1 CPU排查的五大坑

坑1:只看user%不看sys%

问题:sys%高但user%低,误以为是应用问题
场景:IO读写导致系统调用频繁
解决方案:
- sys%高通常是IO问题,不是应用CPU问题
- 检查磁盘IO和网络IO

坑2:忽略load average

问题:CPU使用率不高,但load average很高
场景:很多进程在等待CPU
解决方案:
- load average > CPU核数说明有等待
- 可能是IO等待或锁等待

坑3:只抓一次jstack

问题:只抓了一次线程栈,但问题是一瞬间的
场景:问题线程已经结束,抓不到
解决方案:
- 用 arthas 的 watch 或 monitor 持续监控
- 或者在问题发生时立即抓取

坑4:忽略GC线程

问题:GC线程占用的CPU被忽略
场景:Full GC时GC线程占用大量CPU
解决方案:
- jstack输出中看到 "GC task thread" 字样
- 用 jstat -gcutil 确认是否有频繁GC

坑5:只看当前时刻

问题:CPU飙升是一瞬间的,当前时刻抓不到
场景:定时任务导致的周期性CPU飙升
解决方案:
- 查看CPU使用率的趋势
- 用 top 的 HISTORY 功能
- 查看监控曲线

4.2 快速止血方案

CPU飙升时的紧急处理:

1. 限流
   - 减少进入的请求量
   - 保护系统不被压垮

2. 重启
   - 紧急重启服务
   - 先限流再重启,避免雪崩

3. 扩容
   - 紧急扩容
   - 分散CPU压力

4. 降级
   - 关闭非核心功能
   - 减少计算量

五、排查工具速查 🟢

工具用途使用场景
top进程CPU排名初步定位
top -Hp线程CPU排名精确定位
jstack线程栈打印定位代码
jstatGC统计判断GC问题
jmap堆内存分析内存问题
arthas线上诊断全方位排查
async-profiler火焰图性能分析
perf系统性能内核级别

六、真实面试回放 🟡

面试官:线上Java服务CPU打满,怎么排查?

候选人(大张):分四步走。

第一步:用 top 看看是哪个进程CPU高。

第二步:用 top -Hp pid 看看是哪个线程CPU高,记下线程ID。

第三步:用 printf '%x\n' 线程ID 转成16进制,然后用 jstack pid | grep 16进制 找到对应的线程栈。

第四步:看线程栈里的代码,找出问题。

面试官:jstack里看到的线程状态有哪些?

大张:有五种:

  • RUNNABLE:正在运行
  • BLOCKED:被阻塞等待锁
  • WAITING:等待被唤醒
  • TIMED_WAITING:限时等待
  • NEW/TERMINATED:创建/结束状态

CPU高通常看 RUNNABLE 状态的线程。

面试官:如果jstack里看到大量线程处于BLOCKED状态呢?

大张:说明有锁竞争。很多线程在等待同一个锁。

要看线程栈里等待的是什么锁,然后看持有锁的线程在做什么。

如果持有锁的线程在执行耗时操作(如数据库查询、网络IO),其他线程就会长时间BLOCKED。

【面试官手记】

大张这场面试的亮点:

  1. 排查流程完整:top → top -Hp → jstack

  2. 知道线程ID转换:10进制转16进制

  3. 知道线程状态含义:BLOCKED说明有锁竞争

  4. 知道进一步分析:看持有锁的线程

CPU排查是P6工程师必备技能,能完整回答的候选人,说明有生产环境排查经验。

CPU飙升排查的核心是层层定位:进程 → 线程 → 代码。记住四步排查法:

  1. top:定位高CPU的进程
  2. top -Hp:定位高CPU的线程
  3. jstack:打印线程栈,定位代码
  4. arthas:精确定位方法耗时

快速止血:限流 → 重启 → 扩容 → 降级。