一次服务器CPU软中断飙高导致服务间歇性超时的排查实录

一、问题背景

周三下午三点,业务群突然炸锅——客服反馈”工单系统卡得要命,点一个页面转十几秒才出来”。我看了一眼 Grafana,订单服务的 P99 延迟从平时的 120ms 飙到了 8 秒以上,Nginx 的 504 错误率从 0.01% 蹿到了 3.7%。但诡异的是,核心业务服务器(两台 32 核 64GB 的物理机,CentOS 7.9,前面挂 LVS+Keepalived)的 CPU 使用率才 58%~65%,内存也还有 22GB 空闲,磁盘 IO wait 只有 2.3%。看起来哪哪都正常,偏偏服务就是慢。

业务方催得紧,老板在群里 @ 了我三次。这次的排查过程,让我对”CPU 使用率不高就代表没事”这个惯性认知有了全新的认识。

二、故障现象

初步登录服务器后,我快速过了一遍常规指标:

1
2
3
4
5
6
7
8
9
10
$ top -bn1 | head -15
top - 15:12:34 up 137 days, 2:44, 3 users, load average: 18.45, 14.22, 11.30
Tasks: 412 total, 1 running, 411 sleeping, 0 stopped, 0 zombie
%Cpu(s): 18.2 us, 8.7 sy, 0.0 ni, 68.1 id, 0.0 wa, 0.0 hi, 5.0 si, 0.0 st
KiB Mem: 65712344 total, 43567112 used, 22145232 free, 512344 buffers
KiB Swap: 16777212 total, 1245000 used, 15532212 free. 32145678 cached Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
17812 appuser 20 0 12.345g 5.231g 28700 S 45.2 8.3 1234:12 java
5642 nginx 20 0 287456 45678 3200 S 12.3 0.1 345:23 nginx

表面看,Java 进程占 45% CPU、Nginx 占 12%,负载 18.45 对于 32 核来说也不算高——但注意 %Cpu(s) 那行有个不对劲的数字:**5.0 si**。

这里解释一下 top 中 CPU 状态各字段的含义:

  • us(user):用户态进程占用
  • sy(system):内核态占用
  • si(softirq):软中断占用——这个值是关键线索
  • hi(hardirq):硬中断占用

正常情况下,si 在 1% 以下,超过 2% 就要引起警觉,超过 5% 则通常是网卡中断风暴的前兆。

进一步用 mpstat 确认:

1
2
3
4
5
6
7
8
$ mpstat -P ALL 2 5
03:15:01 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
03:15:03 PM all 18.45 0.00 9.12 2.30 0.50 12.35 0.00 0.00 57.28
03:15:03 PM 0 2.12 0.00 1.50 0.00 0.00 92.38 0.00 0.00 4.00
03:15:03 PM 1 35.23 0.00 15.42 0.00 0.00 0.12 0.00 0.00 49.23
03:15:03 PM 2 1.35 0.00 2.34 0.00 0.00 0.08 0.00 0.00 96.23
03:15:03 PM 3 36.12 0.00 14.23 0.00 0.00 0.05 0.00 0.00 49.60
...

真相大白——**CPU 0 的 %soft 高达 92.38%**,几乎全部被软中断占满!而其他 31 个核心要么在跑业务、要么几乎空闲。32 核的机器,网络中断处理却全部压在 CPU 0 一个核上,形成了典型的”单核处理所有网卡中断”的性能瓶颈。

再查一下网卡中断分布:

1
2
3
4
5
6
$ cat /proc/interrupts | grep -E "CPU0|eth0"
CPU0 CPU1 CPU2 CPU3 ... CPU31
59: 82341456 0 0 0 ... 0 IR-PCI-MSI-edge eth0-TxRx-0
60: 102345678 0 0 0 ... 0 IR-PCI-MSI-edge eth0-TxRx-1
61: 95678901 0 0 0 ... 0 IR-PCI-MSI-edge eth0-TxRx-2
62: 89765432 0 0 0 ... 0 IR-PCI-MSI-edge eth0-TxRx-3

四队列万兆网卡(Intel X540),四个硬件中断队列(IRQ 59~62)全部绑定在 CPU 0 上。每秒近 4 亿次中断全部由一个核心处理,这个核心在软中断的泥潭里完全无法抽身。

三、排查过程

第一步:排除应用层问题

一开始我以为是 Java 应用有性能瓶颈。先用 Arthas 挂上 Java 进程看了一眼:

1
2
$ arthas-boot
[arthas@17812]$ dashboard

JVM 堆内存使用 62%,GC 频率正常(Young GC 约 15 次/分钟,Full GC 0 次),线程池没有积压。排除 JVM 层面的问题。

再看 Nginx 日志,大量请求的 upstream_response_time 高达 815 秒,但 upstream_connect_time 只有 13ms。说明 Nginx 和后端 Java 应用之间的 TCP 连接建立很快,但数据传输极慢——这直接把矛头指向了内核网络栈的处理性能

第二步:定位内核层面的瓶颈

topmpstat 已经暴露了软中断问题,但我需要确认它确实是网络软中断,而不是其他类型(如块设备、定时器、RCU 等):

1
2
3
4
5
6
7
8
9
10
11
12
$ cat /proc/softirqs | head -10
CPU0 CPU1 CPU2 ... CPU31
HI: 0 0 0 ... 0
TIMER: 45678901 23456789 21234567 ... 19876543
NET_TX: 123456 789 1023 ... 567
NET_RX: 723456789 456 789 ... 345
BLOCK: 34567 1023 2048 ... 1567
IRQ_POLL: 0 0 0 ... 0
TASKLET: 1234567 34567 23456 ... 12345
SCHED: 12345678 3456789 4567890 ... 2345678
HRTIMER: 2345678 8765432 7654321 ... 3456789
RCU: 123456789 45678901 38901234 ... 56789012

CPU 0 的 NET_RX(网络接收软中断) 高达 7.2 亿次,是 CPU 1 的 160 万倍。问题确认:所有网卡接收中断的回调函数 net_rx_action() 全部在 CPU 0 上执行,导致该核心被独占。

第三步:追溯中断亲和性配置

检查网卡中断的 SMP 亲和性(smp_affinity):

1
2
3
4
5
6
7
8
$ cat /proc/irq/59/smp_affinity
00000001
$ cat /proc/irq/60/smp_affinity
00000001
$ cat /proc/irq/61/smp_affinity
00000001
$ cat /proc/irq/62/smp_affinity
00000001

每一个中断的 smp_affinity 都是 00000001,即只允许 CPU 0 处理。这是系统默认行为,网卡驱动加载时 irqbalance 服务没有正确分发中断。

查一下 irqbalance 的状态:

1
2
3
4
$ systemctl status irqbalance
● irqbalance.service - irqbalance daemon
Loaded: loaded (/usr/lib/systemd/system/irqbalance.service; enabled)
Active: failed (Result: exit-code) since Wed 2026-05-14 03:10:23 CST; 1 months 16 days ago

果然——irqbalance 在 5 月 14 日凌晨系统自动安全更新重启后挂掉了,之后再也没起来过。这 47 天里,所有新增的网络中断都堆积在 CPU 0 上,随着业务流量逐步增长(Q2 日均请求从 180 万涨到 310 万),CPU 0 的软中断负担越来越重,直到今天下午流量峰值时彻底撑不住。

第四步:深入理解软中断的处理机制

这里有必要先理清 Linux 网络数据包的接收路径,否则很难理解为什么”一个 CPU 忙、31 个 CPU 闲”也会导致性能问题:

1
2
3
4
5
6
7
8
网卡硬件中断 → IRQ handler(极短,记录到 poll_list)
→ 触发软中断 NET_RX_SOFTIRQ
→ net_rx_action() 轮询 poll_list
→ NAPI poll(网卡驱动的 poll 函数)
→ 从 Ring Buffer 取数据包
→ 构建 sk_buff
→ 送入协议栈(IP→TCP→Socket)
→ 唤醒等待该 Socket 的用户态进程

关键问题在于:用户态进程被唤醒后,内核调度器倾向于将进程调度到”距离数据最近的 CPU”上执行——也就是唤醒它的那个 CPU。如果进程恰好被调度到了 CPU 0(软中断所在核心),而 CPU 0 此时正忙于处理海量软中断(%soft 92%),进程几乎没有 CPU 时间来执行实际业务逻辑。这就形成了一个恶性循环:

1
2
3
4
5
6
7
软中断占满 CPU 0
→ 用户态进程被调度到 CPU 0(被唤醒)
→ 进程得不到 CPU 时间片(被软中断抢占)
→ 进程处理请求变慢
→ 请求积压、TCP 接收缓冲区塞满
→ 触发更多数据包到达、更多软中断
→ 软中断更重、CPU 0 更忙

如果用 perf 来验证,会更直观:

1
2
3
4
5
6
7
8
9
10
$ perf top -C 0
Samples: 1M of event 'cpu-clock', Event count (approx.): 123456789000
Overhead Shared Object Symbol
42.35% [kernel] [k] net_rx_action
18.72% [kernel] [k] __netif_receive_skb_core
12.45% [kernel] [k] ip_rcv
8.91% [kernel] [k] tcp_v4_rcv
5.23% [kernel] [k] process_backlog
3.12% [kernel] [k] napi_gro_receive
...

CPU 0 上 90% 以上的时间都在处理网络协议栈,真正留给用户态进程的时间不到 10%。而一个正常的 Java 请求处理需要经过 JSON 反序列化、业务逻辑计算、数据库查询、结果组装等步骤——被挤压到 10% 的 CPU 时间里,自然慢如蜗牛。

四、解决方案

紧急止血:手动绑定网卡中断到多个核心

业务不能等,先手动把四个网卡中断队列分别绑到不同的 CPU 核心:

1
2
3
4
5
6
7
8
# 关闭 irqbalance(防止它反向操作覆盖我们的设置)
systemctl stop irqbalance

# 将中断队列分别绑定到 CPU 0、8、16、24(分散在不同的物理核心和 NUMA 节点上)
echo 00000100 > /proc/irq/59/smp_affinity # IRQ 59 → CPU 8
echo 00000100 > /proc/irq/60/smp_affinity # IRQ 60 → CPU 8
echo 00010000 > /proc/irq/61/smp_affinity # IRQ 61 → CPU 16
echo 01000000 > /proc/irq/62/smp_affinity # IRQ 62 → CPU 24

smp_affinity 的值是一个 16 进制位掩码,每一位代表一个 CPU。例如 01000000 的二进制表示为 CPU 24。

验证中断分布变化:

1
2
3
4
5
$ cat /proc/interrupts | grep eth0 | awk '{print $1, $2, $3, $4, $5, $NF}'
59: 82341456 0 0 0 ... eth0-TxRx-0
60: 102345678 0 0 0 ... eth0-TxRx-1 # 刚改完,历史计数还在
61: 95678901 0 0 0 ... eth0-TxRx-2
62: 89765432 0 0 0 ... eth0-TxRx-3

历史计数不会清零,等待几秒后再次查看,确认新的中断确实在流向指定的 CPU:

1
$ watch -n1 'cat /proc/interrupts | grep eth0'

大约 30 秒后,mpstat 确认软中断已分散:

1
2
3
4
5
6
$ mpstat -P 0,8,16,24 1
03:22:30 PM CPU %usr %sys %soft
03:22:31 PM 0 1.23 2.45 3.12
03:22:31 PM 8 0.56 1.23 18.45
03:22:31 PM 16 0.34 1.56 20.12
03:22:31 PM 24 0.67 1.34 17.89

Nginx 504 错误率应声从 3.7% 跌到 0.02%,P99 延迟从 8 秒回落到 180ms。

长期优化:RPS + RFS 进一步分散软中断

手动中断绑定解决了燃眉之急,但四个硬件队列只能分散到四个核心,对于 32 核机器来说远远不够。更好的方案是启用 RPS(Receive Packet Steering),让内核在软件层面把数据包分发给更多核心处理:

1
2
3
4
5
6
7
8
9
10
11
12
# 启用 RPS:把 CPU 0-31 全部加入 eth0 各队列的处理核心掩码
# ffffffff = 32 个 CPU 全部参与软中断处理
echo ffffffff > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo ffffffff > /sys/class/net/eth0/queues/rx-1/rps_cpus
echo ffffffff > /sys/class/net/eth0/queues/rx-2/rps_cpus
echo ffffffff > /sys/class/net/eth0/queues/rx-3/rps_cpus

# 调整 RPS 流表大小(默认 4096,高并发场景建议 32768)
echo 32768 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
echo 32768 > /sys/class/net/eth0/queues/rx-1/rps_flow_cnt
echo 32768 > /sys/class/net/eth0/queues/rx-2/rps_flow_cnt
echo 32768 > /sys/class/net/eth0/queues/rx-3/rps_flow_cnt

再启用 RFS(Receive Flow Steering),让内核把同一流的数据包定向到应用所在的 CPU,进一步提升缓存命中率:

1
2
3
4
5
6
7
# 设置全局 RFS 表大小
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 为每个接收队列设置 RFS 条目数
for i in $(seq 0 3); do
echo 8192 > /sys/class/net/eth0/queues/rx-$i/rps_flow_cnt
done

持久化这些配置到 /etc/rc.d/rc.local 或 systemd 服务,确保重启后不丢失:

1
2
3
4
5
6
7
8
9
cat >> /etc/rc.d/rc.local << 'EOF'
# RPS/RFS optimization for eth0
for q in /sys/class/net/eth0/queues/rx-*; do
echo ffffffff > $q/rps_cpus
echo 32768 > $q/rps_flow_cnt
done
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
EOF
chmod +x /etc/rc.d/rc.local

修复 irqbalance 服务

根源之一是 irqbalance 挂了没人知道,修复并加固:

1
2
3
4
5
6
# 置入正式的中断绑定策略脚本
cat > /etc/sysconfig/irqbalance << 'EOF'
IRQBALANCE_ARGS="--hintpolicy=exact --banirq=59 --banirq=60 --banirq=61 --banirq=62"
EOF
systemctl enable irqbalance
systemctl start irqbalance

这里用 --banirq 把我们已经手动绑定好的网卡中断排除在 irqbalance 的自动管理之外,避免它反转我们的设置。

五、根因分析

这次问题的根本原因是一个三层连锁失效

  1. irqbalance 服务静默故障:5 月 14 日的安全更新重启了服务器,irqbalance 因依赖的某些内核模块加载时序问题启动失败,但没有触发任何告警。此后 47 天,服务器一直在”IRQ 全部绑在 CPU 0”的单核中断处理模式下运行。

  2. 缺乏软中断监控:我们的 Prometheus 监控采集了 CPU 使用率、内存、磁盘、网络流量等指标,但没有采集 /proc/softirqs 的数据。node_exporter 默认的 --collector.interrupts 参数只采集硬中断(/proc/interrupts),不采集软中断(/proc/softirqs)。这导致 CPU 0 的软中断使用率从 2% 慢慢爬到 92% 的过程中,监控面板一片风平浪静。

  3. 业务流量自然增长越过临界点:Q2 日均请求量从 180 万涨到 310 万(+72%),而”单核处理所有网络中断”这个架构的极限大约在日均 250 万请求左右。超过这个临界点后,软中断开始抢占用户态 CPU 时间,性能呈断崖式下跌,而不是线性退化。

六、预防措施

1. 补齐软中断监控

node_exporter 启动参数中加入软中断采集:

1
2
3
4
5
# /etc/systemd/system/node_exporter.service 修改
ExecStart=/usr/local/bin/node_exporter \
--collector.interrupts \
--collector.softirqs \
--web.listen-address=:9100

对应地,在 Prometheus 中添加告警规则:

1
2
3
4
5
6
7
8
9
10
11
12
# softirq_alerts.yml
groups:
- name: softirq
rules:
- alert: HighSoftirqPerCPU
expr: rate(node_softirqs_total{softirq="NET_RX"}[5m]) > 100000
for: 5m
labels:
severity: warning
annotations:
summary: "CPU {{ $labels.cpu }} 网络软中断过高"
description: "当前速率 {{ $value | humanize }}/s,可能导致服务延迟增加"

2. 建立服务器上线标准模板

今后所有新上线的物理服务器或大规格虚机,必须在初始化阶段完成以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 标准网络优化脚本(纳入装机 Kickstart/PXE 模板)
# 1. 启用 RPS
# 2. 设置 Ring Buffer 大小
# 3. 启用 TSO/GRO
# 4. 调整 TCP backlog

# RPS 在所有队列启用
for q in /sys/class/net/*/queues/rx-*; do
[ -f $q/rps_cpus ] && echo ffffffff > $q/rps_cpus
[ -f $q/rps_flow_cnt ] && echo 32768 > $q/rps_flow_cnt
done

# Ring Buffer 拉到最大
ethtool -G eth0 rx 4096 tx 4096

# TCP backlog
sysctl -w net.core.netdev_max_backlog=5000
sysctl -w net.core.somaxconn=4096

3. irqbalance 存活监控

在基础监控中加入 irqbalance 进程存活检测:

1
2
3
4
5
6
7
8
9
# 利用 node_exporter 的 textfile collector
# 或直接用 process_exporter
- alert: IrqbalanceDown
expr: absent(process_start_time_seconds{process="irqbalance"})
for: 10m
labels:
severity: critical
annotations:
summary: "irqbalance 服务未运行"

4. 定期性能基线巡检

每季度对核心服务器做一次性能基线检查,包括中断分布、软中断速率、NUMA 亲和性等——用脚本自动化,而不是靠人记住。

七、总结

这次排查给我上了深刻的一课:**”CPU 整体使用率不高”绝对不等于”CPU 没有瓶颈”**。top 那行 %Cpu(s) 里面的 si(软中断)字段,平时看起来永远是个位数,以至于大多数人(包括之前的我)都习惯性忽略它。但恰恰是这个不起眼的指标,一旦集中在单核上,就能把整台服务器拖垮。

回顾整个过程,工具链用得并不复杂——topmpstat/proc/interrupts/proc/softirqsperf top——都是 Linux 自带的基础工具。真正的难点在于思维路径:当 CPU 不高、内存够用、磁盘不忙、网络带宽也充足的时候,你的排查方向应该往哪里走?这次经历让我意识到,中断处理(硬中断 + 软中断)是一个经常被遗漏的排查维度。现代服务器动辄几十甚至上百核,如果中断亲和性配置不当,”一核有难、多核围观”的场面会比想象中更容易出现。

另外,监控的盲区就是事故的土壤。我们花了大量精力监控应用指标、基础设施指标,却偏偏漏掉了软中断这个看似”底层”的指标。47 天的时间里,CPU 0 的软中断从正常慢慢走到极限,如果有哪怕一条与此相关的告警,都不至于等到业务方投诉才发现。

最后抛一个问题给各位同行:你们线上服务器的 /proc/softirqs 分布是什么样的?不妨现在就打开看看,说不定有惊喜。


本文涉及的 /proc 文件系统和 sysfs 接口均为 Linux 标准接口,配置示例基于 CentOS 7.9 和 Intel X540 网卡,其他发行版和网卡型号请根据实际情况调整。