节点的CPU又报警了。做运维和基础架构的同学,对这句话应该再熟悉不过。过去几年里,我处理过的“节点CPU占用”问题少说也有几十次,但每一次排查,我都不会直接去看监控图或者杀进程,因为“CPU占用高”这句话本身太笼统了——同一个数字背后,可能是业务流量突然上涨,可能是代码死循环,也可能是网卡中断把某个核打满,甚至可能是内核自己的模块在空转。这篇内容我想从一次典型的排查流程说起,把CPU占用这类问题的判断方法、排查顺序、底层逻辑和恢复手段一次讲透,适合刚接触服务器运维的开发者,也适合那些经常被CPU告警拉去救火、却总感觉无处下手的同学。
1. 从“节点CPU高”到“到底哪个指标在撒谎”:先建立判断基线
很多人一看到CPU占用高,第一件事就是跑 top,看到某个进程CPU冲到100%,就觉得抓到凶手了。但这里有个非常隐蔽的问题:top 里显示的CPU百分比,默认是一个进程在所有CPU核心上的累计占用,而不是单核占用。如果你用的是4核节点,某个进程显示300%,意味着它吃满了将近3个核,这在多线程应用里很常见,但它到底是正常的高负载还是异常跑飞,光靠这一行数字根本说不清楚。
我自己的习惯是,上任何一台节点,先不急着看进程,而是跑三条命令建立基线。
第一条是 uptime。它能看到最近1分钟、5分钟、15分钟的负载平均值。注意这里的“负载”不是CPU利用率,而是处在可运行状态和不可中断睡眠状态的进程总数。如果你的节点有8个核,load average在8左右,说明整个系统的计算需求刚好饱和;如果load到了16甚至20,说明有大量任务在排队等待CPU,这时候你再看CPU利用率是90%还是99%,意义就不大了——队列已经堵死了。反过来,如果你看到load很低、但某个进程CPU很高,那这个高占用大概率是单线程死循环或者某个中断处理在猛烧,这种情况往往才是最需要警惕的。
第二条是 mpstat -P ALL 1 5。这条命令的价值在于把每个核心的利用率分开看。很多CPU隐患是“结构性”的,比如8个核里只有1号核跑到100%,其余7个核都在20%以下。如果你只看整体利用率,它会告诉你“CPU不忙啊,才30%”,但那个被打满的核已经让整个系统的响应变慢。出现这种单核打满的情况,十有八九是中断分布不均、RPS没有开启,或者某个单线程应用被调度到了固定核上。
第三条是 top 进去之后按 1,看每个核的使用率分布,然后按 x 高亮排序列,再按 b 高亮当前排序字段对应的行。这能快速定位到底是哪个进程在吃CPU,也能看到 us(用户态)、sy(内核态)、wa(IO等待)、hi(硬中断)、si(软中断)这几个关键字段的比例。比如 si 经常冲到30%以上,说明网络包处理占了大量CPU资源,这时候你去抓业务进程,其实是白费力气,因为真正的消耗在中断处理环节。
把这几个指标看完,你才能大致判断问题属于哪一类:是整体计算量上来了,还是单核被打爆,又或者是中断和内核态消耗异常。这一步没做扎实,后面所有的定位都会跑偏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 逐层定位:进程、线程、中断与内核线程的“追凶”顺序
基线确定之后,我会按照一个固定的顺序逐层往下追:进程、线程、内核线程、中断,最后才是硬件和驱动。之所以固定这样的顺序,是因为越往上层定位越快,成本也越低,而且大部分问题到线程这一层就能水落石出。
先说进程层。top 看到的是进程,但如果某个进程是多线程的,你还得知道CPU到底耗在哪个线程上。推荐用 pidstat -t -p <PID> 1 5,它会按线程维度输出CPU占用。举个例子,之前某公司的消息网关节点出现CPU占用飙高,top 里看到进程占用150%,但用 pidstat -t 一拆,发现是一个名为 pool-4-thread-7 的线程独自吃了120%。顺着这个线程去查代码,才发现是某个连接池的探活逻辑在循环里面重复创建连接,导致线程一直在做无用功。如果你只看进程级别,根本看不到这个问题的根源。
如果进程和线程都没有明显异常,下一步就把目光移向内核线程。用 top 按 P 排序,注意观察名字像 kworker、ksoftirqd、rcuos、migration 这样的内核任务。kworker 占用高,往往对应着某种内核工作队列,可能是磁盘IO、可能是驱动轮询,也可能是文件系统事务;ksoftirqd 占用高,则直接指向软中断处理不过来;rcuos 这类RCU相关线程飙高,一般是内核里的读-复制更新机制遭遇了某种阻塞。这几个内核线程可以说是我排查CPU问题时的高频“嫌疑人”。
再往下就是中断了。中断分为硬中断和软中断,这也是很多人容易忽视的一块。先看 /proc/interrupts,这个文件会统计每个CPU核心分别处理了多少个硬中断。如果你发现某个核心的中断数明显高于其他核心,说明当前节点的中断亲和性可能没有配置好,或者设备的中断被自动集中到了一个核心上。软中断的分布则要看 /proc/softirqs,对比各个核心的计数,同样能发现是不是存在“单核背锅”的情况。
走到这一步,有一个非常值得记住的原则:不要在一开始就使用 strace 或 perf 这类重工具。 不是它们不好,而是在没有确定是用户态还是内核态消耗之前,直接用这些工具会引入巨大的额外开销,还可能把现场搞乱。strace 在目标进程上附加时,会让进程的运行速度慢几倍甚至几十倍,这在生产节点上是不能接受的。等前面的基本盘摸清了,确认是某个特定进程在疯狂做系统调用,再用 perf top -p <PID> 去采样用户态调用栈,那时候才是一针见血的操作。
下面这张表是我电脑里长期存着的一张“追凶路线图”,每次CPU告警我都会先拿它对照一遍:
| 观察对象 | 关键命令 | 指向的问题类型 |
|---|---|---|
| 整体负载 | uptime |
计算需求是否饱和,队列是否积压 |
| 每核占用 | mpstat -P ALL 1 5 |
全局压力还是单核结构性问题 |
| 进程占用 | top -p <PID> |
哪个业务进程在吃CPU |
| 线程拆分 | pidstat -t -p <PID> 1 5 |
线程池、死循环等代码层问题 |
| 内核线程 | top 按P排序 |
kworker/ksoftirqd等内核任务异常 |
| 硬中断分布 | /proc/interrupts |
中断亲和性失衡 |
| 软中断分布 | /proc/softirqs |
网络收包、定时器等软中断处理异常 |
把这张表走完,绝大部分CPU占用问题的“大头”都能定住。
3. 一个让kworker持续升高的隐形杀手:中断风暴的完整排查实录
这一节我想完整还原一次处理经过。当时某公司的内部监控系统报出几个应用节点的CPU占用持续在85%以上,负载却不怎么高,业务响应也没受到明显影响。乍一看像是CPU利用率虚高,但既然告警了就得查清楚。
我先跑了 uptime,发现负载只有2.0左右,而节点是16核。这个组合很有意思:16核的机器负载才2,说明没有排队任务,但CPU占用却有85%——说明CPU大量消耗在了某种“不算进负载”的工作上。什么工作会这样?首当其冲就是中断处理。
紧接着用 mpstat -P ALL 1 3 观察。结果发现CPU0占用接近100%,CPU1到CPU15都在10%以下。单核打满、整体不忙,这是典型的中断集中到某一个核心的症状。我把 top 打开按 P 排序,看到排在最前面的内核线程是 ksoftirqd/0,也就是说CPU0上的软中断处理已经成了瓶颈。到这里,方向基本明确:网卡收包产生了大量软中断,并且全部压在了CPU0上。
为了确认是哪个设备的中断在作怪,我去查了 /proc/interrupts。不看不知道,网卡的队列0对应的中断计数已经到了几千万,而队列1到7只有几十万。这说明网卡多队列机制虽然存在,但实际流量并没有均匀散到各个队列,而是几乎全部打进了队列0。再加上系统没有开启RPS(Receive Packet Steering),收包中断只能由CPU0单独处理,于是CPU0就成了火力集中点。
查到这里,可能很多人会选择直接改中断亲和性,把网卡各个队列的中断绑到不同CPU上。但手动绑定有几个坑:第一,网卡的队列中断号在不同驱动下是会变化的,配置文件里写死的号可能在重启后失效;第二,中断绑定的CPU如果恰好也在处理其他高优先级任务,反而会造成新的不均衡。我当时用的方案是双管齐下。
第一步,修改 irqbalance 服务的工作方式。这个服务本身就是为了让中断在CPU之间均衡分布而存在的,但它在某些场景下会倾向于把多个设备中断集中到少量核心上,目的是为了利用CPU缓存的局部性。我修改了 /etc/sysconfig/irqbalance 里的 IRQBALANCE_BANNED_CPUS 参数,把一些承担了关键业务线程的CPU排除在中断分配范围之外,网卡中断自然会被分配到更空闲的CPU上。
第二步,给网卡队列开启RPS和RFS。RPS可以把收到的数据包从硬件中断所在的CPU分发到其他CPU的软中断队列里处理,等于在软件层面做了一次二次均衡。具体操作是设置 /sys/class/net/eth0/queues/rx-0/rps_cpus 里的CPU掩码。举个例子,把rps_cpus设置为 f(二进制1111),表示允许CPU0到3共同处理这个队列的软中断;同时设置 /proc/sys/net/core/rps_sock_flow_entries 和每个队列的 rps_flow_cnt 来开启RFS,让同一个数据流的包尽量固定到同一个CPU处理,避免因为乱序导致CPU缓存失效。
这两步操作完成后,CPU0的占用率从100%降到了30%左右,其他核心的占用率也起来了,整体利用率反而比之前更健康。这个小节我想强调的是:中断风暴这种问题,只靠“升级CPU”或者“加机器”是永远解决不了的,因为问题压根不在算力不够,而在算力分布不均。 排查这类问题最关键的点在于,当你看到CPU占用高但负载不匹配时,一定要第一时间想到中断处理这条路,而不是急着去扩容。
4. 反推CPU调度的底层机制:为什么有些优化看着合理却没用
排查到这一步,CPU占用的“表层问题”通常已经解决了。但如果你只停留在“改参数、看监控”的层面,下次换个节点、换个业务,同样的问题还会以另一种形式找上门来。所以我一直觉得,搞CPU问题的人,多少要了解一点CPU调度的底层逻辑,不然你很难解释为什么有些优化手段看起来合理,实际用了却没效果。
先讲一个很多人忽略的概念:CPU时间片和上下文切换。Linux的CFS调度器(完全公平调度器)不是按“谁先来谁先跑”的方式分配CPU,而是按照每个任务的虚拟运行时间去安排。每个线程在获得CPU后,都有一个时间片,时间片用完之后,调度器就会把它换下去,让其他线程跑。如果节点上线程数量特别多,每个线程分到的时间片就非常短,CPU就会把大量时间花在“换人”上,而不是“干活”上。此时你从 top 里看到的CPU占用可能高达80%,但真正用来执行业务代码的占比很低,大部分都耗在了上下文切换里。
用 vmstat 1 就能看到这个现象:cs(context switch)列如果每秒超过几十万次,而CPU的 us 占比又没那么高,那基本可以断定是线程数量失控了。很多同学碰到这种情况的第一反应是“加CPU核数”,但你把核数从8加到16,上下文切换的整体次数不会减少太多,因为你没解决“线程太多”这个根因。正确的思路反而是减少线程数,或者让线程在执行时尽量少地主动让出CPU,比如把锁的粒度调大、把带锁操作换成无锁数据结构、避免在热路径上打日志。
再说一个跟“优化看着对但没用”密切相关的机制:NUMA。现在的多路服务器基本都是NUMA架构,内存访问分本地和远端,访问远端内存的延迟比本地高几十纳秒。如果某个进程的线程被调度到了CPU0和CPU30上,这两个CPU分别属于不同的NUMA节点,那么线程访问的内存就可能落在远端节点上,内存延迟变大,CPU花费在等待数据上的“停顿周期”就会变多。你看到的现象是CPU占用80%,但实际计算效率可能只有60%。这时候你去优化代码、减少计算量,效果都不会太明显,真正该做的是把线程和它使用的内存放在同一个NUMA节点上——这就要用到 numactl 命令了。启动进程时用 numactl --cpunodebind=0 --membind=0 指定CPU节点和内存节点,大多数情况下能让CPU占用率出现肉眼可见的下降。
还有一个经常被忽视的点是超线程(SMT)。很多人觉得超线程等于白送的CPU资源,但实际上一个物理核上的两个逻辑核共享执行单元和缓存,如果你把两个计算密集型的线程调度到同一个物理核的两个逻辑核上,它们会互相争抢资源,单线程性能反而会下降,整体吞吐也没有想象中的翻倍。这时候如果你绑定CPU亲和性,只绑物理核而不同时绑定两个逻辑线程,效果会立竿见影。
结合上面的分析,我自己整理过一张“症状到根因的粗筛清单”,一般到这一步基本能判断出该往哪个方向深挖:
| 观察到的现象 | 可能根因 | 第一优先操作 |
|---|---|---|
| 负载低、CPU占高、si高 | 软中断集中 | 开RPS,查中断分布 |
| cs上下文切换极高 | 线程过多或锁竞争激烈 | 调线程池、精简锁 |
| 单核打满、其他核闲置 | 单线程应用或中断亲和性不强 | 绑核或中断再均衡 |
| 整体us高、耗时却上升 | NUMA跨节点访问或SMT争抢 | numactl绑节点、物理核绑核 |
| 内存足够但CPU wa高 | 磁盘IO慢导致进程阻塞 | 先定位IO再考虑业务层 |
5. 恢复与预防:从优化参数到持续观测的一揽子落地动作
把原理理解透了,最后还是要落到“怎么恢复、怎么防止再犯”上。这一节我按照代码层、系统层、调度层、观测层四个维度,分享一些实际用下来稳妥有效的做法,每一条都有具体的操作参照。
代码层是最容易被忽略、但收益最大的环节。很多CPU占用异常根本不用动系统配置,改代码就能解决。最常见的是热路径上的日志打印。有一次某个节点的CPU占用在版本发布后从20%涨到70%,查到最后是业务团队在循环里新增了一条 log.info,而且没有加采样。日志输出看着是小操作,但在高并发下它会引发大量的IO和锁竞争,CPU基本都耗在序列化字符串上了。后来他们把日志降级为异步日志,并加了限流采样,CPU立刻降了40个百分点。所以每次CPU异常,我都建议先看最近有没有变更代码,特别是日志、循环、连接池、JSON序列化这些常规嫌疑点。
系统层的调整要谨慎,但很值得做。除了前面说的RPS和irqbalance,还有一个操作是调整内核的时钟频率策略。有些服务对延迟不敏感,性能模式反而会导致CPU一直保持高主频,占用数字不好看但实际计算快;而有些服务需要稳定延迟,这时使用 tuned 或者直接设置 cpupower frequency-set -g performance 是比较好的方式。另外,如果节点上跑的是微服务应用,JVM进程可以留意GC线程造成的CPU消耗。用 jstat -gcutil <PID> 观察GC频率,如果Full GC频繁,CPU大量消耗在垃圾回收上,那就得调整堆参数,这经常是Java应用CPU占用的主角。
调度层的核心思路是把资源“管起来”。容器节点上最常见的翻身手段是给容器设置CPU限额。比如在 Docker 中可以用 --cpus=2 限制容器最多使用2个CPU,在 Kubernetes 的 Pod 配置中可以用 resources.limits.cpu: "2000m" 设置硬限制。这里的坑在于:不是加了limit就万事大吉。如果CPU请求值设得太低而limit设得高,调度器可能把多个容器塞进同一个节点,导致节点整体CPU使用率长期接近极限,这时候表现出的问题可能不是“单个容器CPU高”,而是“节点CPU高但每个容器都正常”。所以配置CPU限额时要同时关注 request 和 limit,并把节点预留一部分CPU给系统进程和内核处理。
Web服务或API网关这类直接的业务节点,还可以考虑调整线程池大小。线程池开得越大,在低并发下CPU闲置越多,在高并发下线程切换代价也越大。我个人的经验是,IO密集型应用的线程数一般设为CPU核数的两倍左右,计算密集型应用则设置成CPU核数加一,然后在压测环境里逐步调整,观察TP99和CPU占用之间的平衡。
观测层是长期平稳运行的最稳妥保障。CPU问题的真正难点,不是处理一次告警,而是做到“在它影响业务前发现它”。我建议每个节点都部署好以下几类监控指标:
- CPU利用率的整体趋势和每核分布,至少保留30天,便于回溯“上一次CPU高的时候发生了什么变更”。
- 上下文切换次数和软中断次数,这两个指标在问题刚萌芽时往往比CPU利用率更早出现异常。
- 进程线程数与单线程CPU占用,这个能从粒度上快速圈定业务进程内部的问题。
- 中断分布和RPS配置的状态,避免重启后配置丢失导致“过一阵子又犯”。
告警规则也要有所思考,不要只盯着“CPU整体利用率”。我会额外设置两个告警:一个是“单核CPU利用率超过90%并持续5分钟”,专门抓结构性挤兑;另一个是“上下文切换超过每秒10万次并持续10分钟”,专门抓线程失控。这两个告警抓到的问题,往往比整体CPU告警更早、更精准。
最后再分享一个小习惯。每次处理完一起CPU问题,我都会把当时的命令、输出、调整的参数和原因写进一份很短的排查记录,按日期命名。半年下来它就是一套属于你自己的“CPU问题指纹库”。下次再出现类似现象,翻一下这个记录,定位时间能压缩一半以上。CPU占用问题通常不会只出现一次,做好记录,其实就是积累你面对下一起告警时最可靠的底气。
