CPU飙高问题排查实战指南:从指标观测到根因定位的完整方法论 📅 发布时间:2026/9/15 15:33:16 👁 浏览次数: 我们直接进入正题。这一章聊的是 CPU 飙高问题的排查背景是我在实际运维和性能调优过程中反复用到的一套方法。上一章我梳理了通用故障排查的三个阶段先看现象、再圈范围、最后定位根因。这一章就把 CPU 这个维度单独拎出来做一次实战拆解从指标认识到工具链使用再到真实案例复盘争取让你下次遇到 CPU 高的时候不再手忙脚乱。很多刚接触排查的朋友一看到 CPU 100% 就紧张第一反应是想去重启或者直接杀进程。我的建议是先压住这个冲动CPU 飙高是结果不是原因。花十分钟理清楚是谁在消耗 CPU、消耗在内核还是用户态、是持续性的还是瞬时峰值往往比盲目重启要有效得多。下面我按实际排查的顺序来讲每一步都会说明为什么这么做。1. 拿到CPU飙高告警先分清是高在哪一层1.1 先用 top 建立第一印象但不急着杀进程不管现场是 Linux 服务器还是 Windows 物理机我都会先用系统自带工具看全局。以 Linux 为例登录后的第一个命令基本都是top按大写 P 让进程按 CPU 占用率排序。这一步要做的事情很简单找出 CPU 占用率最高的前几个进程记下它们的 PID、进程名和 CPU 占用率同时看一眼整个系统的 load average 和 CPU 使用率的分布。这里有个容易忽略的点top里的%CPU默认是进程从启动到当前的平均值不是瞬时值。所以你看到某个进程显示 200%、300%不一定代表它当前真的在疯狂消耗有可能是历史累积。我通常会在 top 界面按大写 H 切换到线程视图再按 P 排序配合1键查看每个逻辑核的使用率这样能更快确认到底是不是某个线程在持续占用。Windows 上对应的操作是打开任务管理器进到“性能”选项卡看 CPU 使用率再切到“进程”选项卡按 CPU 排序。不过任务管理器在某些场景下不够精确特别是你想看到底是哪个线程在跑。更推荐用 Process Explorer 或者 Windows 自带的资源监视器后面我会专门用一节说 Windows 平台的做法。1.2 us、sy、wa、st这四个字段决定了排查方向top顶部的 CPU 状态行里有一组百分比字段分别是 us、sy、ni、id、wa、hi、si、st。很多人扫一眼就过去了但这几个数字其实是定位方向的关键。我通常会重点关注四个us、sy、wa、st。ususer space高说明用户态程序在大量消耗 CPU比如业务代码里的死循环、密集计算、正则回溯、GC 线程等。方向是查进程内部的线程热点。sysystem高说明内核态占据了大量 CPU比如系统调用过于频繁、中断处理异常、上下文切换过多、锁竞争激烈。方向是查系统调用、内核线程、中断分布。waI/O wait高说明 CPU 在等待磁盘、网络等 I/O 操作完成。方向是查磁盘读写、swap 换页、存储设备健康状态。ststeal time高常见于虚拟化环境说明宿主机上的其他虚拟机在抢占 CPU 资源你的虚机在“被动等待”。方向是联系云服务商或查看宿主机负载。这四个方向决定了后续用哪一套工具组合。如果 us 高你再去查磁盘 I/O 就是浪费时间如果 wa 高你盯着进程代码也很难看出问题。我自己的习惯是先瞄一眼这四个字段再决定走哪条支线等于是在地图上先把路径确定下来。1.3 用 vmstat 和 mpstat 交叉验证用户态、内核态与等待top给出的是一个瞬时截面还不够稳定。我一般会在top看第一眼之后立刻跑一组vmstat和一个mpstat采样用多个角度的数据来做交叉验证。vmstat 1 5表示每 1 秒采样一次总共 5 次。重点看r运行队列长度、cs上下文切换次数、in中断次数、si/soswap 换入换出、waI/O 等待。如果r持续大于 CPU 核心数说明 CPU 已经处于过载状态如果cs高到几十万说明系统主要时间可能花在线程切换上如果si/so不为 0说明内存压力已经传导到了磁盘交换这会间接导致 CPU 飙高。mpstat -P ALL 1可以逐个 CPU 核查看使用率。这一步的意义在于确认是所有核一起高还是某个核被打满。如果是单核吃满但整体使用率不高那多半是某个单线程程序绑定到了固定核心或者是中断都打到了同一个 CPU 上。如果是整体所有核都高那就是并行负载过重通常是业务代码问题或者资源争抢。这三个工具跑完我基本能形成一个初步判断CPU 高在哪个层级是用户态、内核态、等待还是被抢占。接下来才是真正定位具体元凶的阶段。2. 从进程到线程再到代码把CPU大户揪出来2.1 确定肇事进程后用 top -H 找到具体线程宏观层面判断完之后就要把范围缩小到进程和线程。假设top里某个 Java 进程的 PID 是 12345CPU 占用率特别高下一步我通常执行top -Hp 12345这样就能看到这个进程内部所有线程的 CPU 占用情况。按 P 排序后找到最靠前的那个线程记下它的线程 IDTID。这里要特别留意Linux 里的线程 PID 实际上就是线程 ID后续所有定位操作都靠这个数字。如果当前是 Glibc 2.30 以下的版本ps -L -p 12345也能列出线程四列中的 SPID 就是线程号。拿到线程号以后把它转成十六进制因为绝大多数线程栈工具的线程标识是用十六进制显示的。比如 TID 是 12345转成十六进制是 0x3039转换工具很多printf %x 12345就能直接算出来。2.2 线程栈转储Java用jstackC/C用gdb拿到线程的十六进制 ID 后接下来的操作取决于你用的运行时环境。如果是 Java 应用我会执行jstack 12345 /tmp/stack.txt然后在 dump 文件里搜索之前算出来的十六进制线程 ID。jstack 输出的 nid 字段就是十六进制线程号找到以后往上翻几行就能看到这个线程当前在执行的代码包括类名、方法名、行号以及整个调用链。这里有一个实际容易被坑的点jstack 抓的是采样那一刻的线程栈但 CPU 高通常是持续性的你应该多抓几次来对比。我一般间隔 2 到 3 秒连续抓 5 次如果每次栈顶都指向同一个方法打击目标基本确定了如果栈各不相同那可能是多个线程轮换执行或者线程池在频繁创建新任务。如果是 C/C 程序最常用的是gdb -p PID附加进去然后执行thread apply all bt打印所有线程的堆栈。不过生产环境直接上 gdb 要慎重一方面它会暂停进程运行另一方面在容器里可能要额外授权。更轻量一点的办法是使用pstack其实就是 gdb 的封装瞬时打印线程栈然后分析热点。如果现场允许也可以用gcore生成 core dump 再做离线分析这样对在线业务影响更小。2.3 采样工具perf连代码都不用重新编译线程栈方法适用于能拿到栈信息的程序但如果你手里是个编译过的二进制没有符号表或者代码不是 Java/C/C 这类常规语言jstack 和 gdb 就可能失效。这时候我强烈推荐perf。perf top -p PID可以实时显示进程内各个函数的 CPU 采样占比整个过程不需要重新编译连进程都不用重启。你会惊讶地发现原来很多让人头疼的问题用perf top一看就现原形了比如某个库函数或者某个系统调用独占了一大块 CPU。如果需要更完整的数据可以用perf record -p PID -g -- sleep 30采样30秒再perf report查看采样报告。-g是开启调用图记录能还原完整调用链。这个命令在处理无符号二进制时可能看到的是内存地址而不是函数名这时候不要慌把/proc/PID/maps里的加载地址和addr2line配合使用往往能确定到模块级甚至代码行级。2.4 如果现场已经没了用sar和atop还原历史很多情况下你接到反馈时 CPU 已经恢复正常了或者在别的同事机器上根本没有现场。这时候别直接放弃系统级的历史数据往往还在。sar -u可以查看历史的 CPU 使用率sar -q查看历史负载sar -w查看上下文切换。如果系统装了atop它的历史记录文件通常在/var/log/atop可以用atop -r 文件回放当时每个进程的 CPU、内存、磁盘实时状态。我见过不少同事在 CPU 飙高时只盯着当前画面错过黄金排障期而实际上历史记录里已经把肇事进程拍得清清楚楚了。在容器环境里宿主机上的sar不一定能看到容器内进程但可以看宿主机整体 CPU。这时候可以在容器外通过docker stats --no-stream或者podman stats看每个容器的资源占用再进入容器内部用前面说的 pidstat、top 进一步缩小范围。3. 三种高频真实案例复盘从问题到根因3.1 Python多进程死循环CPU时间片被吃满也不一定报警我遇到过一个很典型的 Python 服务每天某个固定时段 CPU 就飙到 80% 以上但毫无规律。一开始怀疑是定时任务但翻日志也没看到什么异常任务启动了。用top很快发现是 Python 主进程下面挂了多个子进程每个子进程都占了不少 CPU。继续用top -Hp看线程发现每个子进程里有几个线程的 CPU 很高。然后py-spy dump抓了 Python 堆栈发现热点全在一个读取配置文件后做的全量字段解析函数里。这个函数本身没有死循环但底层的multiprocessing池在某一时刻把大量任务一次性丢进来了任务队列堆积后所有 worker 都在疯抢 CPU。真正有意思的问题在于根因每次发布新配置后进程都会做一次全量刷新而配置里有个 list 字段在特定数据规模下会造成 O(n^2) 的拼接操作数据量一大所有 worker 都卡在这个拼接逻辑里。这个案例给我的教训是CPU 飙高很多时候不是某一个瞬间突然发生的而是某个逻辑在数据规模达到临界点后原地爆炸。排查时要放大视野别只看 CPU 高那一刻正在跑什么还要想为什么此时此刻开始高。后来我把py-spy列进了基础工具清单里因为它可以无侵入抓 Python 程序的栈比 gdb 友好太多。3.2 Java GC频繁导致CPU飙高别只盯着业务代码Java 服务 CPU 飙高的案例我碰到得最多其中很大一部分不是业务代码问题而是 GC 引起的。表现是 CPU 高、RT响应时间飙高、GC 日志里 Full GC 频繁。用jstat -gcutil PID 1000可以每隔一秒打印一次年轻代、老年代使用率和 GC 耗时。如果看到老年代一直处于高位Full GC 次数不断上涨那么 CPU 消耗主要花在垃圾回收上了。这时候下一步是拿堆转储文件做分析。我常用jmap -dump:live,formatb,fileheap.bin PID导出一份堆快照然后用 Eclipse MAT 或者 JDK 自带的jhsdb jmap来分析大对象和引用链。曾经在一个订单服务里发现 Full GC 后老年代使用率立刻上升分析堆发现是一张缓存表被一次性加载到了内存而这张表的数据量比预估的大了一个数量级直接导致每次请求都要扫描大对象。Java 场景里有个特别常见的误区CPU 高你会下意识觉得是线程在忙但 GC 线程占用的 CPU 同样会体现在 us 里而且它消耗的时间往往非常惊人。遇到 Java 服务 CPU 飙高第一步先看 GC 日志花不了五分钟就能排除或确认这个方向可以避免大量无效排查。3.3 内存交换引发CPU“偷懒”kswapd占用罪魁祸首有一种 CPU 飙高非常隐蔽表面上看进程 CPU 不算特别高系统整体负载却高得吓人响应也很慢。这类问题十有八九和内存压力有关。Linux 在内存不足时会启动kswapd0内核线程来回收内存页如果回收压力很大kswapd 会一直占用 CPU同时触发大量磁盘 I/O进一步加剧 wa 的升高。我一个线上的案例就是典型的这种场景。当时top里看到kswapd0占了 30%-40% 的 CPU但业务进程 CPU 并没有打满整体就莫名其妙很卡。查vmstat发现si和so不断跳动这意味着内存在持续换入换出。最终定位到是服务里一个本地缓存没有设置上限随着运行时间推移无限制增长导致物理内存被耗尽系统被迫频繁进行磁盘交换。这种问题的排查方向是看内存水位。free -h看 available 内存是否告急vmstat 1看 swap 使用情况也可以用cat /proc/meminfo查看CommitLimit和Committed_AS判断系统是否过度分配内存。解决思路往往是给缓存设置上限、优化 JVM 堆内缓存大小或者考虑引入外部缓存组件而不是一上来就加大物理内存因为内存越加缓存增长越猛问题只会换一种形式继续出现。3.4 一个“假”CPU飙高云上steal与超线程陷阱还有一种场景特别让人头疼就是 CPU 看起来高但业务进程线程栈里所有线程都处于等待状态压根没活可干。这种“假性飙高”在虚拟化环境里非常常见。用top或者mpstat能看到st字段高含义是虚拟机管理器占用了本该分配给当前虚拟机的 CPU 时间。最简单的解释是宿主机上还有其他虚拟机在抢 CPU你的影子机器看着只跑了 20%实际上物理 CPU 早就被邻居吃完了。看st超过 10%就该找云服务商或者宿主机管理员核对宿主负载了。超线程也会造成类似误解。两路超线程逻辑核共享物理核的执行资源如果你的程序是计算密集型开了超线程后单个逻辑核的 CPU 使用率看上去上升了但吞吐并没有翻倍反而可能出现“一核有难八核围观”的局部资源争抢。排查时看lscpu里有Thread(s) per core的字段如果为 2说明开了超线程。再配合top的 1 键查看逻辑核分布如果只有同一物理核的两个逻辑核使用率高其他核空闲就得考虑是否需要绑核或者调整调度策略。4. 别忘了硬件与系统配置CPU飙高不全是软件问题4.1 睿频、功耗限制与温度CPU跑不满反而“假高”有些时候 CPU 使用率高并不是程序有问题而是硬件行为造成的。比如服务器设了功耗墙CPU 温度一高就主动降频频率下降后同等工作量下 CPU 利用率百分比反而上升看起来就像“CPU 飙高”实际上业务吞吐在下跌。检查方式用sensors查看温度再用lscpu或者cpupower frequency-info查看当前频率与标称睿频频率。如果温度常年贴着上限跑频率又比基准低不少那基本就是散热或者风扇策略的问题。数据中心机房环境里如果机柜通风不畅前几台服务器特别容易出现这种问题球往往不在代码上。Windows 平台下同样需要关注功耗策略偏向了“节能”还是“高性能”。电源计划设为节能模式时CPU 会频繁降频来省电感知上就是同样的负载耗时更长CPU 利用率也波动更剧烈。可以临时切换到“高性能”计划对比一下如果现象明显改善说明频率调度策略是诱因之一。4.2 中断与软中断分布网卡多队列、irqbalance与绑核syCPU 高时除了系统调用频繁还要考虑中断分配不均的问题。网卡大量收包时如果中断都集中在单个 CPU 核上你会看到个别核使用率高得离谱整体负载却一般。这种情况最直接的解决方法是打开网卡多队列让不同队列的中断分散到多个核。Linux 下可以确认网卡支持的队列数看ethtool -l eth0再通过 irqbalance 服务自动平衡或者手动设置/proc/irq/中断号/smp_affinity做绑核。判断中断是否集中在某核可以跑cat /proc/interrupts看某个中断号的分布情况如果某个 CPU 的中断计数远高于其他核就有必要做一次均衡。软中断softirq同理。比如NET_RX软中断在多个网络命名空间之间频繁转发时对 CPU 的消耗并不低。借助perf top如果看到softirq相关的符号占用了大量 CPU再结合网络收发量大的特征基本可以确认方向。4.3 系统级参数vm.swappiness、watermark与CPU之间的隐性关系我一直觉得内存和 CPU 的问题不能分开看。内存水位过低时触发回收回收过程会消耗 CPU还会伴随磁盘 I/O反过来 CPU 高也会加剧内存分配失败导致更多重试和回收。所以排查 CPU 问题时常要检查几个系统参数。/proc/sys/vm/swappiness控制内核倾向于回收匿名页还是缓存页默认 60 在部分服务器上偏激进容易过早换出进程内存造成后续访问抖动。我自己在数据库类服务上通常会调低到 10 左右但不能一概而论调太低可能导致文件缓存回收不够及时需要根据实际 workload 调整。还有vm.min_free_kbytes和 watermark 的设置。低水位时内核会触发直接内存回收direct reclaim在/proc/vmstat里对应pgscan_direct、pgsteal_direct这些指标。如果你看vmstat发现r队列和cs都高同时/proc/vmstat里pgscan_direct在涨那 CPU 消耗很可能有一部分花在了内存回收上。这类问题不是简单加 CPU 能解决的优先看内存分配和缓存策略是否合理。5. 排查方法沉淀成清单提升下一次的效率5.1 一份可以直接抄的故障排查命令清单排查 CPU 飙高问题单靠命令记忆并不牢靠。我在工位上贴过一张 A4 纸命令清单后来也分享给了团队。这里整理一份精简版按“观察整体-定位进程-深入线程/函数-分析根因”四个阶段排列覆盖 Linux 平台常见场景阶段常用命令关注点看整体top、vmstat 1 5us/sy/wa/st、load、进程排序看CPU分布mpstat -P ALL 1单核耗尽还是整体高看进程pidstat -p PID 1、top -Hp PID具体进程和线程 CPU 时间看栈jstack、pstack、py-spy dump线程在哪个方法里采样分析perf top、perf record/report函数级热点与调用链看系统调用strace -cp PID系统调用次数与耗时看历史sar -u、atop -r持续监控和回放看内存关联free -h、cat /proc/vmstat回收、swap、直接内存回收这张表看起来简单但每次真到现场的时候都很管用。建议你在空闲环境里把每条命令都跑一遍至少知道每个命令的输出长什么样真到故障时心里才不慌。5.2 配置告警与预留现场比事后抢救更重要排查是应急手段真正的功夫在平时。CPU 飙高问题之所以在事后很难定位有两个原因一是现场丢失二是没有对照基准。我现在的标准做法是在所有服务器上配置基础的性能监控采集CPU 使用率、load、每个进程的 CPU 占用、关键线程的堆栈定期抓取。不一定追求很重的 APM 体系先用collectd、telegraf这类轻量采集器把数据打到 Prometheus配合 Grafana 看趋势再加几条告警规则。比如 CPU 使用率持续 5 分钟超过 85% 告警、单核 CPU 长时间 100% 告警、上下文切换次数突变告警。告警阈值需要结合历史基线来定不能拍脑袋写个 90%有些服务正常就是 70%有些服务到 30% 就已经出问题了。另外我建议对核心 Java 服务开启 GC 日志、对 Python 服务开启运维侧性能采样工具这些日志平时不起眼一旦 CPU 飙高就是最可靠的现场证据。5.3 最后分享两个容易被忽略的细节先说第一个CPU 飙高时不要只盯 CPU。很多 CPU 问题本质上是内存、磁盘、网络的次生灾害。查 CPU 的同时一定要把free -h、df -h、iostat -x的结果一起记录四位一体综合分析才能避免在错误方向上钻很久。再说第二个每次排查完我会把结论写成一段“一句话根因 验证方式”存进团队的故障知识库。比如“某服务 CPU 高根因是配置 list 重复拼接导致的 O(n^2) 热点验证方法是 py-spy 抓栈看到 xx 函数持续在栈顶”。这比写长篇报告有用得多下次遇到类似的 CPU 问题时直接查关键词就能节省大量时间。基于我个人的实际操作体会CPU 飙高排查并不神秘本质上就是一个“先分方向、再缩范围、最后验证根因”的过程。把最基础的几个工具练熟把历史数据留好把每次故障沉淀成清单你的排障速度会明显提升。这一章的内容差不多到这里下一次如果有人问起“CPU 飙高怎么排查”你就可以直接把这套方法甩给他了。