前言
"CPU 飙到 100% 怎么排查"是 SRE 面试的必考题,但十个候选人八个开口就是工具连招:top、vmstat、iostat、pidstat、perf------报得挺顺,方向全错。本文先拆掉"CPU 100% = 故障"这个隐含前提,给出三种性质的定性判断,再按 us/sy/wa/si 四分法展开每一条排查命令的实际用法,最后附一个最邪门的真实案例:sys 90%、strace/iostat/数据库视图全部干净,根因是大内存进程 fork 触发的内核页表锁竞争。
一、先定性:CPU 100% 有三种性质,只有一种算故障
| 性质 | 特征 | 该做什么 |
|---|---|---|
| ① 压测/容量测试故意打满 | 你自己打上去的 | 记录饱和点和 P99 拐点,这是容量规划的输入,不是告警 |
| ② 业务增长顶到边界 | 曲线跟着 QPS/连接数慢慢爬 | 走扩容、加缓存、拆读写------容量流程,不是救火 |
| ③ 突变打满、与业务量对不上 | 平时 30% 突然 100%;或半夜没人用反而满 | 只有这种才轮到 top 和下面的四分法 |
面试官追问一句"这 100% 是不是你想要的",答不上来就露馅------这是本题真正的分水岭。
还有一个口径陷阱:top 第一行 %Cpu(s) 是整机所有核汇总 ,进程列表里的 %CPU 是单核基准(可跑到核数×100)。"CPU 100%" 必须先问清是整机满还是单核满,两者给出的扩容建议完全相反。
二、us 高:CPU 真在替应用算(命令实操)
us 是用户态代码消耗,四档里唯一一种"加机器理论上真能缓解"的情况。标准动作链:
# 1. 锁定进程(按 CPU 排序)
top # 大写 P 按 CPU 排序
# 2. 下钻线程
top -Hp <pid> # 看哪个线程最烧
# 3. 线程号转十六进制(Java 栈对齐用)
printf "%x\n" <tid>
# 4. 快速看热点函数(30 秒出结论)
perf top -p <pid>
# 5. 留证据出火焰图
perf record -F 99 -g -p <pid> -- sleep 30
perf script > out.perf
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > flame.svg
FlameGraph 工具安装:
# perf 本体(Debian/Ubuntu 与 RHEL 系)
apt install linux-tools-common linux-tools-$(uname -r) # 或 yum install perf
# 火焰图渲染脚本
git clone https://github.com/brendangregg/FlameGraph.git
export PATH=$PATH:$(pwd)/FlameGraph
火焰图只看一个东西:平顶。最宽的平台就是最烧 CPU 的函数。常见元凶:死循环、正则回溯、频繁序列化、GC 压力。Java 加一手 jstack,线程栈跟火焰图对上,能定位到代码行。
三、sy 高:几乎全是应用侧行为,先判诱因
CPU 不会自己进内核------sy 高背后全是应用在发系统调用。值钱的是先判是哪一类诱因,因为处理方向完全不同:
| 诱因 | 判别命令 | 看什么 | 处理方向 |
|---|---|---|---|
| ① 上下文切换风暴 | vmstat 1、pidstat -wt 1 |
cs 列每秒几万次警惕、几十万次基本是风暴 | 减线程、降锁粒度、连接复用 |
| ② syscall 密集 | strace -c -p <pid>(采样 10s) |
syscall 按次数/耗时排序,read/write/fsync 占比 | 合并 IO、批量写、降 fsync 频率 |
| ③ 单点堵塞连锁等待 | pidstat -wt、应用侧锁/会话视图 |
大量线程卡在同一把锁/同一个等待事件 | 找到阻塞源头,其他都是治标 |
| ④ 大内存进程被高频 fork | perf record -g(见第五节) |
内核页表函数占大头 | 禁高频 fork / 进程池 / HugePage |
两个实操注意:
- strace 在生产要慎用 ------它会让目标进程每次系统调用都陷入停顿,高并发服务挂上 strace 本身就能把性能打崩。粗排用
strace -c(统计模式,开销小),细查可用perf trace或 bpftrace 这类低开销方案。 - 关键判断:sy 高盲目扩容几乎一定是错的。锁竞争越扩越凶,线程越多切换越多,机器多只是把同一个问题复制更多份。
四、wa 高:八成是访问模式问题,不是磁盘问题
wa 的准确定义:"CPU 空闲、且至少有一个进程在等 IO"的占比。wa 高时 CPU 其实是闲着的------所以 wa 高和总使用率低,常常是同一件事的两种说法。
# 磁盘层
iostat -x 1 # 看 %util / await / aqu-sz
# 进程层
iotop -oP # 只看有 IO 的进程
组合读法(这是最容易翻车的陷阱):
- %util 逼近 100 → 磁盘真被打满,考虑换盘/分流;
- %util 还低但 await 已在涨 → 不是磁盘瓶颈,是访问模式问题:大查询、逐行提交、碎 IO、日志狂刷、归档抢占。落点是改访问模式(拆查询、加索引、合并碎 IO、批量写、调刷盘策略);
- wa 要结合核数看:4 核机一个进程等 IO 就能顶 100%,64 核机同样负载可能只有 2%------单看 wa 会误判。
关键判断:wa 高加 CPU 一点用没有------CPU 现在是闲的,加多少都还是闲着等 IO。
五、si 高与一次 sys 90%、全部工具失效的排查
si 是软中断,主要来自网络收包:
mpstat -P ALL 1 # si 集中在哪几颗核
cat /proc/softirqs # 中断类型分布
sar -n DEV 1 # 收包速率
cat /proc/interrupts # 各 IRQ 挂在哪些核
最常见根因不是流量大,是中断亲和没收好------NET_RX 全压一两颗核,那颗核 si 100%、整机才 5%。解法是网卡多队列 + RPS/RFS(跟应用代码无关):
# 把某个 IRQ 绑到指定核(写掩码)
echo 2 > /proc/irq/24/smp_affinity
# RPS: 软件层把收包处理摊到多核(每队列掩码, f = 低 4 核)
for q in /sys/class/net/eth0/queues/rx-*; do echo f > $q/rps_cpus; done
# RFS: 让协议栈把包送到应用所在核
sysctl -w net.core.rps_sock_flow_entries=32768
for q in /sys/class/net/eth0/queues/rx-*; do echo 4096 > $q/rps_flow_cnt; done
# 32768 = 队列数 × 每队列 rps_flow_cnt
顺手看一眼 st(steal):那是宿主机抢走的 CPU,云上超卖常见,你修不了,只能报云平台。
真实案例:sys 90%,strace / iostat / AWR 全干净
MySQL 实例 CPU 跑满,sys 占 70%~90%、us 很低。套路查遍全部干净:慢查询日志干净、QPS 平稳、strace -c 看系统调用频次正常、iostat 磁盘不忙、内存充足。但 sys 就在那儿。
根因:定时备份脚本用 fork() 拉子进程跑 mysqldump,而 mysqld 占了 40GB 内存。fork 不拷贝物理内存,但内核必须遍历复制全部页表 ------几十万个页表条目每次 fork 都要过一次全局页表锁,CPU 全烧在 ptep_clear_flush 这类页表函数上。mysqld 自己没有高频系统调用,开销全在 fork 那一刻的内核遍历里。
验证:停掉定时 fork 任务,sys 十秒内回落。同一坑在 Oracle 上又踩一次(SGA 64G + fork 型监控 Agent),这次 AWR 完全看不到------瓶颈在 OS 内核层,数据库内部视图根本抓不到。
这类问题四个共同特征:sys 高 us 低 / 全部常规工具干净 / 机器上有几十 GB 大内存进程 / 有定时任务在 fork。唯一解法是 perf 开内核调用栈:
perf record -g -a -- sleep 30 # -g 关键: 记录调用栈, 包括内核栈
perf report
# 会看到 ptep_clear_flush / copy_page_range 这类页表函数占大头
只开普通 perf 看函数名,你只会看到一堆看不懂的内核符号。修法三选一:LVM snapshot 替代 mysqldump 定时 fork / 缩小进程内存 / posix_spawn 替代 fork / 启用 HugePage 压页表数量。一句话教训:大内存进程不要高频 fork。
六、load 高 ≠ CPU 高:先抓 D 状态进程
最容易翻车的追问:load 50 但 CPU 才 20%,一堆人慌着加机器。
ps -eo stat,pid,cmd | grep -E "^D" # 抓不可中断睡眠进程
D 状态在等 IO 或等锁,这种负载加多少机器都下不来,瓶颈不在算力。真正 CPU 型的高 load,才回到上面四分法。
总结
先判性质(压测打满/业务饱和/突变打满,只有第三种算故障)→ 整机满还是单核满 → us 查应用(进程→线程→火焰图→代码行)→ sy 先判四类诱因(切换风暴/syscall 密集/堵塞连锁/fork 页表锁)再定处理 → wa 改访问模式不是加 CPU(%util vs await 组合读)→ si 查中断亲和与多队列 → load 高先抓 D 状态。工具谁都会背,能把"为什么是这个顺序"讲清楚,面试官就知道你在生产上真值过班。