CPU 才 30%,接口就崩了?一核有难七核围观

场景:Java 服务接口 RT 从 20ms 飙到 2s,CPU 整体使用率只有 30%,但 si 占了 80% 路径:top 发现 si 异常 → mpstat 确认单核 → /proc/softirqs 定位 NET_RX → perf top 锁定驱动 → 网卡多队列 + RPS 拆散

上篇我们聊了上下文切换如何偷走 CPU 时间,这次来看软中断——一个让单核默默扛下所有的场景。

CPU 使用率才 30%,接口 RT 却飙到 2 秒。应用日志看不出任何异常。

但 top 一跑,si 列赫然占着 80%。


症状

接口突然崩了

下午 14:00,报警群炸了。

核心交易接口的 P99 RT 从 20ms 直线飙升到 2s,接着是 5s。上游服务开始超时重试,重试又加剧了负载。15 分钟内,错误率从 0.1% 跳到了 8%。

RT 监控曲线——上午平稳在 20ms,下午 14:00 开始直线上升到 2s+

第一反应和所有人一样:翻日志。

没有慢 SQL、没有锁竞争、没有 GC 抖动。CPU 监控显示整体利用率才 30%——这就奇怪了,CPU 不忙,服务怎么会慢?

加机器?但资源明明没用满。重启?上次重启还是 30 天前,要不先试试?

等等,别急着重启。30% 的整体 CPU 利用率可能掩盖了单核的问题。

团队群里已经炸了——有人提议加机器,有人说先重启看看情况。

SRE 值班群告警讨论——CPU0 异常高,有人提议重启,刘老师指出是软中断问题

top 暴露了真相

1 展开每个核:

top -1 输出——CPU0 的 si 列高达 80%,us 只有 5%;CPU1-3 的 si 都在 5% 以下

top - 14:23:45 up 30 days,  1 user,  load average: 2.50, 2.30, 2.10
Tasks: 123 total,   1 running, 122 sleeping,   0 stopped,   0 zombie
%Cpu0  :  5.0 us, 85.0 si, 10.0 id
%Cpu1  : 15.0 us,  5.0 si, 80.0 id
%Cpu2  : 10.0 us,  3.0 si, 87.0 id
%Cpu3  : 12.0 us,  4.0 si, 84.0 id

看到 si 那列的时候,心里有数了。

CPU0 的 si(softirq——内核软中断处理)占着 85%,而其他核的 si 只有 3-5%。整体 CPU 30% 是因为三个核在 idle,但 CPU0 已经快烧穿了。

对手线 Naive:"RT 高 → 加机器 → 不管用 → 重启 → 还是不管用"

加机器和重启都解决不了——因为问题不是应用进程,是内核把软中断全部调度到了 CPU0 上。加机器加的是应用进程的核,不是内核软中断的核。

si 是什么? si(softirq)是 Linux 内核处理软中断的 CPU 时间占比。网卡收包、协议栈处理、定时器回调都在这里跑。正常情况 si 应在 5% 以下,超过 30% 说明软中断在大量消耗 CPU。


诊察

step 1: 哪个核在扛?— mpstat 确认

top 已经给了线索,但 mpstat 更精确——它把 CPU 时间细分到 %soft 列,一目了然。

mpstat -I SCPU -P ALL 1 输出——CPU0 的 %soft 为 80%,其余核均为 3-5%

10:30:01     CPU    %usr   %nice   %sys   %iowait    %irq   %soft   %steal   %idle
10:30:01       0    5.00    0.00   10.00      0.00    0.00   80.00     0.00    5.00
10:30:01       1   15.00    0.00    5.00      0.00    0.00    5.00     0.00   75.00
10:30:01       2   10.00    0.00    3.00      0.00    0.00    3.00     0.00   84.00
10:30:01       3   12.00    0.00    4.00      0.00    0.00    4.00     0.00   80.00

%soft 列 80%、CPU0 vs CPU1-3 的差距 > 15x——实锤了,软中断全部压在单核上

为什么我先跑 mpstat 而不是 sar? mpstat -I SCPU 可以实时看到每个核的软中断占比,秒级刷新。sar 虽然能看历史趋势,但排查当下问题时,实时数据比历史曲线更能帮你定位。生产排查的原则是:先用最快的命令确认方向,再用更精确的工具下钻。

如果你在 %soft 列看到了超过 30% 的值,基本可以确认软中断在吃 CPU。而 %irq(硬件中断)通常很低(< 5%),因为硬中断只做最基本的通知,重活全在 softirq 里。

step 2: 什么中断类型?— /proc/softirqs 定位

知道是软中断了,但软中断有很多种:网络收包(NET_RX)、网络发包(NET_TX)、定时器(TIMER)、任务队列(TASKLET)……是哪种在吃 CPU?

/proc/softirqs 给出了每个 CPU 上各类软中断的累计计数——看哪个涨得快,就是哪个。

cat /proc/softirqs 两次输出对比——NET_RX 在 CPU0 上的计数超出其他核两个数量级且在持续增长

                    CPU0       CPU1       CPU2       CPU3
          HI:          0          0          0          0
       TIMER:     152345     145678     138901     142345
      NET_TX:        456        234        345        123
      NET_RX:   98765432     123456      98765     234567  ← CPU0 高出 2 个数量级
       BLOCK:       1234       2345       3456       1234
     TASKLET:        678        456        567        345
       SCHED:     234567     234567     234567     234567
     HRTIMER:        123        234        345        123
         RCU:     345678     345678     345678     345678

NET_RX(网络收包软中断) 在 CPU0 上的计数是 9876 万,其他核只有 9-23 万——差了 两个数量级。而且 watch -d 能看到它在持续增长。

术语/proc/softirqs 是内核暴露每个 CPU 上各类软中断计数的接口。每行是一个软中断类型,每列是一个 CPU。数值是累计计数,不重置——所以重点是看相对差增长趋势

这里有个观察技巧:TIMER 在四核上基本均衡(14-15 万),说明定时器软中断没问题。NET_TX 很低说明发包量不大。唯独 NET_RX 在 CPU0 上异常——所有网络流量全压在 CPU0 处理

step 3: 驱动层在忙什么?— perf top 下钻

现在知道是网络收包导致的软中断。但网络收包处理路径很长:驱动轮询 → 协议栈 → socket → 应用。具体哪个函数在耗 CPU?

perf top -G -U 可以看调用链的热点分布:

perf top -G -U 输出——ixgbe_poll 占 42%,ixgbe_clean_rx_irq 占 15%

Samples: 10K of event 'cpu-clock', 4000 Hz
  Children      Self  Symbol
+  99.55%     0.00%  [kernel.kallsyms]
+  98.12%     0.00%  __softirqentry_text_start
+  96.50%     0.00%  net_rx_action
+  84.20%    42.10%  ixgbe_poll            ← 驱动轮询收包
+  42.10%     0.00%  napi_consume_skb
+  30.50%    15.20%  ixgbe_clean_rx_irq    ← 清理 RX 描述符
+  15.10%     0.00%  __kmem_cache_free
+  12.30%     0.00%  kfree_skb
+  10.10%     0.00%  ip_local_deliver_finish
+   8.50%     0.00%  tcp_v4_rcv

Children 列是包含子函数调用的总开销。ixgbe_poll 的 children 84%——也就是说 84% 的 CPU 时间都花在了网卡驱动轮询收包上,其中 42% 是 poll 函数本身,15% 是清理 RX 描述符。

术语:NAPI(New API)是 Linux 网卡驱动的新式收包机制。在 NAPI 之前,每个数据包都触发一个硬件中断,高吞吐下会导致"中断风暴"——CPU 光处理中断就忙不过来。NAPI 的解决思路是:硬中断触发后关中断,切到轮询模式,在软中断上下文里批量收包。好处是高吞吐时不会中断风暴,坏处是 softirq 会一直占着 CPU。这就是你看到 si 高的原因——驱动在努力收包,但努力过头了

回顾一下当前状态:CPU0 上 ixgbe 驱动在疯狂轮询收包,其他三个核在围观。一核有难,七核围观

step 4: 修复前后对比

diff 对比——修复前 CPU0 %soft=80%,修复后降至 15%,四核均匀分摊

指标 修复前 修复后
CPU0 %soft 80% 15%
CPU1 %soft 5% 22%
CPU2 %soft 3% 20%
CPU3 %soft 4% 18%
NET_RX CPU0 9876 万 234 万
接口 RT P99 5s 35ms

这个数字的变化说明了一个简单的道理:网络收包的处理能力是没有变的,变的是分布。 修复前一个核扛 100%,修复后四个核各扛 25%。


路径 — 下次遇到直接跑

三步定位命令组合——top/mpstat → /proc/softirqs → perf top

看到 si% > 30% 时,按这个顺序跑:

# 1. 看哪个核在扛
top -1                     # 看 si% + 单核分布,3 秒出结果
mpstat -I SCPU -P ALL 1    # 确认 si 集中在哪个核,更精确

# 2. 定位中断类型
cat /proc/softirqs         # 看哪个计数器高
watch -d -n 1 cat /proc/softirqs  # 盯变化趋势

# 3. 下钻到驱动热点
perf top -G -U             # 看热函数在驱动哪一层(交互式,等 10-15 秒)

判断规则:

看到 说明
si% 整体高于 30% 软中断在吃 CPU,不是应用问题
si 集中在单个核 中断/软中断没做亲和性均衡
NET_RX 异常高 网络收包是根因
驱动 poll 函数热点 NAPI 轮询处理不过来

如果服务器没有 perf 怎么办?用 /proc/softirqs 多看几轮,观察 NET_RX 的增长速率也能判断。如果每秒增长几十万,网络流量很大了。

经验之谈:这三个命令的排障时间约 30 秒。top 第一眼 3 秒,mpstat 确认 5 秒,softirqs 定位 10 秒,perf 下钻 15 秒。从接到报警到确认根因——不超过 1 分钟。


定位 — 内核在做什么?

Diagram: 硬中断 → CPU0 hardirq → raise NET_RX softirq → ixgbe_poll 轮询 → 协议栈 → 应用。CPU1-3 空闲等待

软中断的血泪史

很多人以为软中断是一个"内核特性",但其实它是一个不得已的设计

早期的 Linux 用硬件中断处理所有收包。每个数据包来一个中断——10 万个包就是 10 万次中断。到了 90 年代末期,千兆网卡普及后,这种模式直接崩了。CPU 花在中断上下文切换上的时间比实际处理包的时间还多。

这就是 NAPI 诞生的背景:关中断,开轮询

NAPI 的流程是: 1. 网卡收到数据包 → 触发硬件中断 2. CPU 响应中断 → 关掉网卡的中断 → 把网卡挂在 CPU 的 softirq 轮询列表上 3. CPU 退出硬中断上下文 → 立即进入软中断上下文 → 调用驱动的 poll 函数批量收包 4. 一直收,直到收完或者达到预算(netdev_budget,默认 300 个包) 5. 收完再开中断,等下一波

这个流程本身没问题——它解决了中断风暴。但它引入了一个新问题:谁触发软中断,谁就负责处理到底。

为什么是单核?

关键就在这里。

硬件中断由 IRQ 亲和性(/proc/irq/{N}/smp_affinity)决定在哪个 CPU 上触发。如果 BIOS 或者内核没特殊配置,默认所有 IRQ 都落在 CPU0 上

CPU0 响应硬中断 → CPU0 raise NET_RX softirq → CPU0 进入 net_rx_action → CPU0 调用 ixgbe_poll所有收包都在 CPU0 上完成

其他三个核呢?它们在 idle,或者跑应用代码。

这就是"一核有难七核围观"的根源:IRQ 没做均衡,softirq 也不跨核迁移——谁触发谁负责,触发者被累死。

金句:"Linux 不会无缘无故变慢——它只是在等你找到哪个资源饱和了。"

这次是软中断队列在 CPU0 上饱和了。不是 CPU 不够,是网卡的收包处理没有分散到所有核。


处方

扩网卡队列 + 开 RPS

修复思路很简单:把单核的任务拆到多核。有三种方式,从上到下推荐:

方法 原理 适用场景
扩网卡队列(ethtool -L) 让网卡把流量分散到多个硬件队列 网卡支持多队列
RPS 跨核均衡 用软件把包分发到其他核的 softirq 任何网卡
调整 IRQ affinity 手动绑 IRQ 到指定核 精细控制

大多数云服务器用的是多队列网卡(ixgbe/ena/virtio),先试 ethtool 扩队列,不行再上 RPS。

修复命令——ethtool 扩队列 + RPS 跨核均衡 + irqbalance 确认

# 1. 查看当前网卡队列数
ethtool -l eth0
# 输出: Pre-set maximums: RX: 4, TX: 4, Combined: 4
#        Current hardware settings: RX: 1, TX: 1, Combined: 1
# 当前只有 1 个队列,最大支持 4 个

# 2. 扩到 4 个队列
ethtool -L eth0 combined 4

# 3. 开启 RPS(Receive Packet Steering)跨核均衡
# f = 二进制 1111 = CPU0-3 都参与收包处理
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
cat /sys/class/net/eth0/queues/rx-0/rps_cpus
# 输出: 00000000,0000000f

# 4. 确认 irqbalance 运行
systemctl status irqbalance
# 如果没运行:systemctl start irqbalance && systemctl enable irqbalance

# 5. 持久化 RPS(写入 sysctl 或 rc.local)
# /etc/sysctl.d/90-rps.conf:
# net.core.rps_default_mask = f
# 或者写到 /etc/rc.d/rc.local

术语:RPS 是内核的软中断跨核分发机制。网卡硬件队列只能被一个核的硬中断触发,但 RX 包入队后,RPS 通过 hash 把包调度到其他核的 softirq 去处理——相当于用软件实现了多队列。RPS 的 mask 是 bitmask,f=15 表示四个核都参与,1 表示只绑 CPU0。

重要:irqbalance 和 RPS 的关系

irqbalance 服务会自动分配硬件中断到不同 CPU。如果你开了 irqbalance,IRQ 本身可能已经被分散了——但每个硬件队列触发后,softirq 仍然只在该 CPU 上跑。

所以「扩队列 + irqbalance」能解决硬中断集中问题,但「RPS」解决的是软中断集中问题。两者不冲突,互补。

验证

修复后 mpstat 输出——四核 %soft 均匀分布在 15-22%

# 验证 1:mpstat 看分布
mpstat -I SCPU -P ALL 1
# 期望:四核 %soft 在 15-25%

# 验证 2:软中断计数
cat /proc/softirqs | grep NET_RX
# 期望:四核数量相近,不再 CPU0 独占

# 验证 3:接口 RT
curl -w '%{time_total}\n' -o /dev/null -s http://localhost:8080/api/trade
# 期望:回到 20ms 基线

修复后 CPU0 的 %soft 从 80% 降到 15%,四个核均匀分摊在 15-22%。接口 RT 回到 20ms,P99 35ms。

生产环境注意事项

扩队列(ethtool -L): - 重启网卡后失效,需写入网卡配置文件(/etc/sysconfig/network-scripts/ifcfg-eth0udev 规则) - 部分云服务器不允许改队列数(有上限或只读),这时 RPS 是唯一选择

RPS 持久化: - /proc/sys/net/core/rps_default_mask 只对新创建的网卡生效(对 eth0 不生效) - 现有网卡必须逐队列写:echo f > /sys/class/net/eth0/queues/rx-*/rps_cpus - 写入 rc.localsystemd service 确保重启后生效

性能开销: - RPS 有额外 CPU 开销(IP 头 hash + 跨核 IPI 中断)。在低流量场景下,RPS 可能得不偿失——多核间同步 cache 的开销可能超过单核处理的成本 - 一般建议流量 > 500Mbps 或 pps > 10 万时才考虑 RPS - 可以用 perf stat 在开/关 RPS 前后对比 IPC(每时钟周期指令数),如果下降超过 10% 说明 cache miss 开销大于收益

修复后接口 RT 回到基线。整个排查耗时不到 1 小时——其中确认根因不到 5 分钟。

团队群里做了复盘总结——这个 case 的核心认知是:软中断高不是问题,软中断分配不均才是。

SRE 值班群复盘——讨论为什么 NET_RX 会堆在 CPU0,RPS 适用场景,上线前检查清单

三行命令定位,两个参数修复。不用加机器,不用改代码。


内核版本说明:本文 /proc/softirqs 和 RPS 路径适用于 Linux 2.6.35+,irqbalance 服务适用于 systemd 系统(CentOS 7+/Ubuntu 16.04+)。早期内核(2.6.35 之前)的 /proc/softirqs 字段可能不同(如缺少 RCU/HRTIMER)。RPS 在 2.6.35 引入,之前内核无此功能。


下篇我们聊 Load Average 很高但 CPU 空闲——不是应用代码慢,是进程在等什么?

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