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%。

第一反应和所有人一样:翻日志。
没有慢 SQL、没有锁竞争、没有 GC 抖动。CPU 监控显示整体利用率才 30%——这就奇怪了,CPU 不忙,服务怎么会慢?
加机器?但资源明明没用满。重启?上次重启还是 30 天前,要不先试试?
等等,别急着重启。30% 的整体 CPU 利用率可能掩盖了单核的问题。
团队群里已经炸了——有人提议加机器,有人说先重启看看情况。

top 暴露了真相
按 1 展开每个核:

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 列,一目了然。

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 上各类软中断的累计计数——看哪个涨得快,就是哪个。

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 可以看调用链的热点分布:

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: 修复前后对比

| 指标 | 修复前 | 修复后 |
|---|---|---|
| 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%。
路径 — 下次遇到直接跑

看到 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 分钟。
定位 — 内核在做什么?

软中断的血泪史
很多人以为软中断是一个"内核特性",但其实它是一个不得已的设计。
早期的 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。

# 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」解决的是软中断集中问题。两者不冲突,互补。
验证

# 验证 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-eth0 或 udev 规则)
- 部分云服务器不允许改队列数(有上限或只读),这时 RPS 是唯一选择
RPS 持久化:
- /proc/sys/net/core/rps_default_mask 只对新创建的网卡生效(对 eth0 不生效)
- 现有网卡必须逐队列写:echo f > /sys/class/net/eth0/queues/rx-*/rps_cpus
- 写入 rc.local 或 systemd service 确保重启后生效
性能开销:
- RPS 有额外 CPU 开销(IP 头 hash + 跨核 IPI 中断)。在低流量场景下,RPS 可能得不偿失——多核间同步 cache 的开销可能超过单核处理的成本
- 一般建议流量 > 500Mbps 或 pps > 10 万时才考虑 RPS
- 可以用 perf stat 在开/关 RPS 前后对比 IPC(每时钟周期指令数),如果下降超过 10% 说明 cache miss 开销大于收益
修复后接口 RT 回到基线。整个排查耗时不到 1 小时——其中确认根因不到 5 分钟。
团队群里做了复盘总结——这个 case 的核心认知是:软中断高不是问题,软中断分配不均才是。

三行命令定位,两个参数修复。不用加机器,不用改代码。
内核版本说明:本文
/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拆代码的曹操」