🚨 救命!生产环境CPU爆表了!——一位老Java程序员的抢救实录

161 阅读20分钟

面试题再现:生产环境CPU占用率突然飙升到100%,如何定位和解决问题?

场景:假设你负责的一个核心微服务在某个下午突然报警,CPU使用率持续100%,导致服务响应缓慢。


📖 前言:当CPU遇上"猝死"

想象一下,某个阳光明媚的下午,你正端着咖啡,准备刷一刷技术博客。突然!手机疯狂震动,运维群炸了锅:

💥 紧急告警!💥
服务名称:order-service
CPU使用率:100% 🔥
响应时间:从50ms飙升到5000ms
用户投诉:电话都打爆了!
老板状态:正在赶来的路上...

这时候,你的内心OS是不是这样的:😱😱😱

别慌! 作为一个身经百战的10年老Java工程师,我见过的事故比你写过的代码都多!今天就让我手把手教你,如何像福尔摩斯一样,层层剥茧,找到那个"犯罪嫌疑人"!


🎯 核心思路:五步排查法

在讲具体操作之前,先给你画个全景图。CPU飙升排查就像看病,我们的诊断流程是这样的:

┌─────────────────────────────────────────────────────────────┐
│                     CPU飙升排查全流程                           │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
        ┌────────────────────────────────────────┐
        │  第一步:系统层面 - 找到"病人"           │
        │  使用 top 命令定位是哪个进程出问题        │
        └────────────────────────────────────────┘
                              │
                              ▼
        ┌────────────────────────────────────────┐
        │  第二步:进程层面 - 揪出"元凶"           │
        │  找到进程内哪个线程占用CPU最高           │
        └────────────────────────────────────────┘
                              │
                              ▼
        ┌────────────────────────────────────────┐
        │  第三步:线程层面 - 获取"犯罪证据"        │
        │  通过 jstack 获取线程堆栈信息           │
        └────────────────────────────────────────┘
                              │
                              ▼
        ┌────────────────────────────────────────┐
        │  第四步:代码层面 - 分析"作案动机"        │
        │  定位到具体的代码行,分析问题原因         │
        └────────────────────────────────────────┘
                              │
                              ▼
        ┌────────────────────────────────────────┐
        │  第五步:解决方案 - 开出"药方"           │
        │  根据问题原因制定解决方案并验证           │
        └────────────────────────────────────────┘

🔍 第一步:系统全局诊断 - 确认"病情"

1.1 使用 top 命令俯瞰全局

首先,我们要像医生一样给系统"把脉"。登录到服务器,执行:

top

你会看到这样的画面:

top - 14:35:28 up 30 days,  5:46,  3 users,  load average: 8.90, 7.32, 6.55
Tasks: 328 total,   4 running, 324 sleeping,   0 stopped,   0 zombie
%Cpu(s): 98.7 us,  1.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  16384.0 total,   2048.5 free,  12288.3 used,   2047.2 buff/cache
MiB Swap:   4096.0 total,   4096.0 free,      0.0 used.   3584.2 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
12345 admin     20   0 8192000 4096000  24576 R  99.9   25.0  123:45.67 java
  789 mysql     20   0 2048000 1024000  16384 S   0.3    6.3   45:12.34 mysqld
  456 nginx     20   0  524288  131072   8192 S   0.1    0.8    5:23.11 nginx

关键指标解读

指标含义正常值异常现象
load average系统负载(1分钟、5分钟、15分钟)< CPU核心数远超CPU核心数(如8核机器负载到8.9)
%Cpu(s): us用户态CPU占用< 80%接近100% ⚠️
%Cpu(s): sy系统态CPU占用< 20%过高说明系统调用频繁
%Cpu(s): idCPU空闲率> 20%接近0%说明CPU已跑满
%Cpu(s): waIO等待时间< 10%过高说明磁盘IO瓶颈

生活比喻: 想象你的服务器是一家餐厅:

  • us(用户态) = 厨师在炒菜做饭(应用程序工作)
  • sy(系统态) = 服务员点菜收银(系统调用)
  • id(空闲) = 厨师和服务员摸鱼休息的时间
  • wa(IO等待) = 等外卖送达或等食材配送

us=98.7%id=0% 时,说明你的"厨师"已经累到虚脱,根本没时间休息!

1.2 锁定"嫌疑人"进程

从上面的输出中,我们看到 PID 12345 的 Java 进程占用了 99.9% 的CPU!

重要信息记录

进程PID:12345
CPU占用:99.9%
内存占用:25.0%(4GB)
进程名称:java

🎯 第一阶段小结:我们已经确认了是哪个Java进程出了问题。但是!一个Java进程可能有成百上千个线程,就像一个餐厅有很多个厨师,到底是哪个"厨师"在拖后腿呢?


🔬 第二步:进程内部透视 - 揪出"罪魁线程"

2.1 使用 top -H 查看线程级别信息

现在,我们要"深入敌后",看看这个Java进程内部都有哪些线程:

top -Hp 12345

参数说明

  • -H:显示线程视图(Threads mode)
  • -p 12345:只看PID为12345的进程

输出结果:

top - 14:36:15 up 30 days,  5:47,  3 users,  load average: 8.95, 7.45, 6.60
Threads: 256 total,   2 running, 254 sleeping,   0 stopped,   0 zombie
%Cpu(s): 98.9 us,  1.1 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

  PID USER      PR  NI    VIRT    RES    SHR S %CPU  %MEM     TIME+ COMMAND
15873 admin     20   0 8192000 4096000  24576 R 87.3   25.0  98:23.45 java
15874 admin     20   0 8192000 4096000  24576 R 12.0   25.0  15:12.67 java
15875 admin     20   0 8192000 4096000  24576 S  0.3   25.0   2:34.12 java
15876 admin     20   0 8192000 4096000  24576 S  0.0   25.0   0:45.89 java

发现了! 线程 15873 占用了 87.3% 的CPU!

关键线程信息

线程ID(TID):15873
CPU占用:87.3%
状态:R(Running - 正在运行)
累计运行时间:9823

2.2 线程状态详解

状态符号含义说明
RRunning正在运行或等待运行(这是我们要关注的!)
SSleeping可中断睡眠(正常等待状态)
DDisk sleep不可中断睡眠(通常在等待IO)
ZZombie僵尸进程(已结束但未被父进程回收)
TTraced/Stopped被调试器暂停或收到停止信号

2.3 还有一个高级技巧

如果你不想看top界面,想直接获取信息,可以用这个命令:

# 一次性获取进程内所有线程的CPU占用情况,并按占用率排序
ps -mp 12345 -o THREAD,tid,time | sort -k2 -rn | head -20

输出:

USER     %CPU PRI SCNT WCHAN  USER SYSTEM   TID     TIME
admin    87.3  19    -     -     -      - 15873 01:38:23
admin    12.0  19    -     -     -      - 15874 00:15:12
admin     0.3  19    -     -     -      - 15875 00:02:34

🧮 第三步:TID十六进制转换 - 破译"密码本"

3.1 为什么要转换?

好了,我们现在知道了罪魁线程是 15873。但是!Java的线程堆栈信息中,线程ID是用十六进制(hexadecimal)表示的,就像黑社会暗号一样。

举个栗子

  • 在操作系统里,线程ID是:15873(十进制)
  • 在jstack输出里,线程ID是:0x3e01(十六进制)

如果不转换,我们在堆栈信息里根本找不到这个线程!这就像你拿着身份证号去查护照号,肯定找不到啊!

3.2 转换方法

方法一:使用 printf 命令(推荐)

printf "%x\n" 15873

输出:

3e01

方法二:使用 Python(备用方案)

python -c "print(hex(15873))"

输出:

0x3e01

方法三:在线计算器 如果服务器上啥都没有,用你的浏览器打开计算器,切换到"程序员模式",输入15873,点击HEX按钮,搞定!

记住这个转换结果

十进制TID:15873
十六进制TID:0x3e01(或 3e01)

生活比喻: 这就像你去银行取钱,柜台小姐姐说"先生,我们这里只认卡号,不认身份证号哦~"。你的身份证号是 十进制线程ID,银行卡号是 十六进制nid,要想取钱(查堆栈),必须先把身份证号转成卡号!


🕵️ 第四步:获取堆栈快照 - 抓取"犯罪现场"

4.1 使用 jstack 抓取堆栈

jstack 是Java虚拟机自带的线程堆栈分析工具,就像案发现场的监控录像,能告诉我们每个线程在干什么。

基础命令

jstack 12345 > /tmp/jstack_12345.txt

提示:把输出重定向到文件,方便后续分析

加强版命令(推荐):

# 连续抓取3次,每次间隔1秒,方便对比分析
for i in {1..3}; do 
    echo "========== Dump $i at $(date) ==========" >> /tmp/jstack_12345.txt
    jstack 12345 >> /tmp/jstack_12345.txt
    sleep 1
done

为什么要抓3次?因为有些问题是瞬时的,有些是持续的。通过对比,我们能更准确地判断!

4.2 在堆栈文件中搜索目标线程

grep -A 30 "3e01" /tmp/jstack_12345.txt

参数说明

  • grep:搜索命令
  • -A 30:显示匹配行及其后30行(一般一个线程堆栈不会超过30行)
  • "3e01":我们刚才转换的十六进制线程ID

输出示例

"http-nio-8080-exec-23" #45 daemon prio=5 os_prio=0 tid=0x00007f8c2c0a1800 nid=0x3e01 runnable [0x00007f8be8ffd000]
   java.lang.Thread.State: RUNNABLE
        at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
        at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
        at java.util.regex.Pattern$BranchConn.match(Pattern.java:4568)
        at java.util.regex.Pattern$CharProperty.match(Pattern.java:3777)
        at java.util.regex.Pattern$Branch.match(Pattern.java:4604)
        at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
        at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
        at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
        ... (循环重复多次) ...
        at com.example.service.OrderService.validateOrderNo(OrderService.java:245)
        at com.example.service.OrderService.processOrder(OrderService.java:128)
        at com.example.controller.OrderController.createOrder(OrderController.java:67)
        at sun.reflect.GeneratedMethodAccessor123.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
        ...

4.3 解读堆栈信息(关键技能!)

让我们逐行分析这个"犯罪现场":

第一行:线程基本信息

"http-nio-8080-exec-23" #45 daemon prio=5 os_prio=0 tid=0x00007f8c2c0a1800 nid=0x3e01 runnable
字段含义示例值解读
"http-nio-8080-exec-23"线程名称这是Tomcat的HTTP工作线程,处理第23个请求
#45线程编号JVM内部编号
daemon是否守护线程daemon=守护线程,非daemon=用户线程
prio=5Java线程优先级1-10,默认5
os_prio=0操作系统线程优先级操作系统层面的优先级
tid=0x...线程ID(JVM内部)JVM内存地址
nid=0x3e01本地线程ID就是我们要找的!
runnable线程状态正在运行中

第二行:Java线程状态

java.lang.Thread.State: RUNNABLE

Java线程6大状态:

┌─────────────────────────────────────────────────────────┐
│                    Java线程状态机                         │
└─────────────────────────────────────────────────────────┘
                              
    NEW(新建)
       │
       │ start()
       ▼
   RUNNABLE(可运行/运行中)  ◄────────────────┐
       │                                      │
       │ 等待锁/IO/sleep                      │
       ▼                                      │
   BLOCKED(阻塞)                            │
   WAITING(无限等待)                        │  notify/interrupt
   TIMED_WAITING(限时等待)  ─────────────────┘
       │
       │ 线程结束
       ▼
  TERMINATED(终止)
状态说明常见场景
RUNNABLE ⚠️正在运行或等待CPU调度CPU密集型任务、死循环
BLOCKED等待获取监视器锁synchronized锁等待
WAITING无限期等待另一个线程的通知Object.wait(), Thread.join()
TIMED_WAITING限时等待Thread.sleep(), wait(timeout)

第三行开始:堆栈调用链

at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
...
at com.example.service.OrderService.validateOrderNo(OrderService.java:245)
at com.example.service.OrderService.processOrder(OrderService.java:128)
at com.example.controller.OrderController.createOrder(OrderController.java:67)

堆栈怎么看?从下往上看!

这就像剥洋葱,从外到内:

  1. 最下面OrderController.createOrder() - Controller接收请求
  2. 中间OrderService.processOrder() - Service处理业务
  3. 关键位置OrderService.validateOrderNo() - 验证订单号
  4. 最上面Pattern$Loop.match() - 正则表达式匹配(重复出现多次!)

🚨 问题发现!

注意到 Pattern$Loop.match 重复出现很多次,这说明:

  • 代码在执行正则表达式匹配
  • 正则表达式陷入了回溯陷阱(Regular Expression Denial of Service, ReDoS)

4.4 多次抓取对比分析

如果你抓取了3次堆栈,对比一下:

# 提取所有3次抓取中,线程3e01的堆栈顶部方法
grep -A 5 "nid=0x3e01" /tmp/jstack_12345.txt | grep "at "

如果3次都是相同的堆栈,说明线程卡死在这里了! 如果3次堆栈不同,说明程序在正常运行,只是运算量大。


📊 第五步:问题分析 - 找到"作案动机"

5.1 定位问题代码

根据堆栈信息,我们定位到:

文件:com.example.service.OrderService
方法:validateOrderNo
行号:245

打开代码文件 OrderService.java,找到第245行:

public class OrderService {
    
    // 第128行:处理订单的入口方法
    public OrderResult processOrder(OrderRequest request) {
        // ... 其他代码 ...
        
        // 第245行:验证订单号格式
        validateOrderNo(request.getOrderNo());
        
        // ... 其他代码 ...
    }
    
    // 第240-250行:订单号验证方法
    private void validateOrderNo(String orderNo) {
        // 正则表达式验证订单号格式
        String regex = "(a+)+b";  // ⚠️ 问题代码!
        
        if (!orderNo.matches(regex)) {  // 🔥 CPU在这里爆炸!
            throw new IllegalArgumentException("订单号格式错误");
        }
    }
}

5.2 问题根因分析

问题类型:正则表达式回溯陷阱(ReDoS - Regular Expression Denial of Service)

为什么会CPU飙升?

让我用一个生活例子解释:

生活比喻: 假设你在玩一个迷宫游戏,正则表达式 (a+)+b 就像这样的规则:

  1. 找到一堆连续的 'a'(a+
  2. 这一堆'a'可以重复出现多次((a+)+
  3. 最后必须以 'b' 结尾

当你输入 "aaaaaaaaaaaaaaaaaaaaac" (20个a后面跟个c)时:

  • 正则引擎会尝试各种分组方式:
    • 试法1:(aaaaaaaaaaaaaaaaaaaaaa) - 失败,没有b
    • 试法2:(aaaaaaaaaaaaaaaaaaaa)(a) - 失败,没有b
    • 试法3:(aaaaaaaaaaaaaaaaaaa)(aa) - 失败,没有b
    • 试法4:(aaaaaaaaaaaaaaaaaaa)(a)(a) - 失败,没有b
    • ... 有 2^20 = 1,048,576 种尝试方式!

这就像你在迷宫里,每走到一个岔路口,都要尝试所有可能的路径,然后发现走不通,再回来,再尝试... 最后把自己累死!

理论解释

输入字符串:"aaaaaaaaaaaaaaaaaaaaac" (20个a + 1个c)
正则表达式:(a+)+b

┌──────────────────────────────────────────────────────────┐
│                     回溯过程示意                          │
└──────────────────────────────────────────────────────────┘

尝试次数与字符串长度的关系:O(2^n)

当n=10时,尝试次数 ≈ 1,024次
当n=20时,尝试次数 ≈ 1,048,576次
当n=30时,尝试次数 ≈ 1,073,741,824次 🔥

每次尝试都要消耗CPU资源,导致CPU占用率飙升!

5.3 常见CPU飙升原因总结

作为10年老Java工程师,我总结了以下常见的CPU飙升原因:

类型问题描述堆栈特征占比
1. 死循环代码逻辑错误,无限循环堆栈始终停在循环体内25%
2. 正则回溯复杂正则表达式遇到特殊输入堆栈中有 Pattern.match()15%
3. 频繁GC内存不足,Full GC频繁堆栈中大量 GC thread20%
4. 大量线程竞争synchronized锁竞争激烈大量线程BLOCKED状态15%
5. 大数据量计算未分页查询,一次性加载大量数据堆栈中有数据处理逻辑10%
6. 序列化/反序列化JSON/XML处理大对象堆栈中有Jackson/Fastjson8%
7. 反射调用大量反射操作堆栈中有 Method.invoke5%
8. 第三方SDK依赖包的Bug堆栈中有第三方类2%

本案例属于:第2类 - 正则回溯


💊 第六步:对症下药 - 制定解决方案

6.1 立即止血方案(5分钟内)

目标:先让服务恢复,再慢慢查问题

方案A:重启服务(最快但不根治)

# 找到Java进程并重启
systemctl restart order-service

# 或者使用kill命令(优雅停止)
kill -15 12345

# 等待30秒后检查是否停止
sleep 30
ps -ef | grep java

# 如果还没停止,强制杀死
kill -9 12345

# 启动服务
./startup.sh

⚠️ 注意:重启只是临时方案,问题还会复发!

方案B:限流(推荐)

如果你用了Spring Cloud Gateway或Nginx,立即启用限流:

# application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10  # 每秒允许10个请求
                redis-rate-limiter.burstCapacity: 20  # 令牌桶容量20

方案C:熔断降级

@Service
public class OrderService {
    
    // 使用Sentinel或Hystrix进行熔断
    @SentinelResource(value = "validateOrderNo", 
                      blockHandler = "handleBlock",
                      fallback = "handleFallback")
    private void validateOrderNo(String orderNo) {
        // 原有验证逻辑
    }
    
    // 熔断处理
    public void handleBlock(String orderNo, BlockException ex) {
        log.warn("订单号验证被限流,orderNo={}", orderNo);
        // 跳过验证,或使用简单验证
    }
}

6.2 根治方案(30分钟内)

修复正则表达式

public class OrderService {
    
    // ❌ 错误的正则(会导致回溯)
    private static final String BAD_REGEX = "(a+)+b";
    
    // ✅ 修复后的正则(避免回溯)
    private static final String GOOD_REGEX = "a+b";
    
    // ✅ 或者使用更精确的订单号验证
    private static final String ORDER_REGEX = "^[A-Z]{2}\\d{16}$";  // 例如:OR1234567890123456
    
    private void validateOrderNo(String orderNo) {
        // 1. 先进行简单的长度和格式检查
        if (orderNo == null || orderNo.length() != 18) {
            throw new IllegalArgumentException("订单号长度错误");
        }
        
        // 2. 使用安全的正则表达式
        if (!orderNo.matches(ORDER_REGEX)) {
            throw new IllegalArgumentException("订单号格式错误");
        }
        
        // 3. 添加超时保护
        validateWithTimeout(orderNo, ORDER_REGEX, 100);  // 100ms超时
    }
    
    /**
     * 带超时保护的正则验证
     */
    private boolean validateWithTimeout(String input, String regex, long timeoutMs) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Boolean> future = executor.submit(() -> input.matches(regex));
        
        try {
            return future.get(timeoutMs, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            future.cancel(true);
            log.error("正则验证超时,input={}, regex={}", input, regex);
            return false;
        } catch (Exception e) {
            log.error("正则验证异常", e);
            return false;
        } finally {
            executor.shutdown();
        }
    }
}

正则表达式优化对比

类型正则表达式时间复杂度20字符耗时安全性
💀 灾难级(a+)+bO(2^n)>10秒危险
⚠️ 较差(a*)*bO(2^n)>5秒危险
✅ 良好a+bO(n)<1ms安全
✅ 优秀^[A-Z]{2}\d{16}$O(n)<1ms安全

6.3 预防方案(长期)

1. 代码审查清单

## 正则表达式审查清单

- [ ] 避免嵌套量词:(a+)+, (a*)+, (a+)*
- [ ] 避免分支重叠:(a|a)+, (a|ab)+
- [ ] 为正则添加超时保护
- [ ] 对用户输入先做长度限制
- [ ] 使用正则性能测试工具检查

2. 添加监控和告警

@Aspect
@Component
public class PerformanceMonitorAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = pjp.getSignature().toShortString();
        
        try {
            Object result = pjp.proceed();
            return result;
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            
            // 记录慢方法
            if (duration > 1000) {
                log.warn("慢方法告警:{} 耗时 {}ms", methodName, duration);
                // 发送告警
                alertService.sendSlowMethodAlert(methodName, duration);
            }
            
            // 记录到监控系统
            Metrics.counter("method.duration", 
                          "method", methodName).increment(duration);
        }
    }
}

3. 单元测试(防御性编程)

@Test
public void testRegexPerformance() {
    String badInput = "a".repeat(20) + "c";  // 恶意输入
    
    // 使用assertTimeout确保测试在100ms内完成
    assertTimeout(Duration.ofMillis(100), () -> {
        orderService.validateOrderNo(badInput);
    }, "正则验证不应超过100ms");
}

@Test
public void testRegexSafety() {
    // 测试各种边界情况
    String[] testCases = {
        "",
        "a".repeat(100),
        "a".repeat(1000),
        "a".repeat(10000),
        "OR" + "0".repeat(16),
    };
    
    for (String testCase : testCases) {
        assertDoesNotThrow(() -> {
            orderService.validateOrderNo(testCase);
        }, "任何输入都不应导致程序崩溃");
    }
}

🛠️ 进阶工具篇:让排查更高效

7.1 Arthas - 阿里开源的Java诊断利器

为什么需要Arthas?

传统的jstack、jmap需要多步操作,而Arthas可以在线诊断,无需重启!

安装Arthas(30秒搞定)

# 下载并启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

选择要诊断的Java进程

[INFO] arthas-boot version: 3.7.1
[INFO] Found existing java process, please choose one and input the serial number.
* [1]: 12345 com.example.OrderApplication
  [2]: 67890 org.elasticsearch.bootstrap.Elasticsearch

输入 1,进入Arthas控制台。

一键定位CPU热点

# 1. 自动找出CPU占用最高的线程
thread -n 3

# 输出示例:
"http-nio-8080-exec-23" Id=45 cpuUsage=87.3% RUNNABLE
    at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
    at com.example.service.OrderService.validateOrderNo(OrderService.java:245)
    ...

# 2. 查看线程详细信息
thread 45

# 3. 查看线程的CPU采样
profiler start
# 等待30秒
profiler stop --format html

生成火焰图(可视化CPU热点)

profiler start
# 让系统运行30-60秒
profiler stop --format html --file /tmp/cpu-flame.html

然后下载 /tmp/cpu-flame.html 用浏览器打开,你会看到:

┌──────────────────────────────────────────────────────┐
│                      CPU火焰图                         │
│  (越宽的部分表示CPU占用越多)                           │
└──────────────────────────────────────────────────────┘

                   ┌─────────────────────┐
                   │  Pattern$Loop.match │  ◄── 最宽,占用最多
                   └─────────────────────┘
          ┌────────────────┴───────────────┐
          │ OrderService.validateOrderNo   │
          └────────────────┬───────────────┘
               ┌───────────┴──────────┐
               │ OrderService.process  │
               └───────────┬──────────┘
                    ┌──────┴──────┐
                    │   Tomcat    │
                    └─────────────┘

监控方法执行

# 监控validateOrderNo方法的执行情况
watch com.example.service.OrderService validateOrderNo '{params, returnObj, throwExp}' -x 2

# 查看方法执行时间
trace com.example.service.OrderService validateOrderNo

# 输出示例:
`---ts=2024-10-20 14:35:28;thread_name=http-nio-8080-exec-23;id=45;is_daemon=true;priority=5;
    `---[5234.56ms] com.example.service.OrderService:validateOrderNo()
        +---[0.01ms] org.springframework.util.Assert:notNull() 
        +---[5234.50ms] java.lang.String:matches()  ◄── 耗时最长!

7.2 其他高级工具

1. jinfo - 查看和修改JVM参数

# 查看所有JVM参数
jinfo 12345

# 动态修改JVM参数(部分参数支持)
jinfo -flag +PrintGCDetails 12345

2. jstat - 查看GC情况

# 每1秒打印一次GC统计,共10次
jstat -gc 12345 1000 10

# 输出示例:
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    YGC   YGCT    FGC    FGCT     GCT   
10752  10752  0      0      65536    45678     131072     98765    51200  45678  1234  12.34   56    123.45   135.79

关键指标解读

指标含义正常值异常现象
YGCYoung GC次数平稳增长暴增(说明对象创建过快)
YGCTYoung GC总耗时< 1s> 10s
FGCFull GC次数很少频繁(如每分钟多次)
FGCTFull GC总耗时< 5s> 60s

如果 FGC频繁且FGCT很高,说明是GC导致的CPU飙升,而不是代码问题!

3. jmap - 分析内存

# 查看堆内存使用情况
jmap -heap 12345

# 查看对象统计
jmap -histo:live 12345 | head -20

# 导出堆转储文件(慎用,会暂停应用)
jmap -dump:live,format=b,file=/tmp/heap.hprof 12345

4. MAT (Memory Analyzer Tool) - 分析堆转储

下载heap.hprof到本地,使用MAT分析:

  1. 打开MAT,加载heap.hprof
  2. 点击 "Leak Suspects Report" 查看内存泄漏
  3. 查看 "Dominator Tree" 找出占用内存最多的对象

📚 案例集锦:真实战场的血泪史

案例1:正则表达式陷阱(本案例)

现象:用户提交订单时,偶尔CPU飙升到100% 原因:正则表达式 (a+)+b 遇到恶意输入导致回溯 解决:简化正则为 ^[A-Z]{2}\d{16}$ 教训:永远不要相信用户输入,正则表达式要有超时保护

案例2:数据库查询未分页

现象:每天凌晨2点CPU飙升,持续30分钟 堆栈特征

at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2914)
at com.example.service.ReportService.exportAllOrders(ReportService.java:89)

代码

// ❌ 错误:一次性查询100万条订单
List<Order> orders = orderDao.selectAll();  // 没有分页!

解决

// ✅ 正确:分页查询+流式处理
int pageSize = 1000;
int pageNum = 0;
while (true) {
    List<Order> orders = orderDao.selectPage(pageNum, pageSize);
    if (orders.isEmpty()) break;
    
    // 处理这一页数据
    processOrders(orders);
    pageNum++;
}

案例3:线程池耗尽+大量线程自旋

现象:周五下午3点(流量高峰),CPU突然100% 堆栈特征:大量线程在自旋等待

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:832)

原因:线程池配置过小

// ❌ 错误配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,    // 核心线程数:只有2个!
    5,    // 最大线程数:只有5个!
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(10)  // 队列长度:只能缓存10个任务!
);

解决

// ✅ 合理配置
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;  // CPU核心数 × 2
int maxPoolSize = corePoolSize * 4;
int queueCapacity = 1000;

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maxPoolSize,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(queueCapacity),
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:让调用者线程执行
);

案例4:频繁Full GC

现象:服务时不时卡顿,CPU飙升到100%,响应时间从50ms增加到5秒 堆栈特征:大量GC线程

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f8c2c001000 nid=0x1234 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f8c2c002000 nid=0x1235 runnable

诊断

jstat -gc 12345 1000 10

# 输出:FGC在1分钟内增加了10次!

原因:内存不足,老年代对象过多

解决

# 增加堆内存
-Xms4g -Xmx4g  # 原来只有1g

# 调整GC参数
-XX:+UseG1GC  # 使用G1垃圾收集器
-XX:MaxGCPauseMillis=200  # 最大GC停顿时间200ms
-XX:+PrintGCDetails  # 打印GC详情
-XX:+PrintGCDateStamps  # 打印GC时间戳

🎓 排查思维导图

CPU飙升100%排查全流程
│
├── 1️⃣ 系统层面:定位进程
│   ├── top 命令查看进程CPU
│   ├── 记录进程PID
│   └── 判断CPU类型(us/sy/wa)
│
├── 2️⃣ 进程层面:定位线程
│   ├── top -Hp <PID> 查看线程
│   ├── 记录高CPU线程TID
│   └── 或使用 ps -mp <PID> -o THREAD,tid,time
│
├── 3️⃣ 线程层面:转换TID
│   ├── printf "%x\n" <TID>
│   ├── 得到十六进制nid
│   └── 记录转换结果
│
├── 4️⃣ 堆栈层面:抓取快照
│   ├── jstack <PID> > thread_dump.txt
│   ├── grep "nid=0x<HEX>" thread_dump.txt
│   ├── 分析堆栈调用链
│   └── 对比多次抓取结果
│
├── 5️⃣ 代码层面:分析问题
│   ├── 死循环?
│   ├── 正则回溯?
│   ├── 频繁GC?
│   ├── 锁竞争?
│   ├── 大数据量?
│   └── 其他逻辑问题?
│
├── 6️⃣ 解决层面:分步实施
│   ├── 🚑 立即止血(重启/限流/熔断)
│   ├── 💊 根治问题(修复代码)
│   ├── 🛡️ 预防复发(监控/告警/测试)
│   └── 📊 后续跟踪(观察指标)
│
└── 7️⃣ 进阶工具:提升效率
    ├── Arthas:在线诊断神器
    ├── jinfo:查看JVM参数
    ├── jstat:GC统计分析
    ├── jmap:内存快照导出
    └── MAT:堆内存分析

🎯 面试回答模板(收藏版)

当面试官问你"生产环境CPU突然100%怎么办",按照这个模板回答,稳了!

## 排查思路(总分总结构)

### 第一步:系统层面定位(30秒)
我会先登录到服务器,使用 `top` 命令查看整体系统状态,确定是哪个Java进程导致的CPU飙升,记录进程PID。同时观察CPU的us、sy、wa等指标,判断是用户态CPU高还是内核态CPU高。

### 第二步:进程内部定位(30秒)
使用 `top -Hp <PID>` 命令,查看该进程内所有线程的CPU占用情况,找出CPU占用最高的线程TID。如果有多个线程占用都很高,需要分别记录。

### 第三步:TID转换(10秒)
因为jstack输出的线程ID是十六进制格式,所以需要使用 `printf "%x\n" <TID>` 将线程ID转换为十六进制,方便后续查找。

### 第四步:获取线程堆栈(1分钟)
使用 `jstack <PID> > thread_dump.txt` 获取线程堆栈快照。为了提高准确性,我会连续抓取3次,每次间隔1秒。然后在文件中搜索转换后的十六进制线程ID,找到对应的堆栈信息。

### 第五步:分析堆栈和代码(3-5分钟)
根据堆栈信息,定位到具体的代码行。从经验来看,常见原因包括:
1. **死循环**:代码逻辑错误导致
2. **正则表达式回溯**:复杂正则遇到恶意输入
3. **频繁Full GC**:内存不足或内存泄漏
4. **锁竞争**:多线程争抢同一个锁
5. **大数据量处理**:未分页查询等

我会结合业务代码和堆栈调用链,判断是哪种情况。

### 第六步:应急处理和根治(视情况而定)
- **紧急止血**:通过限流、熔断或重启服务快速恢复
- **根本解决**:修复代码问题,优化算法逻辑
- **预防措施**:添加监控告警,完善单元测试,进行压测

### 第七步:后续跟踪
修复上线后,持续观察CPU、内存、GC等关键指标,确保问题彻底解决。同时做好事故复盘,总结经验教训,避免类似问题再次发生。

## 进阶补充

如果传统方式排查困难,我还会使用Arthas等工具进行在线诊断,通过 `thread -n 3` 快速找出CPU热点,或使用 `profiler` 生成火焰图进行可视化分析。

如果怀疑是GC问题,我会使用 `jstat -gc <PID>` 查看GC统计,或使用 `jmap -dump` 导出堆内存快照进行深入分析。

��️ 预防胜于治疗:构建监控体系

8.1 监控指标清单

维度指标告警阈值检查频率
CPU使用率> 80%每30秒
内存堆内存使用率> 85%每30秒
GCFull GC频率> 5次/小时实时
GCGC停顿时间> 1秒实时
线程活跃线程数> 500每1分钟
线程死锁线程数> 0每5分钟
响应时间P99延迟> 1秒每30秒
错误率5xx错误率> 1%每30秒

8.2 Spring Boot Actuator集成

// pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: "*"  # 暴露所有端点
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}

访问 http://localhost:8080/actuator/metrics 查看所有指标。

8.3 Prometheus + Grafana监控大盘

# prometheus.yml
scrape_configs:
  - job_name: 'order-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['order-service:8080']

Grafana大盘配置(Dashboard JSON):

{
  "panels": [
    {
      "title": "CPU使用率",
      "targets": [
        {
          "expr": "system_cpu_usage"
        }
      ]
    },
    {
      "title": "JVM堆内存",
      "targets": [
        {
          "expr": "jvm_memory_used_bytes{area=\"heap\"}"
        }
      ]
    },
    {
      "title": "GC次数",
      "targets": [
        {
          "expr": "rate(jvm_gc_pause_seconds_count[1m])"
        }
      ]
    }
  ]
}

🎁 彩蛋:老司机的私房话

作为一个在生产环境踩过无数坑的老Java工程师,我想分享几点心得:

1. 永远保持冷静 🧘

CPU飙升不是世界末日,按部就班排查,99%的问题都能解决。慌乱只会让你思路混乱,反而浪费时间。

2. 提前准备工具 🛠️

不要等到出问题了才去现装工具。我的服务器上常备:

  • jdk自带工具(jstack、jmap、jstat等)
  • Arthas(已提前下载好)
  • 常用脚本(一键抓取堆栈、分析日志等)

3. 建立知识库 📚

每次故障处理完,我都会写一份事故复盘报告,记录:

  • 问题现象
  • 排查过程
  • 根本原因
  • 解决方案
  • 预防措施

时间长了,这就是你的"武功秘籍"。

4. 压测是必须的 💪

任何代码上线前,都要进行压力测试。不要等到生产环境才发现问题。

# 使用JMeter或ab进行压测
ab -n 10000 -c 100 http://localhost:8080/api/orders

# 观察CPU、内存、响应时间等指标

5. 代码审查要严格 👨‍⚖️

特别是涉及正则表达式、循环、递归的代码,一定要仔细review。一个小bug,可能导致整个系统瘫痪。

6. 监控告警是生命线 🚨

如果没有监控,你可能不知道系统已经出问题了。等用户投诉时,黄花菜都凉了。

7. 不要过度优化 ⚖️

优化要基于数据和瓶颈分析,不要凭感觉。"过早的优化是万恶之源"。


📝 总结:核心要点一张图

┌─────────────────────────────────────────────────────────────────┐
│                      CPU飙升排查核心要点                           │
└─────────────────────────────────────────────────────────────────┘

🔍 排查流程(5步法):
   toptop -Hp → printf "%x" → jstack → 分析代码

🛠️ 常用工具:
   基础:top、jstack、jmap、jstat
   进阶:Arthas、MAT、Profiler

🐛 常见原因:
   1️⃣ 死循环 (25%)
   2️⃣ 正则回溯 (15%)
   3️⃣ 频繁GC (20%)
   4️⃣ 锁竞争 (15%)
   5️⃣ 大数据量 (10%)
   6️⃣ 其他 (15%)

💊 解决思路:
   🚑 立即止血:重启/限流/熔断
   💉 根本治疗:修复代码/优化算法
   🛡️ 长期预防:监控/测试/review

📊 监控指标:
   CPU、内存、GC、线程、响应时间、错误率

🎯 核心原则:
   冷静、系统化、数据驱动、持续改进

🎓 课后作业(检验学习效果)

练习1:模拟CPU飙升场景

创建一个简单的Spring Boot应用,包含以下代码:

@RestController
public class TestController {
    
    @GetMapping("/test1")
    public String test1(@RequestParam String input) {
        // 危险的正则表达式
        String regex = "(a+)+b";
        return input.matches(regex) ? "匹配" : "不匹配";
    }
    
    @GetMapping("/test2")
    public String test2() {
        // 死循环
        while (true) {
            Math.random();
        }
    }
    
    @GetMapping("/test3")
    public String test3() {
        // 频繁创建大对象
        List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            list.add(new byte[1024 * 1024]);  // 每次1MB
        }
        return "完成";
    }
}

任务

  1. 启动应用并调用这些接口
  2. 使用本文所学方法,定位到问题代码
  3. 给出解决方案

练习2:编写监控脚本

编写一个Shell脚本,实现:

  1. 每30秒检查一次Java进程的CPU使用率
  2. 如果CPU超过80%,自动抓取jstack堆栈
  3. 发送告警邮件

提示:

#!/bin/bash
while true; do
    cpu=$(top -b -n 1 -p <PID> | grep java | awk '{print $9}')
    if [ $(echo "$cpu > 80" | bc) -eq 1 ]; then
        # 抓取堆栈
        jstack <PID> > /tmp/jstack_$(date +%s).txt
        # 发送告警
        echo "CPU飙升告警" | mail -s "CPU Alert" your@email.com
    fi
    sleep 30
done

🙏 结语

恭喜你!如果你完整读完了这篇文档,你已经掌握了生产环境CPU飙升问题的完整排查思路和方法。

记住,实践才是检验真理的唯一标准。不要只是看,一定要自己动手试试。可以在测试环境模拟各种问题,练习排查流程,直到形成肌肉记忆。

当你在凌晨3点接到告警电话时,能够从容不迫、有条不紊地解决问题,老板看你的眼神都会不一样!👍

最后,送大家一句话:

"没有解决不了的生产问题,只有不够熟练的排查技术。"

加油,未来的技术大牛!🚀


📚 参考资料

  1. 《深入理解Java虚拟机》 - 周志明
  2. Oracle Java SE文档
  3. Arthas官方文档
  4. JVM性能优化指南
  5. 正则表达式性能优化

文档信息

  • 作者:10年老Java工程师
  • 版本:v1.0
  • 最后更新:2024-10-20
  • 适用人群:Java开发工程师、运维工程师、架构师

如果这份文档帮到了你,请记得:

  • ⭐ 收藏起来,以备不时之需
  • 📤 分享给你的小伙伴
  • 💬 有问题随时讨论交流

祝你在技术道路上越走越远! 🎉