适用场景: 告警系统上报 CPU 使用率超过阈值, 需逐层定位至代码行级 前置知识: Linux 进程/线程模型, CPU 时间片调度, JVM 线程模型


一、为什么需要四层诊断模型

1.1 理论依据: 故障定位的"层级原则"

一个 CPU 告警可能由多个层面的问题引起, 每一层需要不同的工具来观测:

系统层 (kernel)    → /proc/stat, vmstat      — 看调度队列和 CPU 时间分配
进程层 (process)   → top, /proc/<pid>/stat    — 看进程级别的 CPU 消耗
线程层 (thread)    → top -H, /proc/<pid>/task — 看线程级别的 CPU 消耗
代码层 (code)      → jstack, perf, Arthas     — 看具体的执行方法/指令

选择依据: 每一层只看上一层无法提供的信息。系统层告诉你"CPU 在忙", 但不告诉你是谁; 进程层告诉你"是 PID 172 在忙", 但如果你直接跳到代码层(比如盲目 jstack), 你拿到 30 个线程的栈, 却不知道该看哪一个——这就是为什么需要线程层: 先找到具体的线程 TID, 再精准定位。

1.2 工具选择矩阵

层面工具核心指标为什么用它而不用其他
系统-时间分配topus/sy/wa/st/id直观分解 CPU 时间去向, 一步锁定瓶颈类型
系统-调度队列vmstatr, b, cs唯一能直接观测运行队列长度和上下文切换率的工具
进程top -p <pid>%CPU, RES确认具体进程的资源占用是否持续
线程top -H -p <pid>%CPU per thread将进程的 CPU 时间拆解到线程粒度
代码jstack <pid>线程栈, 线程状态连接 OS 线程 ID 和 Java 方法调用栈的唯一桥梁

二、分层操作指导

2.1 第一层: 全局观测(top)

操作

top -bn1 | head -20      # 非交互式, 抓取单次快照
top -d 1                 # 交互式, 每秒刷新

输出详解

第一行 — 系统概览
top - 14:32:05 up 120 days, 3:15, 2 users, load average: 8.50, 6.20, 3.80
字段数据来源含义异常判据
up 120 days/proc/uptime系统持续运行时间重启后 uptime 归零, 排查时注意是否是重启后冷启动压力
load average/proc/loadavg过去 1/5/15 分钟的平均可运行线程数 + 不可中断睡眠线程数持续 > CPU 核数 = 系统过载; 三个数字递增说明问题在恶化, 递减说明在恢复

load average 的深层含义:

Linux 内核在每次时钟中断时统计以下三类线程的数量, 累加后取指数移动平均:

  1. TASK_RUNNING — 正在运行或在运行队列中等待的线程
  2. TASK_UNINTERRUPTIBLE — 等待磁盘 I/O、内核锁等的线程(不可被信号打断)

因此 load average 实际上衡量的是"想要 CPU 时间的线程数"——包括正在用的和排队等的。它不是 CPU 使用率, 而是 CPU 的"饥饿指数"。

第三行 — CPU 时间分解(最核心)
%Cpu(s): 95.2 us, 3.1 sy, 0.0 ni, 1.0 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
指标统计来源含义升高时的排查方向
us用户态指令执行时间应用程序代码消耗top -H + jstack 定位方法; 或 perf top 定位热点函数
sy系统调用 + 内核代码执行时间内核态消耗strace -c -p <pid> 统计系统调用频率和耗时; 大量 futex → 锁竞争; 大量 epoll_wait → 网络 I/O
wa至少一个 I/O 在进行时 CPU 空闲的时间等待磁盘/网络 I/Oiostat -x 1 定位具体磁盘; iotop 定位进程; 检查 swap si/so
id完全空闲CPU 空闲正常值应该 > 0; 持续为 0 说明 CPU 已打满
hi/si硬件中断/软件中断处理中断处理消耗si 持续 >5% → 网络包处理密集; hi 突增 → 硬件问题
st虚拟机被宿主机"偷走"的时间虚拟化环境 CPU 争抢只在虚拟化环境中出现; >0 说明宿主机超卖或有 noisy neighbor

典型场景判断速查:

ussywast诊断结论
>80%<10%≈0%0%应用代码 CPU 密集型计算
>60%>30%≈0%0%大量系统调用, 可能是锁竞争或频繁 I/O 操作(futex/read/write)
<30%<10%>40%0%磁盘 I/O 瓶颈, 某进程等待磁盘
<30%<10%≈0%>20%虚拟化环境 CPU 被宿主机限制
正常正常正常正常CPU 不是瓶颈, 考虑内存/网络/应用逻辑
进程列表关键列
  PID USER   PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 4823 tomcat 20   0   8.2g   4.1g   18m S 295.3  13.1   1245:32 java
含义编号多核环境
%CPU进程的 CPU 使用百分比可以超过 100% —— 295% 表示占用了约 3 个 CPU 核心
S进程状态R=Running, S=Sleeping, D=Uninterruptible sleep, Z=Zombie
TIME+进程累计 CPU 时间如果这个数字增长极快, 说明进程长期占用 CPU
RES实际驻留物理内存结合 free 判断是否是内存问题

2.2 第二层: 调度队列验证(vmstat)

为什么需要这一步

top 回答"CPU 在做什么", 但不回答"CPU 是否够用"。这是运维中最常见的判断失误: CPU 使用率 90% 不一定需要扩容, 使用率 30% 也不一定就不需要。CPU 是否够用的唯一判据是: 有没有人在排队。

操作

vmstat 1 5              # 每秒采样一次, 共 5 次
vmstat 1                 # 持续采样, Ctrl+C 停止
vmstat -s                # 显示自系统启动以来的累计统计(看趋势)

输出详解

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs  us sy id wa st
 9  1  708096 2150400 4194304 8388608  0    0    12   150 35000 85000 94  3  2  1  0
procs — 进程调度指标(最重要的两列)
含义数据来源异常判据
r运行队列长度: 正在运行 + 等待 CPU 的进程数内核调度器统计持续 > CPU 核数 = CPU 瓶颈, 需要扩容或优化代码
b不可中断睡眠进程数: 等待 I/O、内核锁等的进程数内核调度器统计持续 > 5 = I/O 瓶颈或锁竞争严重

r 列的深层含义:

Linux 的 CFS(完全公平调度器)为每个 CPU 维护一个运行队列。r 是所有 CPU 运行队列上的任务总数。当一个 CPU 密集任务被调度器分配了时间片, 它从队列中移除; 时间片用完后重新入队。如果队列长度持续大于 CPU 核数, 意味着总有线程在等待 CPU 时间片——这就是用户感知到的"系统响应变慢"。

为什么不用 load average 替代 r?

load average 是平滑后的指数移动平均值, 且包含了不可中断睡眠进程(b), 无法区分"在等 CPU"还是"在等磁盘"。vmstat 的 r 列是瞬时裸数据, 且与 b 分开统计, 可以精准定位瓶颈类型。

system — 系统级活动指标
含义异常判据
in每秒中断次数突然大幅波动 → 检查硬件或网络
cs每秒上下文切换次数> 50000/核 = 线程过多或锁竞争严重

cs 的深层含义:

上下文切换涉及保存/恢复寄存器、刷新 TLB、切换页表, 每次切换消耗约 1-5μs。如果每秒切换 10 万次, 仅开销就占 10%~50% 的 CPU 时间。cs 高 + sy 高 → 锁竞争(大量 futex 系统调用); cs 高 + sy 正常 → 线程数过多, 考虑减少线程池大小或使用协程。

swap — 内存换页指标
含义异常判据
si (swap in)每秒从交换区读入内存的 KB 数持续 > 0 = 严重内存不足, 系统在从磁盘换回页面, 性能会急剧下降
so (swap out)每秒写出到交换区的 KB 数持续 > 0 = 物理内存不足, 系统被迫将内存页面写出到磁盘

关键判断: si/so 都为 0 不代表内存无压力。swpd(已使用的 swap 总量) > 0 说明历史上发生过内存压力, 需要结合 free 命令进一步分析。

io — 磁盘 I/O 指标
含义异常判据
bi每秒从块设备读入的块数(KB)持续 > 10000 → 大量读 I/O
bo每秒写出到块设备的块数(KB)持续 > 10000 → 大量写 I/O

注意: vmstat 的 bi/bo 是粗略指标, 看到异常后应使用 iostat -x 1 做进一步精确定位(wait, svctm, %util 等指标)。


2.3 第三层: 线程级分解(top -H)

理论依据: 进程与线程的关系

在 Linux 中, 线程是调度的最小单位。一个 Java 进程(对应 JVM 实例)内部有数十到数百个线程: 业务线程(Tomcat worker)、GC 线程、JIT 编译线程、RMI 线程等。进程级别的 %CPU 是所有这些线程的总和。top -H 将进程展开为线程列表, 每个线程独立一行。

操作

top -H -p <pid> -bn1 | head -20    # 非交互式, 一次快照
top -H -p <pid>                     # 交互式, 按 CPU 排序
# 在交互式 top 中, 按 f 选择排序列, 按 J 按 CPU 排序

输出解读

  PID USER      PR  NI  S  %CPU  %MEM     TIME+ COMMAND
 5021 tomcat    20   0  R  98.2  13.1     42:15 java
 5022 tomcat    20   0  R  97.8  13.1     41:58 java
 5023 tomcat    20   0  R  96.5  13.1     41:32 java
 4823 tomcat    20   0  S   0.3  13.1   1245:32 java       ← 主线程
观察点含义结论
3 个线程状态都是 R它们在运行队列中(正在执行或排队等 CPU)业务线程而不是 GC 线程
每个线程 %CPU ≈ 99.9%每个线程占满一个核心CPU 密集, 不是间歇性的锁等待
线程名(later via jstack)如果是 http-nio-* → Tomcat 请求线程; 如果是 GC task → GC
主线程(PID=进程PID) %CPU ≈ 0%主线程负责调度, 不执行具体业务正常

关键区分: 业务线程 vs GC 线程

GC 线程的 CPU 使用是间歇性的——GC 触发时短暂飙升, GC 结束后立即回落。如果 top -H 观察到持续的 100% CPU(持续超过 30 秒), 基本不是 GC。验证方法: jstat -gc <pid> 1000 观察 YGC/FGC 的频率和 FGCT 是否有同步的尖峰。


2.4 第四层: 代码行定位(jstack)

理论依据: OS 线程 ID 与 Java 线程的映射

JVM 的每一个 Java 线程(Thread 对象)在底层对应一个 OS 原生线程(pthread)。jstack 输出中的 nid(Native Thread ID)就是该原生线程在操作系统中的线程 ID (以十六进制表示)。top -H 显示的 PID (实际上是 TID, Thread ID) 是十进制的 OS 线程 ID。

映射关系: top -H 的 TID (十进制) = jstack 的 nid (十六进制)

操作

# 1. 从 top -H 获取线程 TID
TID=5021

# 2. 十进制转十六进制
printf "%x\n" $TID
# 输出: 139d

# 3. 导出线程栈
jstack <pid> > /tmp/jstack_<pid>.txt

# 4. 精确匹配目标线程
grep -A100 "nid=0x139d" /tmp/jstack_<pid>.txt

jstack 线程栈解读

"http-nio-8080-exec-15"  ← 线程名 (Tomcat NIO 工作线程 #15)
    #67                  ← 线程编号 (JVM 内部)
    daemon               ← 是守护线程
    prio=5               ← Java 线程优先级 (1~10)
    os_prio=0            ← OS 线程优先级
    tid=0x00007f8a0010b800  ← Java 层面的线程对象地址
    nid=0x139d           ← ★ OS 原生线程 ID (十六进制), 对应 top -H 的 TID=5021
    runnable             ← 线程状态

java.lang.Thread.State: RUNNABLE    ← JVM 线程状态
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.lang.String.replaceAll(String.java:2223)
    at com.example.demo.utils.DataSanitizer.sanitize(DataSanitizer.java:21)
    at com.example.demo.controller.ReportController.exportReport(ReportController.java:30)
    ...

读栈方向: 从下往上。最下面是入口(HTTP 请求进入), 最上面是当前执行点(正则引擎内部)。

JVM 线程状态速查表

状态JVM 定义OS 层表现排查意义
RUNNABLE线程在 JVM 中正在执行可能在真正执行, 也可能在等 OS 分配 CPU 时间片CPU 高 → 计算密集, 看栈顶方法; CPU 不高 → 可能是等 OS 资源(network/socket accept)
BLOCKED等待获取 monitor 锁(synchronized)睡眠, 等待锁释放waiting to lock <0x...>, 然后搜索持有该锁的线程(locked <0x...>)
WAITINGObject.wait() / LockSupport.park() 无超时睡眠自行 wait 不算异常; 但大量 WAITING 在同一对象 → 线程泄漏
TIMED_WAITINGsleep(ms) / wait(ms) / parkNanos()睡眠, 定时唤醒通常是线程池空闲线程或定时任务, 正常; 但如果数量远超线程池配置 → 线程泄漏

如何识别正则回溯的栈签名

正常的方法调用在栈上是树状结构, 不会出现大量重复。正则回溯的特征是同样的几个类在栈上循环重复数十到数百次:

Pattern$Loop.match        ←
Pattern$GroupTail.match    │
Pattern$Curly.match        │  这 4 个类重复出现
Pattern$GroupHead.match    │  几十到几百次
Pattern$Loop.match        ← 这是 NFA 引擎在尝试每一种分组合并
Pattern$GroupTail.match
Pattern$Curly.match
...

一旦看到这种重复模式, 立即可以确认是正则回溯导致 CPU 打满。


三、完整证据链模板

每次故障定位应产出如下格式的证据链:

[告警时间] 2026-06-10 14:30, node-03, CPU > 90% 持续 5 分钟

[Step 1: 全局观测] top -bn1
  → us=95.2%, wa=0%, sy=3.1%, id=1.0%
  → 结论: 纯用户态 CPU 问题, 排除磁盘/虚拟化/系统调用风暴

[Step 2: 调度验证] vmstat 1 5
  → r=9~12 (> CPU 核数 8), b=0~2, si/so=0, cs=~85000
  → 结论: 运行队列严重堆积, CPU 过载, 排除 I/O 和内存换页

[Step 3: 线程定位] top -H -p 4823
  → 线程 5021/5022/5023 各占 ~97% CPU, 状态 R
  → 结论: 3 个业务线程 (http-nio-*) 是 CPU 消费者

[Step 4: 代码定位] jstack 4823 | grep "nid=0x139d"
  → DataSanitizer.sanitize(:21) → String.replaceAll()
  → 回溯签名: Pattern$Loop→GroupTail→Curly→GroupHead 循环
  → 结论: 正则回溯导致 CPU 打满

[根因] DataSanitizer.sanitize() 中 String.replaceAll() 使用回溯正则 (a+)+b,
       3 个用户同时调用 Excel 导出接口时触发了灾难性回溯

[修复] 1.预编译 Pattern; 2.限制输入长度; 3.换用非回溯方式匹配

四、常见误区

误区正确理解
load average 高 = CPU 不够load average 包含了等待 I/O 的进程, 也可能是磁盘瓶颈。必须结合 vmstat r/b 拆开看
us 低 = CPU 没问题wa 高时 CPU 同样饱和, 只是瓶颈不在计算而在 I/O; sy 高时是内核态瓶颈
wa 高 = 磁盘坏了wa 表示"CPU 空闲但同时有 I/O 进行中", 如果 CPU 忙于处理其他进程, wa 可能很低但磁盘依然繁忙
r 列偶尔高一下没关系对延迟敏感的服务(tomcat http handler), r>CPU核数 的每一秒都在增加 P99 延迟
jstack 看到 RUNNABLE = 正常运行RUNNABLE 只是说线程可以被调度, 不代表正在执行——它可能在运行队列里排队等 CPU
jstack 一次就够了高 CPU 场景应连续抓 3 次, 间隔 2 秒。如果三次栈顶都是同一个方法, 才是稳定热点; 如果在不同方法间跳动, 需要分析整体调用链

五、附录: 命令速查卡片

# === 全局 ===
top -bn1 | head -20                  # CPU 宏观时间分布 + 最耗 CPU 进程
nproc                                # 查看 CPU 核数 (判断 load/r 是否过载的前提)

# === 调度 ===
vmstat 1 5                           # CPU 调度队列 + 上下文切换 + 换页 + I/O

# === 线程 ===
top -H -p <pid> -bn1 | head -15      # 进程展开为线程, 找高 CPU 线程

# === 代码 ===
printf "%x\n" <tid>                  # 十进制 TID → 十六进制 nid
jstack <pid> | grep -A60 "nid=0x<hex>"   # OS 线程 → Java 方法栈

# === 辅助 ===
jstack <pid> > /tmp/js.txt           # 导出线程栈到文件, 避免频繁拉取
jstat -gc <pid> 1000 5               # GC 活动监控 (排除 GC 导致的 CPU 尖峰)