260104-JVM面试题(CPU飙高)

JVM CPU 飙高面试题完全指南

目录


一、问题背景

1.1 什么是 CPU 飙高

CPU 飙高是指应用程序的 CPU 使用率异常升高,远超正常基线水平,导致系统响应变慢甚至服务不可用。

核心特征

  • ✅ User CPU 高:业务逻辑计算密集
  • ✅ Sys CPU 高:系统调用频繁(上下文切换、锁竞争)
  • ✅ Wait CPU 高:I/O 阻塞等待
  • ❌ CPU 使用率持续 > 80%

1.2 CPU 飙高的影响

🔥 直接影响

  1. 接口响应变慢

    • RT(响应时间)从毫秒级增加到秒级
    • P99/P95 延迟显著上升
  2. 吞吐量下降

    • QPS(每秒查询数)大幅下降
    • 系统处理能力受限
  3. 用户体验恶化

    • 页面加载缓慢
    • 请求超时增加
    • 用户流失
  4. 雪崩风险

    • 单点故障扩散到其他节点
    • 整个集群性能下降
    • 可能导致服务不可用

1.3 常见症状

🔍 监控指标异常

  1. CPU 使用率突增

    1
    2
    3
    4
    5
    # 正常情况
    CPU Usage: 20%-40%

    # 异常情况
    CPU Usage: 80%-100%
  2. Load Average 飙升

    1
    2
    3
    4
    5
    # 4 核 CPU,正常 Load < 4
    load average: 1.23, 0.98, 0.76 # 正常

    # 异常 Load >> CPU 核数
    load average: 15.67, 12.34, 10.89 # 异常
  3. 线程状态异常

    • RUNNABLE 线程数量激增
    • BLOCKED/WAITING 线程堆积
  4. GC 频率增加

    • Full GC 频繁触发
    • GC 耗时过长

二、排查思路

2.1 总体流程

采用六步法系统化排查:

1
2
3
4
止血 → 定位 → 分析 → 修复 → 验证 → 复盘
↓ ↓ ↓ ↓ ↓ ↓
控制 找到 深挖 解决 确认 改进
影响 问题 根因 方案 效果 预防

每一步的核心目标

阶段 目标 关键动作
止血 快速恢复服务 限流、降级、扩容、重启
定位 找到高 CPU 线程 top、jstack、Arthas
分析 确定根本原因 线程状态、代码逻辑、GC 情况
修复 解决问题 代码优化、JVM 调优、架构改进
验证 确认效果 灰度发布、监控观察
复盘 防止复发 总结经验、完善监控、优化规范

2.2 核心原则

排查原则

  1. 先止血后分析

    • 优先恢复服务可用性
    • 避免问题扩散
  2. 无损排查

    • 线上不盲目执行危险命令
    • 优先使用监控大盘、Arthas 等无损工具
  3. 数据驱动

    • 基于监控数据判断,不凭感觉
    • 保留现场证据(线程 dump、GC 日志)
  4. 闭环思维

    • 排查不是目的,预防才是关键
    • 建立基线监控、压测标准、容量规划

三、止血阶段

3.1 快速定位故障节点

通过监控大盘迅速定位问题节点。

监控指标

1. CPU 使用率分布

1
2
3
4
# Prometheus 查询
node_cpu_seconds_total{mode="user"} # 用户态 CPU
node_cpu_seconds_total{mode="system"} # 内核态 CPU
node_cpu_seconds_total{mode="iowait"} # I/O 等待

分析要点

  • User CPU 高:业务计算密集(死循环、复杂算法)
  • Sys CPU 高:系统调用频繁(上下文切换、锁竞争)
  • Wait CPU 高:I/O 阻塞(磁盘、网络)

2. Load Average

1
2
3
4
5
6
7
# 查看系统负载
uptime
# 输出:load average: 15.67, 12.34, 10.89

# 查看 CPU 核数
nproc
# 输出:8

判断标准

  • Load Average < CPU 核数:正常
  • Load Average ≈ CPU 核数:繁忙
  • Load Average > CPU 核数 × 2:过载

3.2 系统负载分析

结合 vmstat 深入分析系统状态。

vmstat 命令

1
2
3
4
5
6
7
8
# 每 1 秒采样,共 5 次
vmstat 1 5

# 输出示例
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
8 0 0 102400 51200 204800 0 0 100 200 5000 8000 85 10 3 2 0
9 0 0 100000 51200 204800 0 0 120 250 6000 9000 90 8 1 1 0

关键字段解读

字段 含义 正常值 异常说明
r 运行队列长度 < CPU 核数 过大说明 CPU 瓶颈
b 阻塞进程数 0-2 过小说明 I/O 瓶颈
si/so 交换内存 0 非 0 说明物理内存不足
in 中断次数 - 过高说明硬件中断频繁
cs 上下文切换 < 10000 过高说明线程竞争激烈
us 用户态 CPU% 20-60% 过高说明业务计算密集
sy 内核态 CPU% 5-15% 过高说明系统调用频繁
id 空闲 CPU% 20-50% 过低说明 CPU 资源紧张
wa I/O 等待% 0-5% 过高说明 I/O 瓶颈

异常判断

  • cs > 10000:上下文切换过于频繁
  • sy > 20%:系统调用过多
  • wa > 10%:I/O 成为瓶颈
  • si/so > 0:发生 swap,内存不足

3.3 紧急处理措施

根据问题严重程度采取相应措施。

措施优先级

1. 网关限流/降级(首选)

1
2
3
4
5
6
7
8
9
10
11
# Sentinel 限流配置
rules:
- resource: /api/order/create
grade: 1 # QPS 限流
count: 1000
strategy: 0 # 直接拒绝

- resource: /api/report/export
grade: 0 # 线程数限流
count: 10
strategy: 1 # 排队等待

降级策略

  • 非核心接口返回默认值
  • 异步任务延迟执行
  • 缓存兜底

2. 动态扩容

1
2
3
4
5
# Kubernetes 扩容
kubectl scale deployment my-app --replicas=10

# Docker Swarm 扩容
docker service scale my_app=10

注意事项

  • 确保新节点能快速启动
  • 检查依赖服务容量(DB、Redis)
  • 监控扩容后整体负载

3. 摘除故障节点并重启(最后手段)

1
2
3
4
5
6
7
8
9
# Nginx 摘除节点
upstream backend {
server 192.168.1.10:8080 down; # 标记为不可用
server 192.168.1.11:8080;
server 192.168.1.12:8080;
}

# 重启故障节点
systemctl restart my-app

风险提示

  • ⚠️ 重启会丢失现场信息
  • ⚠️ 仅在必要时使用
  • ⚠️ 重启前尽量保存线程 dump

四、定位阶段

4.1 传统方式:top + jstack

经典的四步定位法。

步骤 1:找高 CPU 进程

1
2
3
4
5
# 按 CPU 使用率排序
top -o %CPU

# 或使用 htop(更友好)
htop

操作

  • P 键按 CPU 排序
  • 记录 Java 进程的 PID(如 12345)
  • q 退出

步骤 2:找该进程下高 CPU 线程

1
2
3
4
5
6
7
8
9
10
11
12
# 方法 1:top -Hp
top -Hp 12345

# 方法 2:pidstat(更详细)
pidstat -t -p 12345 1 5

# 输出示例
Linux 5.4.0 (server01) 01/04/26 _x86_64_ (8 CPU)

Average: UID TGID TID %usr %system %guest %CPU CPU Command
Average: 1000 - 12346 85.20 2.30 0.00 87.50 2 java
Average: 1000 - 12347 10.50 1.20 0.00 11.70 3 java

操作

  • 记录占用最高的 TID(如 12346)
  • 关注 %usr(用户态)和 %system(内核态)

步骤 3:TID 转 16 进制

1
2
3
# jstack 需要 16 进制线程 ID
printf "%x\n" 12346
# 输出:303a

说明

  • Linux 线程 ID(TID)是十进制
  • jstack 输出中的线程 ID 是十六进制
  • 需要转换后才能匹配

步骤 4:抓取线程堆栈

1
2
3
4
5
6
7
8
9
# 方法 1:grep 过滤
jstack 12345 | grep "303a" -A 20

# 方法 2:保存到文件分析
jstack 12345 > /tmp/jstack_$(date +%Y%m%d_%H%M%S).txt

# 在文件中搜索
vim /tmp/jstack_20260104_120000.txt
/303a

输出示例

1
2
3
4
5
6
"http-nio-8080-exec-10" #42 daemon prio=5 os_prio=0 tid=0x00007f8b4c001000 nid=0x303a runnable [0x00007f8b2c5fe000]
java.lang.Thread.State: RUNNABLE
at com.example.service.OrderService.calculatePrice(OrderService.java:125)
at com.example.controller.OrderController.createOrder(OrderController.java:45)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...

分析要点

  • 查看线程状态(RUNNABLE/BLOCKED/WAITING)
  • 定位到具体类和方法
  • 查看调用栈上下文

4.2 现代方式:Arthas

阿里巴巴开源的 Java 诊断工具,更高效便捷。

安装 Arthas

1
2
3
4
5
# 下载
curl -O https://arthas.aliyun.com/arthas-boot.jar

# 启动
java -jar arthas-boot.jar

快速定位高 CPU 线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 一键找出 CPU 最高的 5 个线程
thread -n 5

# 输出示例
"http-nio-8080-exec-10" Id=42 RUNNABLE
cpu usage: 87.5%
at com.example.service.OrderService.calculatePrice(OrderService.java:125)
at com.example.controller.OrderController.createOrder(OrderController.java:45)
...

"http-nio-8080-exec-5" Id=37 RUNNABLE
cpu usage: 10.2%
at java.util.HashMap.hash(HashMap.java:339)
...

优势

  • ✅ 无需手动转换 TID
  • ✅ 直接显示 CPU 使用率
  • ✅ 自动排序,一目了然
  • ✅ 支持在线诊断,无需重启

其他有用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看所有线程状态统计
thread

# 查看指定线程堆栈
thread 42

# 实时监控线程 CPU 使用率
thread -n 5 -i 1000 # 每 1 秒刷新

# 生成火焰图
profiler start
# 等待 30 秒
profiler stop --format html

4.3 高级方式:火焰图

直观展示 CPU 热点方法链,比纯看 jstack 更高效。

工具选型

工具 优势 适用场景
async-profiler 低开销、支持 perf events 生产环境首选
Arthas profiler 集成在 Arthas 中,易用 快速诊断
JProfiler 图形化界面,功能强大 本地开发调试

使用 async-profiler

1
2
3
4
5
6
7
8
9
10
# 下载
wget https://github.com/async-profiler/async-profiler/releases/download/v2.9/async-profiler-2.9-linux-x64.tar.gz
tar -xzf async-profiler-2.9-linux-x64.tar.gz

# 采集 30 秒 CPU 数据
cd async-profiler-2.9-linux-x64
./profiler.sh -d 30 -f /tmp/flamegraph.html 12345

# 生成 SVG 格式
./profiler.sh -d 30 -f /tmp/flamegraph.svg 12345

使用 Arthas profiler

1
2
3
4
5
6
7
8
9
# 启动 profiler
profiler start

# 等待 30 秒后停止
profiler stop --format html

# 查看生成的文件
ls -lh /tmp/arthas-output/
# arthas-output/20260104-120000.html

火焰图解读

1
2
3
4
5
6
7
8
9
10
11
12
13
每个方块代表一个方法:
- 宽度:CPU 时间占比(越宽越热点)
- 高度:调用栈深度
- 颜色:随机分配(便于区分)

从上往下看:
- 顶层:入口方法
- 底层:叶子方法(实际执行代码)

从下往上看:
- 找到最宽的方块(CPU 热点)
- 向上追溯调用链
- 定位到具体业务代码

优势

  • ✅ 直观展示 CPU 热点
  • ✅ 快速定位瓶颈方法
  • ✅ 支持离线分析
  • ✅ 开销极低(< 1%)

五、分析阶段

5.1 线程状态分析

根据线程状态判断问题类型。

线程状态分类

状态 说明 典型场景
RUNNABLE 正在运行或可运行 死循环、复杂计算、频繁 GC
BLOCKED 等待获取锁 锁竞争、 synchronized 阻塞
WAITING 无限期等待 Object.wait()、Thread.join()
TIMED_WAITING 限时等待 Thread.sleep()、Object.wait(timeout)

5.2 常见原因分类

1. RUNNABLE 状态 - CPU 密集型

典型原因

原因 说明 示例
死循环 while(true) 无条件退出 while(true) { doSomething(); }
复杂正则 回溯爆炸 Pattern.compile("(a+)+b")
大量计算 数学运算、加密解密 RSA 签名、哈希计算
序列化/反序列化 JSON/XML 解析大对象 Jackson 解析 10MB JSON
频繁 Full GC Stop-The-World 占用 CPU 老年代内存泄漏

验证手段

1
2
3
4
5
6
7
8
# 查看 GC 频率
jstat -gcutil 12345 1000

# 输出示例
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 45.23 67.89 89.12 92.34 88.56 1234 12.345 56 45.678 58.023

FGC 频繁且耗时长

判断标准

  • FGC(Full GC 次数)快速增长
  • FGCT(Full GC 总耗时)占比高
  • O(Old Gen)使用率持续高位

2. BLOCKED/WAITING 状态 - 锁竞争/I/O 阻塞

典型原因

原因 说明 示例
线程锁竞争 synchronized/ReentrantLock 热点数据并发更新
连接池耗尽 DB/Redis 连接不足 HikariCP maximumPoolSize 过小
同步 I/O 阻塞 网络/磁盘 I/O 等待 HTTP 请求超时、文件读写

验证手段

1
2
3
4
5
6
7
8
9
# 查看锁竞争
jstack 12345 | grep "waiting to lock" -B 5 -A 10

# 输出示例
"http-nio-8080-exec-10" #42 daemon prio=5 os_prio=0 tid=0x00007f8b4c001000 nid=0x303a waiting for monitor entry [0x00007f8b2c5fe000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.service.OrderService.updateStock(OrderService.java:89)
- waiting to lock <0x00000000f5e8a123> (a com.example.model.Product)
at com.example.controller.OrderController.createOrder(OrderController.java:45)

数据库慢查询检查

1
2
3
4
5
6
-- MySQL 慢查询
SHOW PROCESSLIST;
SELECT * FROM information_schema.processlist WHERE TIME > 5;

-- Redis 慢查询
SLOWLOG GET 10

3. Sys CPU 占比高 - 系统调用频繁

典型原因

原因 说明 示例
频繁上下文切换 线程数过多 线程池大小不合理
大量短连接 TCP 连接频繁创建销毁 HTTP 短连接未复用
锁自旋 CAS 操作失败重试 AQS 自旋锁
驱动/网络中断 硬件中断处理 网卡驱动问题

验证手段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看上下文切换
pidstat -w 1 5

# 输出示例
Linux 5.4.0 (server01) 01/04/26 _x86_64_ (8 CPU)

Average: UID PID cswch/s nvcswch/s Command
Average: 1000 12345 8500.00 200.00 java

自愿上下文切换过高

# 查看网络中断
sar -n DEV 1 5

# 检查内核参数
cat /proc/sys/net/core/somaxconn
cat /proc/sys/net/ipv4/tcp_tw_recycle

判断标准

  • cswch/s(自愿上下文切换)> 10000:线程竞争激烈
  • nvcswch/s(非自愿上下文切换)> 1000:CPU 资源不足

5.3 验证手段

综合分析多种指标,确认根本原因。

综合检查清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 线程状态统计
jstack 12345 | grep "java.lang.Thread.State" | sort | uniq -c

# 输出示例
85 RUNNABLE
10 BLOCKED
5 WAITING
50 TIMED_WAITING

# 2. GC 情况
jstat -gcutil 12345 1000 5

# 3. 系统负载
vmstat 1 5

# 4. 网络连接
netstat -an | grep ESTABLISHED | wc -l

# 5. 文件句柄
lsof -p 12345 | wc -l

六、修复方案

6.1 代码层优化

针对不同的根本原因采取相应措施。

1. 修复死循环

问题代码

1
2
3
4
5
6
7
8
9
10
// ❌ 错误:无条件死循环
public void processData() {
while (true) {
Data data = queue.poll();
if (data == null) {
continue; // 空队列时一直循环
}
process(data);
}
}

修复方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 正确:设置超时或退出条件
public void processData() {
while (!shutdown) {
try {
Data data = queue.poll(1, TimeUnit.SECONDS); // 超时等待
if (data != null) {
process(data);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}

2. 优化复杂正则

问题代码

1
2
3
4
5
// ❌ 错误:回溯爆炸
String regex = "(a+)+b";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher("aaaaaaaaaaaaaaaaX");
matcher.matches(); // 可能耗时极长

修复方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 正确:优化正则表达式
String regex = "a+b"; // 简化正则
Pattern pattern = Pattern.compile(regex);

// 或设置超时
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Boolean> future = executor.submit(() -> {
return pattern.matcher(input).matches();
});

try {
Boolean result = future.get(1, TimeUnit.SECONDS); // 1 秒超时
} catch (TimeoutException e) {
future.cancel(true);
log.warn("正则匹配超时");
}

3. 改用异步处理

问题代码

1
2
3
4
5
6
// ❌ 错误:同步导出大数据
@GetMapping("/export")
public void exportReport(HttpServletResponse response) throws IOException {
List<Order> orders = orderService.findAll(); // 可能 100 万条
ExcelExporter.export(response.getOutputStream(), orders);
}

修复方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ 正确:异步导出 + 分页
@PostMapping("/export")
public String exportReport(@RequestParam String queryId) {
// 提交异步任务
CompletableFuture.runAsync(() -> {
int pageSize = 1000;
int pageNum = 1;
while (true) {
Page<Order> page = orderService.findByPage(queryId, pageNum, pageSize);
if (page.isEmpty()) break;

ExcelExporter.appendToFile("/tmp/export.xlsx", page.getContent());
pageNum++;
}

// 通知用户下载
notificationService.sendDownloadLink(queryId, "/tmp/export.xlsx");
});

return "导出任务已提交,完成后将通知您";
}

4. 优化算法复杂度

问题代码

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 错误:O(n²) 复杂度
public List<User> findDuplicates(List<User> users) {
List<User> duplicates = new ArrayList<>();
for (int i = 0; i < users.size(); i++) {
for (int j = i + 1; j < users.size(); j++) {
if (users.get(i).getEmail().equals(users.get(j).getEmail())) {
duplicates.add(users.get(i));
}
}
}
return duplicates;
}

修复方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 正确:O(n) 复杂度
public List<User> findDuplicates(List<User> users) {
Map<String, User> emailMap = new HashMap<>();
Set<User> duplicates = new HashSet<>();

for (User user : users) {
User existing = emailMap.put(user.getEmail(), user);
if (existing != null) {
duplicates.add(existing);
}
}

return new ArrayList<>(duplicates);
}

6.2 JVM 层调优

调整 JVM 参数优化性能。

1. 调整 GC 参数

G1 GC 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
# 基础配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标暂停时间
-XX:G1HeapRegionSize=16m # Region 大小
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占比

# 年轻代调优
-XX:G1NewSizePercent=20 # 年轻代最小占比
-XX:G1MaxNewSizePercent=40 # 年轻代最大占比

# 并行度
-XX:ParallelGCThreads=8 # 并行 GC 线程数
-XX:ConcGCThreads=2 # 并发 GC 线程数

调优建议

  • ✅ MaxGCPauseMillis 不宜过小(会导致频繁 GC)
  • ✅ 根据实际负载调整 ParallelGCThreads
  • ✅ 监控 GC 日志验证效果

2. 调大年轻代减少 Minor GC

问题:Minor GC 过于频繁

1
2
3
4
# 查看 Young GC 频率
jstat -gcutil 12345 1000

# 如果 YGC 每秒多次,说明年轻代过小

修复

1
2
3
4
# 增大年轻代
-Xmn2g # 固定年轻代大小
# 或
-XX:NewRatio=2 # 老年代:年轻代 = 2:1

3. 排查内存泄漏

如果 Full GC 频繁且回收效果差,可能是内存泄漏。

排查步骤

1
2
3
4
5
6
7
8
9
# 1. 生成 Heap Dump
jcmd 12345 GC.heap_dump /tmp/heapdump.hprof

# 2. 使用 MAT 分析
# 打开 MAT → 导入 heapdump.hprof → Leak Suspects Report

# 3. 查找泄漏对象
# Dominator Tree → 按 Retained Heap 排序
# Path to GC Roots → 找到引用链

参考:详见 JVM 内存泄漏面试题

6.3 架构层改进

从系统架构层面优化性能。

1. 日志改异步

问题:同步日志 I/O 阻塞

1
2
3
4
5
6
<!-- ❌ 错误:同步日志 -->
<Appenders>
<RollingFile name="file" fileName="app.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</RollingFile>
</Appenders>

修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- ✅ 正确:异步日志 -->
<Appenders>
<Async name="async">
<AppenderRef ref="file"/>
</Async>

<RollingFile name="file" fileName="app.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</RollingFile>
</Appenders>

<Loggers>
<Root level="info">
<AppenderRef ref="async"/>
</Root>
</Loggers>

性能提升

  • ✅ 日志写入不阻塞业务线程
  • ✅ 吞吐量提升 20%-50%

2. 报表走离线数仓

问题:实时查询大数据量报表

1
2
3
4
5
6
// ❌ 错误:实时查询 100 万条数据
@GetMapping("/report/sales")
public SalesReport getSalesReport(@RequestParam String date) {
List<Order> orders = orderRepository.findByDate(date); // 100 万条
return salesAnalyzer.analyze(orders); // 复杂计算
}

修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ✅ 正确:预计算 + 缓存
@GetMapping("/report/sales")
public SalesReport getSalesReport(@RequestParam String date) {
// 从缓存获取预计算结果
String cacheKey = "sales_report:" + date;
SalesReport report = redisTemplate.get(cacheKey);

if (report == null) {
// 缓存未命中,从数仓查询
report = dataWarehouse.querySalesReport(date);
redisTemplate.setex(cacheKey, 3600, report); // 缓存 1 小时
}

return report;
}

// 定时任务:每天凌晨预计算
@Scheduled(cron = "0 0 2 * * ?")
public void precomputeReports() {
String yesterday = LocalDate.now().minusDays(1).toString();
SalesReport report = dataWarehouse.querySalesReport(yesterday);
redisTemplate.setex("sales_report:" + yesterday, 86400 * 7, report);
}

3. 引入缓存/读写分离

缓存优化

1
2
3
4
5
6
7
8
9
10
11
// ✅ 使用 Caffeine 本地缓存
private static final Cache<String, Product> productCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();

public Product getProduct(Long id) {
return productCache.get(id.toString(), key ->
productRepository.findById(Long.parseLong(key))
);
}

读写分离

1
2
3
4
5
6
7
8
9
# Spring Boot 多数据源配置
spring:
datasource:
master:
url: jdbc:mysql://master:3306/mydb
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://slave:3306/mydb
driver-class-name: com.mysql.cj.jdbc.Driver
1
2
3
4
5
6
7
8
9
10
11
12
13
// 读操作走从库
@Transactional(readOnly = true)
@DataSource("slave")
public List<Order> queryOrders() {
return orderRepository.findAll();
}

// 写操作走主库
@Transactional
@DataSource("master")
public Order createOrder(Order order) {
return orderRepository.save(order);
}

七、验证与预防

7.1 灰度发布验证

采用渐进式发布,降低风险。

发布流程

1
2
3
4
5
第 1 天:10% 节点 → 观察核心指标
↓ 无异常
第 2-3 天:30% 节点 → 继续观察
↓ 无异常
第 4-7 天:100% 节点 → 全量发布

观察指标

指标 正常范围 告警阈值
CPU 使用率 20%-60% > 80% 持续 3 分钟
RT(P99) < 500ms > 1000ms
QPS 波动 < 20% 下降 > 30%
错误率 < 0.1% > 1%
Full GC 频率 < 10 次/天 > 1 次/小时

回滚预案

  • 如果 CPU 再次飙高 → 立即回滚
  • 如果 RT 显著增加 → 立即回滚
  • 保留旧版本镜像至少 7 天

7.2 监控告警配置

建立完善的监控告警体系。

Prometheus 告警规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
groups:
- name: cpu-alerts
rules:
# CPU 使用率告警
- alert: HighCpuUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 3m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} CPU 使用率超过 80%"
description: "当前 CPU 使用率: {{ $value }}%"

# Load Average 告警
- alert: HighLoadAverage
expr: node_load1 > on(instance) node_cpu_info{cpu="cpu0"} * 2
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }} Load Average 过高"
description: "1 分钟 Load: {{ $value }}"

# 上下文切换告警
- alert: HighContextSwitches
expr: rate(node_context_switches_total[5m]) > 10000
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} 上下文切换过于频繁"
description: "上下文切换速率: {{ $value }}/s"

线程池监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 暴露线程池指标
@Component
public class ThreadPoolMetrics {

@Autowired
private MeterRegistry meterRegistry;

@PostConstruct
public void init() {
Gauge.builder("threadpool.active.count",
threadPoolExecutor::getActiveCount)
.register(meterRegistry);

Gauge.builder("threadpool.queue.size",
() -> threadPoolExecutor.getQueue().size())
.register(meterRegistry);
}
}

7.3 复盘总结

输出完整的复盘报告。

复盘报告模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# CPU 飙高故障复盘报告

## 1. 故障概述
- **发生时间**:2026-01-04 10:00 - 12:00
- **影响范围**:订单服务 3/10 节点
- **影响时长**:2 小时
- **影响用户**:约 5000 用户

## 2. 根因分析
- **直接原因**:订单导出接口存在死循环 bug
- **根本原因**:Code Review 未发现边界条件问题
- **触发条件**:当队列为空时,while(true) 无条件循环

## 3. 处理过程
- 10:05 监控告警 CPU > 90%
- 10:10 定位到高 CPU 线程
- 10:15 摘除故障节点
- 10:30 修复代码并发布
- 11:00 灰度验证通过
- 12:00 全量发布完成

## 4. 改进项
- [ ] 修复死循环 bug(已完成)
- [ ] 增加单元测试覆盖边界条件(进行中)
- [ ] Code Review checklist 增加死循环检查(计划中)
- [ ] 配置 CPU 使用率告警(已完成)
- [ ] 建立压测基线(计划中)

## 5. 经验教训
- 所有 while 循环必须有明确的退出条件
- 队列消费建议使用阻塞队列的 poll(timeout)
- 加强 Code Review,重点关注循环和递归

八、面试回答模板

8.1 标准回答框架

面试官问:“线上服务 CPU 飙高,你如何排查?请描述完整的调优流程”

回答框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
我会按照「止血 → 定位 → 分析 → 修复 → 验证 → 复盘」六步法来处理:

第一步【止血】:
首先通过监控大盘快速定位故障节点,查看 CPU 使用率分布(user/sys/wait)、
Load Average、vmstat 上下文切换等指标。然后采取紧急措施:网关限流/降
级非核心链路、动态扩容,必要时摘除故障节点并重启,优先恢复服务可用性。

第二步【定位】:
精确定位高 CPU 线程。传统方式是 top 找进程 → top -Hp 找线程 → printf
转 16 进制 → jstack 抓堆栈。更高效的方式是使用 Arthas 的 thread -n 5
直接找出 CPU 最高的线程。如果需要深入分析,可以使用 async-profiler 生
成火焰图,直观看到 CPU 热点方法链。

第三步【分析】:
根据线程状态分析原因:
- RUNNABLE:可能是死循环、复杂正则、大量计算、频繁 Full GC
- BLOCKED/WAITING:可能是锁竞争、连接池耗尽、I/O 阻塞
- Sys CPU 高:可能是频繁上下文切换、大量短连接

通过 jstat 查看 GC 频率,jstack 查看锁竞争,vmstat 查看上下文切换,
综合判断根本原因。

第四步【修复】:
针对不同原因采取对应措施:
- 代码层:修复死循环/正则、改用异步处理、优化算法复杂度
- JVM 层:调整 GC 参数、调大年轻代、排查内存泄漏
- 架构层:日志改异步、报表走离线数仓、引入缓存/读写分离

第五步【验证】:
采用灰度发布策略(10% → 30% → 100%),持续观察 3-7 天。重点关注
CPU 使用率、RT、QPS、错误率曲线是否正常,Full GC 频率是否恢复到
正常水平。

第六步【复盘】:
输出复盘报告,包括根因、影响面、改进项。补充监控告警(CPU > 80% 持
续 3 分钟触发告警),配置线程池/连接池监控。完善 Code Review 规范,
CI 集成静态代码扫描,建立压测基线和容量规划,形成工程化闭环。

8.2 加分项

展示深度思考

  1. 提到具体工具链

    1
    2
    3
    "我们团队使用 Prometheus + Grafana 监控 CPU 指标,
    配合 Arthas 在线诊断,async-profiler 生成火焰图,
    形成完整的诊断链路。"
  2. 提到实际案例

    1
    2
    3
    "我之前遇到过订单导出接口的死循环问题,原因是队列
    为空时 while(true) 没有退出条件。通过在 poll 时设
    置超时时间解决,同时增加了队列监控告警。"
  3. 提到性能权衡

    1
    2
    3
    4
    "在修复时要注意性能影响,比如:
    - 异步日志提升了吞吐量,但可能丢失少量日志
    - 增大年轻代减少了 Minor GC,但增加了 Full GC 风险
    - 需要在稳定性和性能之间找到平衡点"
  4. 提到预防措施

    1
    2
    3
    4
    5
    "除了事后处理,我们更注重事前预防:
    - Code Review 强制检查循环和递归的退出条件
    - CI 集成 SonarQube 检测复杂度和潜在死循环
    - 压测时专门监控 CPU 增长趋势
    - 定期(每季度)进行性能专项排查"

九、常见问题

9.1 Load Average vs CPU Usage

区别

对比项 Load Average CPU Usage
定义 单位时间内运行队列中的平均进程数 CPU 使用百分比
包含状态 R(运行)+ D(不可中断睡眠) 仅 CPU 使用时间
受 I/O 影响 ✅ 是(D 状态也计入) ❌ 否
适用场景 综合负载评估 CPU 瓶颈判断

重要提醒

  • ⚠️ Load 高不一定是 CPU 问题,可能是 D 状态进程(I/O 阻塞)
  • ✅ 需要结合 vmstat 的 wa(I/O wait)字段判断
  • ✅ 如果 wa 高,说明是 I/O 瓶颈,不是 CPU 瓶颈

判断方法

1
2
3
4
5
6
7
# Load 高 + wa 高 → I/O 瓶颈
load average: 15.67, 12.34, 10.89
vmstat: wa = 30% # I/O 等待占比 30%

# Load 高 + us 高 → CPU 瓶颈
load average: 15.67, 12.34, 10.89
vmstat: us = 90% # 用户态 CPU 占比 90%

9.2 生产安全注意事项

安全排查原则

  1. 不盲目执行危险命令

    1
    2
    3
    4
    5
    # ❌ 危险:kill -3 会暂停 JVM
    kill -3 <PID>

    # ✅ 安全:使用 Arthas 无损诊断
    java -jar arthas-boot.jar
  2. 不频繁执行 jstack

    1
    2
    3
    4
    5
    # ❌ 不好:频繁 jstack 会影响性能
    while true; do jstack <PID>; done

    # ✅ 好:间隔执行,保留现场
    jstack <PID> > /tmp/jstack_$(date +%Y%m%d_%H%M%S).txt
  3. 优先使用无损工具

    • ✅ Prometheus + Grafana 监控大盘
    • ✅ Arthas 在线诊断
    • ✅ pidstat、vmstat 系统工具
    • ❌ 谨慎使用 jmap、jstack(会暂停 JVM)
  4. 保留现场证据

    1
    2
    3
    4
    5
    # 保存多种诊断信息
    jstack <PID> > /tmp/jstack.txt
    jstat -gcutil <PID> 1000 10 > /tmp/gc.txt
    vmstat 1 10 > /tmp/vmstat.txt
    top -Hp <PID> -b -n 1 > /tmp/top.txt

参考资料

#
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×