简介:面向Linux运维工程师的高CPU占用问题排查实战文档,聚焦系统负载飙高时的定位与处理思路。资源系统梳理两种常用排查方法:一种通过top排序后结合top -H、线程ID转十六进制、jstack查看线程状态;另一种通过ps -mp获取线程耗时并排序,再结合jstack打印堆栈,二者均能快速锁定异常线程与相关代码。文档还融入真实生产案例,演示Java进程CPU占用率达到300%时的完整排查过程,从初始top发现PID到最终定位线程堆栈,避免盲目重启服务;同时介绍Zabbix、Nagios、阿里云监控及“王教授”运维工具的告警机制,强调监控前置与主动发现的价值。资源为单个PDF文件,大小147KB,内容精炼、操作命令明确,适合运维人员和后端开发者随时查阅,文中方法无需图形界面,可直接在纯命令行环境实践。已有4448人学习下载,对日常系统维护和故障应急具有实际参考意义。
1. CPU占用率高的排查:先定位再动手,别把玄学当性能分析
Linux系统中CPU占用率偏高,可能是运维群里出现频率最高的告警。很多人的第一反应是登录服务器敲top,看到哪个进程%CPU高就kill -9哪个,这套流程在压测环境能交差,到了生产环境经常会翻车:同一个进程的 CPU 统计跟监控平台对不上,或者机器 load 已经快拉满,top里 CPU 占用却不到 60%。本文把一套在服务器上反复用的排查思路拆开讲:怎么从进程定位到线程、从用户态挖到内核态、再从现场判断该调业务还是调资源,适合负责后台服务和容器平台的开发运维,也适合准备 linux 面试题的同学在纸上把整套流程推演一遍。
2. 用 top、ps、mpstat 把高占用进程和线程揪出来
2.1 top 里的三个数:%CPU、load average、us/sy 的正确读法
top是大家最熟的 linux 常用命令,但多数人只看了两个数:第一行的 load average 和进程列表里的%CPU。这两个数恰恰最容易误读。
load average 后面的三个值分别代表过去 1 分钟、5 分钟、15 分钟的平均活跃进程数。活跃进程包含正在运行的 R 状态进程和等待 IO 的 D 状态进程,所以 load 高不等于 CPU 高。一台 4 核机器如果 load 是 4.0,大致可以认为跑满;但如果进程都在等磁盘,load 到 8 也可能%CPU只有 20%。判断是否 CPU 问题,要以 CPU 状态行和%CPU为准,load 只用来感受整体压力趋势。
CPU 状态行里重点看 us(user)、sy(system)、wa(iowait)、si(softirq)、st(steal)。us 高是业务代码消耗,sy 高是系统调用或内核态消耗,wa 高要转向磁盘排查,si 高要怀疑网络包处理,st 高则要考虑虚拟化宿主机抢占。进入top后先按数字键1,把 CPU 展开成每个物理核的视图,如果只有个别核打满,说明问题可能出在单线程或中断绑核,后续排查方向完全不一样。
进程列表默认按 CPU 排序,但按P可以强制排序,按H可以切换成线程视图,按c显示完整命令行。这里花一分钟记住top -H -p PID这个组合:它会直接进入某个进程的线程视图,是定位单线程热点最快的入口。
2.2 ps 的 %CPU 只是平均值,线程级视角要交给 pidstat
很多新手只靠ps aux看 CPU,这是第一个坑:ps 报告的是进程从启动到现在的平均 CPU 占用,不是当前实时值。一个刚崩溃重启的进程,哪怕现在把 CPU 打满,ps里也可能只显示个位数百分比。所以ps适合拿来确认进程存在、PID、父子关系,不适合判断当前热点。
我一般会分两步。第一步用ps做粗筛,把所有进程按 CPU 占用排序,看一眼到底哪个进程可疑:
ps -eo pid,ppid,%cpu,%mem,user,comm --sort=-%cpu | head -n 20-e表示所有进程,-o指定输出列,--sort=-%cpu按 CPU 占用从高到低排序。这条命令 1 秒出结果,适合在 CPU 飙高时先用它定一个嫌疑进程,再用下面这条看该进程内部的线程:
pidstat -u -t -p 12345 1 5pidstat来自 sysstat 包,-u表示监控 CPU,-t显示线程级别详情,-p 12345指定进程 PID,最后的1 5表示每秒采样一次、共采样 5 次。输出里每行对应一个线程,%usr是用户态占用,%system是内核态占用,%CPU是线程整体占用,TID是线程号。这样定位出来的线程号可以直接对照jstack、perf的调用栈结果,把问题从“进程层面”压到“线程层面”。
这里有个很重要的换算概念:单个线程的%CPU上限就是 100%,相当于占满一个逻辑核;一个进程显示 300%,说明它内部有 3 个线程在并行跑满。后续不管用perf还是看 Java 线程栈,都要以 TID 为单位去对,而不是用 PID。
2.3 mpstat 看核间分布:单核打满和全核打满是两种病
判断完线程,下一步要知道 CPU 压力是不是均匀分布在所有核上。这决定了你要查的是单线程代码问题、绑核问题,还是多线程并发资源耗尽。命令是mpstat:
mpstat -P ALL 1 3-P ALL展示所有 CPU 核,1 3表示每秒输出一次、输出三次。返回的表格里每一行是一个 CPU 核的实时占用,%usr、%sys、%irq、%soft、%steal分别对应不同来源。
看到的结果通常归成两类。一类是某个核长期超过 90%,其他核却很悠闲,这种大概率是单线程的逻辑写在关键路径上,或者内核把中断都塞给了一个核;另一类是每个核都高,这种要么是多线程死循环,要么是锁竞争导致所有线程都在自旋等待,要么是宿主机 CPU 超卖。
还有一种容易被忽略的情况:业务进程每个核都只有 30% 左右,但 CPU 总占用加起来很高。这种分布常见于日志写入、压缩、序列化这类影子开销多的服务。不要只盯着最高的核看,要按“总 CPU 时间消耗”去算账。
| 观察现象 | 可能原因 | 下一步动作 |
|---|---|---|
| 单核打满,其他核空闲 | 单线程密集计算、中断绑核 | 查代码热点、查 /proc/interrupts |
| 所有核同步打满 | 多线程死循环、锁竞争 | perf 采样看内核态函数 |
| 各核 20%~40% 但总量高 | 日志、压缩、序列化等影子开销 | 逐一核对辅助进程与内核线程 |
| %steal 明显偏高 | 虚拟机 CPU 被宿主机抢占 | 检查云主机规格与邻居负载 |
3. 从进程态挖到内核态:perf、vmstat 与中断风暴的排查路径
3.1 perf top 看热点函数:数据比直觉靠谱
进程和线程定位完了,只知道“谁”在烧 CPU,还不知道“为什么”。这时候再猜就属于玄学,直接把采样工具挂上去看内核和用户态函数分布。最简单的是perf top:
perf top -p 12345它会按 CPU 采样频率对当前进程的热点函数实时排序,默认带调用栈展开功能。看到用户态函数名,基本能判断是不是死循环;看到内核态函数名,也能猜个大概方向。比如native_queued_spin_lock_slowpath出现频率很高,多半是自旋锁竞争;do_softirq相关函数高,要转向网络包处理;finish_task_switch高,说明线程切换本身消耗了大量 CPU,进程内线程数可能太多。
如果希望保存下来慢慢分析,可以用采样方式:
perf record -g -p 12345 -- sleep 10 perf report-g记录调用栈,-p指定进程,-- sleep 10表示采 10 秒就自动结束。perf report进入交互界面后会生成一张热点调用树,按回车逐级展开,能看到完整调用链。这条链路比任何日志都能说明问题。
需要注意两点。第一,内核符号可能需要 root 权限才能读全,普通用户经常看到一堆[unknown],生产环境可以临时用 root 执行受限命令后退出,不要长期挂 root daemon。第二,Java、Go 这类 JIT 或运行时语言,perf 直接看到的用户态符号往往是运行时自身,配合jstack或py-spy把 TID 翻译成业务线程名,才算是闭环。
3.2 vmstat 的 r 列和 cs 列:上下文切换为什么会飙升
perf能看函数级,但很多机房环境不方便装额外工具,此时vmstat是个不需要安装的备选。执行:
vmstat 1 5关注五列:r、cs、us、sy、wa。r 是当前处于可运行状态的进程数,如果这个值持续大于 CPU 核数,说明有一批进程在排队,CPU 确实不够用;cs 是每秒上下文切换次数,这个值正常情况下每核几百到一千多,超过两三万就要警惕,进程线程可能像抽风一样在抢时间片;us 和 sy 分别是用户态与内核态的 CPU 时间占比;wa 是 IO 等待占比。
我最常碰到的场景是:CPU 占用看着不高,us 只有 20%,但 sy 占了 40%,cs 数值异常高。这种往往是短生命周期线程大量创建销毁,或者定时器、信号触发太频繁,导致 CPU 大量浪费在“切换”而不是“干活”上。排查时用pidstat -w -t -p PID 1 5看每个线程的 cswch/s(自愿切换) 和 nvcswch/s(非自愿切换),自愿切换过多说明线程在等待锁或 IO,非自愿切换过多说明时间片不够、线程数超过核数太多。
3.3 把 D 状态进程和 iowait 从 CPU 占用里剥出去
很多人把 load 高和 CPU 高混为一谈,真实世界里有大量 load 很高但 CPU 闲置的场景。罪魁祸首通常是 D 状态进程,也就是不可中断睡眠状态,典型情况是进程在等磁盘 IO 或 NFS 响应。D 状态进程会计入 load average,但不消耗 CPU 时间片,所以top里 CPU 占用看着不高,机器却卡得不行。
排查命令很简单:
ps -eo pid,stat,wchan:30,comm | awk '$2 ~ /^D/ {print}'wchan:30显示进程当前等待的内核函数名,能看到它究竟卡在哪个内核函数上。比如卡在wait_on_page_bit说明在等内存页回写,卡在nfs相关函数说明远端存储有问题。看到一堆 D 状态进程时,不要再调 CPU 配置,先去查磁盘iostat -x 1,看%util和svctm,或者确认 NFS 挂载点是否可达。
3.4 中断风暴:ksoftirqd 和网卡多队列
还有一种 CPU 高是中断带来的,表面上找不到用户态进程,但top里 si(软中断) 或者 hi(硬中断) 不低,同时 ksoftirqd 内核线程占用会飙上去。这个场景在流量型服务上很常见:网卡每收一个包就触发一次中断,如果小包特别多,中断处理本身吃掉的 CPU 可能比业务代码还多。
先看中断是否集中在某个核上:
cat /proc/interrupts左侧是中断号,各列对应每个 CPU 核收到的中断次数。如果网卡相关中断号只在 CPU0 上涨,说明当前驱动或配置把所有网络中断都绑到了第一个核,这就是单核被打满但其他核空闲的原因之一。
常见做法是先确认网卡是否支持多队列,用网卡工具查看队列数量,然后调整队列数与 CPU 核数匹配,让多个核分摊收包中断。如果是虚拟化环境且驱动不支持多队列,可以尝试开启 RPS/RFS 这类软件分发机制,让软中断均匀分散到各核,避免单核成为瓶颈。这类问题的后续验证也很直接:mpstat -P ALL里各核%soft是否趋于均匀,且用户态业务 CPU 是否顺势降下来。
4. 对高 CPU 问题的常见处置手段:从锁竞争到进程配额
4.1 死循环和忙等:从热点函数到止损措施
代码里出现死循环或忙等,是 CPU 高最直接的原因。perf top显示某个用户态函数长期压在顶部,基本就能下结论。处理顺序应该是先止损、再定位、后修复。
止损手段要看服务形态。能快速回滚的版本直接回滚;不能回滚的,临时把并发调低比直接 kill 进程更安全,因为 kill 会中断所有请求,影响面更大。常见做法是先用kill -STOP PID暂停进程,确认现场影响范围后再决定是否kill -TERM。kill -9是最后的选择,因为它不给进程清理连接和释放内存的机会,可能留下半关闭的 socket 或脏数据。
止血后再按语言选工具:C/C++ 用perf看调用栈;Java 服务用jstack -l PID抓线程栈,多抓几次对比是不是同一个方法卡住;Python 服务可用py-spy dump --pid PID拿到当前正在执行的 Python 函数名。定位到具体函数后再改代码,不要不停重启,重启只是把黑匣子又盖回去了。
4.2 锁竞争:线程数不是越大越好
另一个高发原因是锁竞争。现象很典型:CPU 占用高,业务吞吐却上不去,perf top中自旋锁相关函数排在前列,火焰图里锁等待区域又宽又矮。很多人第一反应是加线程,实际上在高 CPU 密集场景下,线程数远超核数就会产生大量上下文切换和锁自旋,CPU 全部耗在等锁上,活没干多少。
这类问题要先看锁的粒度。把大锁拆成细锁、用读写锁代替互斥锁、减少持锁期间的操作,都是常用手段。线程池大小也不是简单 2 倍核数就合理,CPU 密集场景通常设为核数或核数加一,IO 密集场景再放宽。这个问题也是 linux 面试题里反复出现的:给你一个四核机器,线程池开 100 个线程跑纯计算,吞吐能提升吗?答案是不能,反而可能因为锁竞争和切换开销让性能下降。
如果代码一时改不动,可以先用taskset把进程绑定到固定核上,限制它与其他进程争抢,但这只适用于单实例或对延迟不敏感的服务,多实例场景慎用。
4.3 日志滚轮和压缩:让日志进程成了 CPU 大户
很多 CPU 高的问题不在业务进程,而在日志链路上。新装的 Linux 系统更容易踩这个坑:systemd-journald 默认把每个服务的输出都收进 journal,如果某个程序疯狂打日志,journald 的 CPU 会先飙起来。接着是旧日志压缩归档,日志越大,压缩进程消耗越高。
排查时别只盯业务进程,把systemd-journald、rsyslogd、logrotate的 CPU 都看一遍:
pidstat -u -p $(pidof systemd-journald) 1 5如果 journald 占用显著,先看是不是有服务一条条刷屏,把日志级别调上去或者把无关输出重定向到 /dev/null。journald 侧可以调整/etc/systemd/journald.conf里的SystemMaxUse限制总日志容量,RateLimitIntervalSec和RateLimitBurst控制单位时间最多接收多少条日志,修改后重启 journald 生效。rsyslog 同步写盘压力大时,可以改成异步队列,或者按日志量调整滚动策略,避免一个日志文件膨胀到几百兆后再一次性压缩。这个环节经常被忽略,属于排查顺序里性价比很高的一步。
4.4 用 systemd 配额给失控进程设上限
当进程暂时杀不得、代码又改不动,可以先把它的 CPU 占用限制住,保住同一台机器上其他服务。systemd 管理服务可以直接用运行时命令:
systemctl set-property your-service.service CPUQuota=150%CPUQuota=150%表示最多使用 1.5 个核的 CPU 资源,底层对应 cgroup 的 cpu.max 配额,150% 实际是 150000/100000 的比例关系。设置后立即生效,重启也会保留。想临时看一下效果再决定去留,可以执行同样的命令把配额改回空值去除限制。
对不在 systemd 管理下的进程,可以用cpulimit -p PID -l 50这类用户态工具,它通过暂停和恢复进程来控制 CPU,适合应急,但不适合精度要求高的场景。要记住:配额只是止损,不是修复,长时间用配额压着有问题的进程,会让延迟劣化且难以察觉,还是要留出时间窗处理根因。
4.5 虚拟机 CPU steal:宿主机抢走了本该属于你的 CPU
云服务器和虚拟化环境里,还有一种“假高占用”的根因不在你机器内部,而在宿主机超卖。mpstat输出里的%steal字段或者top状态行里的st,代表虚拟机申请 CPU 时间但被宿主机调度延后的比例。如果%steal持续超过 10%,应用的 CPU 占用和延迟都会肉眼可见地恶化,但你在虚机里查任何进程都查不出问题。
这种场景的处理思路很简单:不要调业务参数,先调整部署位置。常见做法是迁移到低负载宿主机,或者在购买资源时选择限制超卖比例的实例规格。如果服务跨多台机器,优先把流量切走;如果无法迁移,可以尝试把业务线程的调度优先级调低,减少被宿主机抢占时的连锁抖动,但这个措施效果有限。判断 CPU 高之前先看%steal,能省掉后面一整轮业务层排查。
5. CPU占用率排查避坑:5 个容易翻车的误判场景
5.1 top 显示占用正常,但机器整体卡成 PPT
现象:top里各核%CPU只有 20% 左右,但敲命令明显卡顿,SSH 窗口响应延迟好几十秒。
原因:CPU 状态行里的 us、sy 都不高时,要回看 load average 和 wa。常见根因是磁盘 IO 饱和,进程大量进入 D 状态排进 run queue,load 被顶高,而 CPU 真正花在“等 IO”上而不是“算”上,CPU 百分比自然不高。
解决:用iostat -x 1看磁盘%util,用ps -eo pid,stat | grep D数 D 状态进程,确认后再决定扩容磁盘、迁移数据还是排查慢存储。不要把突破点放在 CPU 上,那条路是死的。
5.2 一个进程显示 300% 或 800%,以为是 bug 或中毒
现象:进程列表里某个进程%CPU显示超过 100%,甚至 900%,第一反应是数值异常或进程被种了矿机。
原因:top和pidstat的%CPU都是按核归一化的,100% 代表一个逻辑核打满。多线程进程的多个线程并行运行,占用多个核,总和超过 100% 是完全正常的。
解决:用top -H -p PID切到线程视图,确认是多个线程各占一部分,还是某个线程单独占满。按 H 切换后,如果所有线程加起来和进程总占用对得上,说明是并行计算而非异常;如果单个线程超过 100%,反而要先怀疑进程绑核或者多个线程绑定同一个核,再去查业务是否正常。
5.3 strace 一挂上去,CPU 占用反而降了
现象:现场怎么看怎么可疑,strace 跟踪几秒后,CPU 占用以肉眼可见速度下降,让人以为是误报。
原因:strace 会拦截每次系统调用,让原本高频的系统调用变慢,整个程序的执行节奏被拉低,CPU 占用自然降下来。这不代表问题自动消失,是干扰工具改变了测量对象。
解决:遇到这种情况就不要再用strace -p PID做长时跟踪,改用短窗口采集,比如timeout 5 strace -f -c -p PID,只看汇总统计;或者换用perf采样,它对程序时序的影响小得多。记住,排查工具的副作用本身也是排查的一部分。
5.4 监控平台显示 CPU 90%,登录服务器看却不到 30%
现象:监控告警 CPU 持续 90%,但 SSH 上去执行top和mpstat,占用率只有 30% 左右,两边数据对不上。
原因:监控平台通常按 1 分钟或 5 分钟周期聚合,会把高峰期和低峰期平均;top看的是当前瞬间值,两者采样窗口不同,本来就有差异。另一个常见原因是监控统计的是 cgroup 视图而top看的是整机视图,容器场景尤其明显。
解决:先把自己采集的周期拉长,用pidstat -u 1 60连续记录一分钟,看平均 CPU 和监控平台是否接近;再确认容器里用top时是否已经进入了对应容器的 PID namespace。数据对不齐时,以长时间连续采样为准,不要拿一个瞬间值反驳监控平台。
5.5 内存回收被误判为 CPU 问题
现象:us 和 sy 都不高,但 kswapd 内核线程 CPU 起高,系统整体响应也变慢。
原因:内存接近上限时,内核要反复扫描内存页做回收,这个过程消耗 CPU 时间,还会触发直接回收进而阻塞进程。CPU 高只是表面现象,根因在内存。
解决:顺手看free -h和vmstat的 si、so 两列,si/so 持续不为零说明已经发生换页。用dmesg -T | tail看有没有 oom 相关记录,有的话优先处理内存泄漏和调低缓存上限,而不是给 CPU 加配。内存问题伪装成 CPU 问题的比例不低,CPU 排查的前三个步骤里应该带一眼内存。
6. 把排查流程固化成一个采集脚本,再对照验证结果
第一次排查 CPU 高时,凭记忆敲命令容易漏项,等现场过去了再补采集就晚了。我现在养成的习惯是提前在机器上备一个采集脚本,任何一个环节告警都先让它抓现场。以下这个脚本会在当前目录生成带时间戳的快照文件,涵盖 load、CPU 状态、进程排行、线程排行四个维度:
#!/bin/bash stamp=$(date '+%Y%m%d_%H%M%S') outdir="/tmp/cpu_debug" mkdir -p "$outdir" { echo "===== snapshot $stamp =====" uptime echo "----- CPU state -----" top -bn1 | head -n 8 echo "----- process top -----" ps -eo pid,ppid,%cpu,%mem,user,comm --sort=-%cpu | head -n 15 echo "----- thread top -----" pidstat -u -t 1 1 | sort -k 9 -r | head -n 15 } > "$outdir/snapshot_${stamp}.txt" echo "saved to $outdir/snapshot_${stamp}.txt"top -bn1是非交互模式,只取一次快照;pidstat -u -t 1 1采样一秒钟内的线程占用,用sort -k 9 -r按第 9 列%CPU排序。脚本要在疑似 CPU 高的第一时间执行,越早越能抓到当时的真实负载,而不是事后补测的近似值。
如果问题持续而反复,还可以用一段时间的趋势记录替代快照:
pidstat -u -t -p 12345 5 120 > /tmp/cpu_debug/pidstat_$stamp.log &每 5 秒采一次、持续 10 分钟,后台运行,对比不同时段的线程热点变化,能看出是持续打满还是周期性脉冲。
验证解决效果时,不要只比较单个进程的 CPU 百分比,要对照三组指标:负载状态下降、上下文切换 cs 回落、应用侧延迟或吞吐恢复。CPU 占用降下来但请求延迟更差了,说明只是把热点压住、根因没动。我现在每次调整完配置都会先抓十分钟的pidstat记录,和修复前的数据并排看,确认业务指标也一起好转才算结束。希望帮到你。
本文还有配套的精品资源,点击获取