整体 CPU 才 20%,一个核却烧到 100%?中断全堆它头上了

场景:整体 CPU 20%,单核 100% 告警,网卡吞吐从 900MB/s 腰斩到 400MB/s 路径:top 单核 → /proc/interrupts 中断分布 → irqbalance 服务状态 → IRQ 亲和原理 → 修复

上篇我们聊了容器 CFS 节流(throttling)如何偷偷挂线程,这次从"容器"回到"物理机"——一个 16 核的网关机,整体 CPU 才 20%,却有个核常年烧在 100%。应用层日志干干净净,RT 却抖成了心电图。

症状

现象:整体 20%,单核 100%

下午 15:30,监控告警:CPU0 使用率 99.8%,持续 10 分钟。但同一块面板上,整机平均 CPU 只有 20%。

API 网关的 P99 RT 从 8ms 抖到 80ms,网卡吞吐从 900MB/s 掉到 400MB/s——流量没降,出口却像是被人掐了脖子。

监控面板上,整机 CPU 平均 20%,但 CPU0 的使用率曲线像一根烧红的针一样刺在 100%。

第一反应和所有人一样:翻应用日志。没有慢 SQL、没有锁竞争、没有 GC 抖动。top 按 1 展开后真相浮出来——CPU0 的 hi 列占着 95%,其他 15 个核全在围观。

hi 是什么? top 的 hi 列表示 CPU 花在处理硬件中断(硬中断)上的时间占比。网卡收包、磁盘 IO 完成、定时器到期都会触发硬件中断。正常情况 hi 应在 1% 以下,超过 50% 说明硬件中断正在吞噬 CPU。注意:硬中断和软中断在 top 里是两列——hi(硬件中断处理)和 si(软中断处理),这次问题在 hi 上。

整体 CPU 只有 20% 不是因为系统闲——是 16 个核里 1 个在燃烧,15 个在睡觉,平均值把火情盖住了。平均值掩盖了单核瓶颈,这是 CPU 排查里最经典的视觉陷阱。

反直觉:加机器为什么没用?

团队第一反应还是那三板斧——重启、加机器、调 JVM 参数。但这次都有个直觉上的坑:

加机器加的是应用进程的核,不是中断的核。 中断由内核分配,不随应用扩缩容迁移。你加一台机器,中断还是堆在原来的 CPU0 上——新机器的核闲着,旧机器的 CPU0 继续烧。


诊察

命令 1:top -1 确认单核分布

top -1 展开每个核——CPU0 的 hi 列 95%,其余 15 核 hi 全在 0-1%

CPU0 的 hi 95%,si 5%;其他核 hi 全在 0-1%。硬中断全部堆在 CPU0——和上次软中断篇(si 堆单核)症状不同,这次是 hi(硬中断)而不是 si(软中断)。

为什么我先跑 top 而不是直接看 /proc/interrupts? top 3 秒给出"哪个核、哪类时间"的全局图,先确定方向。/proc/interrupts 是定位细节的,等确认是中断问题后再下钻。排查原则:先用最快的命令确认方向,再用精确的工具定位。

命令 2:/proc/interrupts 看中断落在哪个核

知道是硬中断了,但是哪个中断、落在哪个核/proc/interrupts 给出了每个中断在每颗核上的累计计数——看哪个涨得快、落在哪,就是它。

cat /proc/interrupts 输出——eth0 的 4 个 TX/RX 队列中断计数全部堆在 CPU0 列,CPU1-15 为 0

4 个网卡队列中断(eth0-TxRx-0~3)全部堆在 CPU0,计数 1-4 亿,其他 15 核全为 0。10G 网卡满速跑时,1500B 的 MTU 下 900MB/s 约等于每秒 60 万个包,每个包触发一次中断,CPU0 光响应中断就忙不过来。

为什么网卡有 4 个队列还是堆在 CPU0? 网卡多队列(RSS)只是把流量 hash 到 4 个队列,但每个队列的中断可以绑定到任意核。队列的 MSI-X 向量在注册时绑定到 boot CPU(默认 CPU0)——没有 irqbalance 或者它没干活,4 个队列就全部落在一颗核上

命令 3:smp_affinity + irqbalance 服务状态

中断到底允不允许跑在其他核?看亲和性掩码:

cat /proc/irq/{46-49}/smp_affinity_list 输出全是 0,再查 systemctl status irqbalance 显示 inactive (dead)

三个证据连起来读:

  1. smp_affinity_list 全是 0——4 个队列中断只允许在 CPU0 上触发,其他核根本没资格处理;
  2. systemctl status 显示 Active: inactive (dead)——irqbalance 服务根本没在跑;
  3. cat /etc/sysconfig/irqbalance 发现 IRQBALANCE_BANNED_CPUS="fffe"——十六进制掩码,bit 位为 1 表示该 CPU 被禁止参与中断均衡fffe = 1111 1111 1111 1110,bit0 是 0、bit1-15 全是 1:只允许 CPU0 接收中断,其余 15 颗核全被 ban 了

服务和配置两层都没干活——这是本次排查的"有鬼"时刻:不是硬件坏、不是驱动 bug,是一行配置文件把 15 颗核全锁死了


路径 — 下次遇到直接跑

三步定位命令组合

看到 hi% > 50% 时,按这个顺序跑:

# 1. 看哪个核在扛(3 秒)
top -1                     # 看 hi% + 单核分布

# 2. 看中断落在哪、是哪个中断(5 秒)
grep -E 'eth0|ens|em[0-9]' /proc/interrupts   # 看网卡中断计数分布
mpstat -P ALL 1             # 更精确:逐核 %irq/%soft 列

# 3. 看亲和性 + irqbalance 状态(3 秒)
cat /proc/irq/46/smp_affinity_list
systemctl status irqbalance
cat /etc/sysconfig/irqbalance    # 或 /etc/default/irqbalance(Debian)

判断规则

看到 说明
hi% 高、si% 硬件中断问题(不是软中断)
中断计数全堆 CPU0 列 中断没做均衡
smp_affinity_list 全是 0 亲和性被限制或没分散
irqbalance inactive 或 BANNED_CPUS 有值 均衡服务没跑/配置把核 ban 了

定位 — 内核为什么让一个核扛下所有?

IRQ 亲和原理图——中断默认亲和 CPU0,irqbalance 守护进程周期性把中断迁移到多核,但 BANNED_CPUS 把它锁死

这不是应用层的问题——是系统层的中断分配环节出了问题:内核把所有 IRQ 亲和到 CPU0,而 irqbalance 被禁用、BANNED_CPUS 又把其余 15 核锁死。 回看症状段"应用层日志干干净净"的排除结论——应用层确实没病,病在内核的中断路由上。

硬中断默认落在 CPU0

硬件中断的亲和性由 /proc/irq/{N}/smp_affinity 控制——这是一个 bitmask,bit 位为 1 表示该中断允许在这颗核上触发。多数硬件中断在 boot 时默认落在 CPU0(boot CPU)。

为什么默认 CPU0?因为系统初始化时中断控制器(APIC)只把 CPU0 设为可用目标。而本文场景的网卡 MSI-X 向量(Message Signaled Interrupts,消息信号中断,PCIe 设备写特定地址触发中断的方式)在驱动注册时绑定到 boot CPU——如果没有 irqbalance 或驱动主动改写亲和性,中断就永远留在 CPU0

irqbalance 本该解决这件事

irqbalance 是一个守护进程,它周期性(默认 10 秒)扫描 /proc/interrupts,计算每颗核的中断负载,然后把中断迁移到负载低的核上——这就是"中断均衡"。

但这次它没干活:

  1. 服务状态 inactive (dead)——irqbalance 没在运行。为什么没跑?Loaded: disabled,服务被设为不随开机启动。可能是安装时没 enable,或某次"优化"里被关掉了。
  2. IRQBALANCE_BANNED_CPUS="fffe"——就算服务启动,配置也把它锁死了:除了 CPU0 的 15 颗核全被 ban。两个问题叠加,中断只能堆 CPU0。

内核版本说明:IRQ 亲和机制(/proc/irq/*/smp_affinity)在所有主流内核版本通用。irqbalance 的配置文件路径有差异——RHEL/CentOS 是 /etc/sysconfig/irqbalance,Debian/Ubuntu 是 /etc/default/irqbalance,但 IRQBALANCE_BANNED_CPUS 变量语义一致。


处方

修复 1:修正 BANNED_CPUS 配置

把误配的 fffe 清掉,让所有核都参与中断均衡:

$ cat /etc/sysconfig/irqbalance
# IRQBALANCE_BANNED_CPUS: 禁止参与中断均衡的 CPU(十六进制掩码)
IRQBALANCE_BANNED_CPUS=""     # 空 = 所有核都可参与

修复 2:启动并启用 irqbalance

$ systemctl start irqbalance
$ systemctl enable irqbalance
$ systemctl status irqbalance
● irqbalance.service - irqbalance daemon
   Active: active (running)

验证:中断重新分散

修复前后对比——CPU0 hi 从 95% 降到 4%,eth0 的 4 个队列中断分散到 CPU0/4/8/12

修复后 CPU0 的 hi 降到 4%,top 单核输出一目了然:

$ top -1
%Cpu0  :  8.0 us,  1.0 sy,  0.0 ni, 86.0 id,  0.0 wa,  4.0 hi,  1.0 si,  0.0 st
指标 修复前 修复后
CPU0 hi% 95% 4%
中断分布 全堆 CPU0 分散到 4 颗核
网卡吞吐 400MB/s 880MB/s
网关 P99 RT 80ms 9ms

三行配置,不用加机器,不用改代码,网卡吞吐翻倍。

上线前检查清单

检查项 命令
irqbalance 是否启用 systemctl is-enabled irqbalance
BANNED_CPUS 是否有值 grep BANNED /etc/sysconfig/irqbalance
中断是否分散 grep eth0 /proc/interrupts 看多核列
单核 hi 是否异常 top -1 看各核 hi 列

附:完整命令清单

全量速查(7 步)

# 1. 看单核分布(先确认方向,3 秒)
top -1

# 2. 确认是硬中断还是软中断
mpstat -P ALL 1                # 逐核 %irq vs %soft 列

# 3. 看中断计数分布(哪类中断、落在哪个核)
grep -E 'eth0|ens|em[0-9]' /proc/interrupts

# 4. 看中断亲和性
cat /proc/irq/{46,47,48,49}/smp_affinity_list

# 5. 查 irqbalance 服务状态与配置
systemctl status irqbalance
cat /etc/sysconfig/irqbalance        # RHEL/CentOS
cat /etc/default/irqbalance          # Debian/Ubuntu

# 6. 修复:清空 BANNED_CPUS + 启动服务
#    /etc/sysconfig/irqbalance 中 IRQBALANCE_BANNED_CPUS="" 或删除该行
systemctl start irqbalance
systemctl enable irqbalance

# 7. 验证
grep eth0 /proc/interrupts           # 看是否分散到多核
top -1                               # CPU0 hi% 应明显下降

下篇我们聊系统调用过多导致 CPU 100%——这一次 CPU 时间全花在内核态,sys 列飙高,strace 定位法登场。

📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」