4、CPU问题定位:CPU飙高、上下文切换频繁、负载异常的排查方法

CPU出问题,说白了就是它「忙不过来」或者「忙得没效率」。我见过太多同学一看到CPU 100%就慌了,直接重启机器。其实大可不必。咱们一步步来,把问题拆开看。

4.1 先搞清楚:CPU飙高、上下文切换、负载异常,到底有啥区别?

这三个概念经常被混在一起说,但本质完全不同。我习惯用一个比喻来解释:

  • CPU飙高:好比一个工人一直在干活,手没停过。可能是真忙,也可能是在做无用功。
  • 上下文切换频繁:工人刚拿起扳手,又放下换螺丝刀,再放下换锤子。频繁换工具本身就很耗时间。
  • 负载异常:工头给工人派了100个活,但工人只有两只手。活堆在那里,干不完。

你想想看,这三者经常同时出现,但根因可能完全不同。咱们得一个一个查。

4.2 CPU飙高:到底是谁在吃CPU?

CPU飙高是最常见的故障。我遇到过一个案例,线上服务突然从20%飙到95%,用户开始报超时。当时我第一反应不是重启,而是先看看谁在吃CPU。

4.2.1 用 top 命令快速定位

# 直接运行 top,按 P 键按CPU使用率排序
top -c

这里有个小技巧:-c 参数会显示完整的命令行,方便你看到是哪个进程的哪个启动参数。我曾经遇到过一个Java进程,名字叫 java,但其实是别人埋的挖矿程序。看完整命令行才露馅。

4.2.2 找到线程级别的消耗

光知道进程还不够,得知道是哪个线程在搞事情。尤其是Java、Go这类多线程应用。

# 找到进程PID后,查看它的线程
top -H -p <PID>

# 或者用 ps 命令
ps -mp <PID> -o THREAD,tid,time

嗯,这里要注意:top -H 显示的 TID 是十进制的。如果你要 dump 线程栈,很多工具需要十六进制。转换一下:printf "%x\n" <TID>

4.2.3 火焰图:一眼看出热点

我个人非常推荐火焰图。它能把CPU时间花在哪个函数上一目了然。

# 使用 perf 采样
perf record -F 99 -p <PID> -g -- sleep 30
perf script > out.perf

# 生成火焰图(需要 FlameGraph 工具)
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > cpu.svg

火焰图怎么看?「平顶宽」的函数就是热点。如果看到 syscall 或者 lock 相关的调用占了大头,那问题可能不在业务逻辑,而在系统层面。

核心思路:CPU飙高排查三步走——进程级 → 线程级 → 函数级。别跳步,跳步容易漏信息。

4.3 上下文切换频繁:系统在「空转」

上下文切换本身不是坏事,但频繁切换意味着CPU大量时间花在了「换人」上,而不是真正干活。

4.3.1 怎么看上下文切换?

# 查看系统整体上下文切换
vmstat 1

# 重点关注 cs(context switch)列
# 如果 cs 持续超过 10万/秒,基本可以判定有问题

# 查看具体进程的上下文切换
pidstat -w 1

我记得有一次排查一个消息队列服务,CPU不高(30%左右),但吞吐量上不去。一看 vmstat,cs 列飙到 20万/秒。再查 pidstat -w,发现某个线程的 cswch/s(自愿上下文切换)特别高。

4.3.2 自愿切换 vs 非自愿切换

类型 含义 常见原因
自愿切换 (cswch) 线程主动让出CPU,比如等待I/O、锁、sleep 锁竞争激烈、I/O等待过多
非自愿切换 (nvcswch) 时间片用完,被系统强制调度 线程数过多、CPU资源不足

怎么区分?pidstat -w -p <PID> 1 会分别显示 cswch/snvcswch/s

避坑指南:我曾经遇到一个案例,非自愿切换很高,但CPU并不高。原因是开了几百个线程,每个线程只做一点点计算就休眠。系统调度器疲于奔命。解决方案很简单——用线程池限制线程数。

4.4 负载异常:CPU不忙,但活干不完

负载(Load Average)是个容易被误解的指标。很多人以为负载高就是CPU忙,其实不一定。

4.4.1 负载的本质

负载 = 正在运行的进程数 + 等待运行的进程数 + 等待I/O的进程数。

所以负载高可能有三种情况:

  • CPU密集型:CPU使用率也高,好排查
  • I/O密集型:CPU不高,但进程都在等磁盘或网络
  • 锁竞争:进程在等锁释放,CPU可能也不高

4.4.2 区分CPU负载和I/O负载

# 看 CPU 使用率和负载的对比
# CPU 低,负载高 → 大概率是 I/O 问题
top

# 进一步确认 I/O 等待
iostat -x 1

# 看 %iowait 列,如果持续超过 10%,说明磁盘是瓶颈

我遇到过最典型的一个场景:数据库服务器CPU只有20%,但负载到了30。查了半天,发现是磁盘在做全表扫描,大量进程在等磁盘读数据。加了个索引,负载直接降到2。

4.4.3 用 uptime 看趋势

uptime
# 输出示例:load average: 2.34, 1.89, 1.56

三个数字分别对应1分钟、5分钟、15分钟的平均负载。如果1分钟远高于15分钟,说明问题是最近才出现的,可能是突增流量或代码刚上线。如果三个数字都高且平稳,说明系统已经持续过载很久了。

注意:负载的「高」是相对的。8核机器负载8不算高,2核机器负载8就快挂了。一定要结合CPU核数来看。一般建议负载不超过核数的70%。

4.5 综合排查:一个真实案例

最后分享一个我印象很深的案例。某天晚上,线上告警:CPU 95%,负载 40(32核机器)。

我的排查过程:

  1. top 看进程:发现是 Nginx 进程吃CPU。奇怪,Nginx 一般不会这么高。
  2. perf 采样:生成火焰图,发现热点在 ngx_http_upstream_check 模块。
  3. 查配置:发现健康检查间隔设置成了 100ms,而且后端有200个上游。相当于每秒发 2000 个探测请求。
  4. 改配置:把健康检查间隔改成 1s,CPU 直接降到 15%。

你看,问题其实很简单。但如果不看火焰图,光靠猜,可能折腾一晚上都找不到根因。

4.6 总结:我的排查清单

遇到CPU问题,我一般按这个顺序来:

  • 先看 top,确认是哪个进程
  • 再看 vmstat,确认是CPU忙还是I/O忙
  • 然后用 pidstat -w 看上下文切换类型
  • 最后用 perf火焰图 定位热点函数

说白了,排查CPU问题就像破案。别急着下结论,多收集证据。工具就这些,关键是你得知道什么时候用哪个。