服务器 CPU 软中断飙高,服务为何间歇超时?

服务器CPU使用率60%不高,top也看不出异常进程,但服务频繁超时——最终定位到网络软中断全部压在单核上,这才是隐藏的性能杀手。

一、问题背景

周三下午三点,业务群突然炸锅——客服反馈"工单系统卡得要命,点一个页面转十几秒才出来"。我看了一眼 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 网卡,其他发行版和网卡型号请根据实际情况调整。

使用 Hugo 构建
主题 StackJimmy 设计