一次服务器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 | $ top -bn1 | head -15 |
表面看,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 | $ mpstat -P ALL 2 5 |
真相大白——**CPU 0 的 %soft 高达 92.38%**,几乎全部被软中断占满!而其他 31 个核心要么在跑业务、要么几乎空闲。32 核的机器,网络中断处理却全部压在 CPU 0 一个核上,形成了典型的”单核处理所有网卡中断”的性能瓶颈。
再查一下网卡中断分布:
1 | $ cat /proc/interrupts | grep -E "CPU0|eth0" |
四队列万兆网卡(Intel X540),四个硬件中断队列(IRQ 59~62)全部绑定在 CPU 0 上。每秒近 4 亿次中断全部由一个核心处理,这个核心在软中断的泥潭里完全无法抽身。
三、排查过程
第一步:排除应用层问题
一开始我以为是 Java 应用有性能瓶颈。先用 Arthas 挂上 Java 进程看了一眼:
1 | $ arthas-boot |
JVM 堆内存使用 62%,GC 频率正常(Young GC 约 15 次/分钟,Full GC 0 次),线程池没有积压。排除 JVM 层面的问题。
再看 Nginx 日志,大量请求的 upstream_response_time 高达 815 秒,但 3ms。说明 Nginx 和后端 Java 应用之间的 TCP 连接建立很快,但数据传输极慢——这直接把矛头指向了内核网络栈的处理性能。upstream_connect_time 只有 1
第二步:定位内核层面的瓶颈
top 和 mpstat 已经暴露了软中断问题,但我需要确认它确实是网络软中断,而不是其他类型(如块设备、定时器、RCU 等):
1 | $ cat /proc/softirqs | head -10 |
CPU 0 的 NET_RX(网络接收软中断) 高达 7.2 亿次,是 CPU 1 的 160 万倍。问题确认:所有网卡接收中断的回调函数 net_rx_action() 全部在 CPU 0 上执行,导致该核心被独占。
第三步:追溯中断亲和性配置
检查网卡中断的 SMP 亲和性(smp_affinity):
1 | $ cat /proc/irq/59/smp_affinity |
每一个中断的 smp_affinity 都是 00000001,即只允许 CPU 0 处理。这是系统默认行为,网卡驱动加载时 irqbalance 服务没有正确分发中断。
查一下 irqbalance 的状态:
1 | $ systemctl status irqbalance |
果然——irqbalance 在 5 月 14 日凌晨系统自动安全更新重启后挂掉了,之后再也没起来过。这 47 天里,所有新增的网络中断都堆积在 CPU 0 上,随着业务流量逐步增长(Q2 日均请求从 180 万涨到 310 万),CPU 0 的软中断负担越来越重,直到今天下午流量峰值时彻底撑不住。
第四步:深入理解软中断的处理机制
这里有必要先理清 Linux 网络数据包的接收路径,否则很难理解为什么”一个 CPU 忙、31 个 CPU 闲”也会导致性能问题:
1 | 网卡硬件中断 → IRQ handler(极短,记录到 poll_list) |
关键问题在于:用户态进程被唤醒后,内核调度器倾向于将进程调度到”距离数据最近的 CPU”上执行——也就是唤醒它的那个 CPU。如果进程恰好被调度到了 CPU 0(软中断所在核心),而 CPU 0 此时正忙于处理海量软中断(%soft 92%),进程几乎没有 CPU 时间来执行实际业务逻辑。这就形成了一个恶性循环:
1 | 软中断占满 CPU 0 |
如果用 perf 来验证,会更直观:
1 | $ perf top -C 0 |
CPU 0 上 90% 以上的时间都在处理网络协议栈,真正留给用户态进程的时间不到 10%。而一个正常的 Java 请求处理需要经过 JSON 反序列化、业务逻辑计算、数据库查询、结果组装等步骤——被挤压到 10% 的 CPU 时间里,自然慢如蜗牛。
四、解决方案
紧急止血:手动绑定网卡中断到多个核心
业务不能等,先手动把四个网卡中断队列分别绑到不同的 CPU 核心:
1 | # 关闭 irqbalance(防止它反向操作覆盖我们的设置) |
smp_affinity 的值是一个 16 进制位掩码,每一位代表一个 CPU。例如 01000000 的二进制表示为 CPU 24。
验证中断分布变化:
1 | $ cat /proc/interrupts | grep eth0 | awk '{print $1, $2, $3, $4, $5, $NF}' |
历史计数不会清零,等待几秒后再次查看,确认新的中断确实在流向指定的 CPU:
1 | $ watch -n1 'cat /proc/interrupts | grep eth0' |
大约 30 秒后,mpstat 确认软中断已分散:
1 | $ mpstat -P 0,8,16,24 1 |
Nginx 504 错误率应声从 3.7% 跌到 0.02%,P99 延迟从 8 秒回落到 180ms。
长期优化:RPS + RFS 进一步分散软中断
手动中断绑定解决了燃眉之急,但四个硬件队列只能分散到四个核心,对于 32 核机器来说远远不够。更好的方案是启用 RPS(Receive Packet Steering),让内核在软件层面把数据包分发给更多核心处理:
1 | # 启用 RPS:把 CPU 0-31 全部加入 eth0 各队列的处理核心掩码 |
再启用 RFS(Receive Flow Steering),让内核把同一流的数据包定向到应用所在的 CPU,进一步提升缓存命中率:
1 | # 设置全局 RFS 表大小 |
持久化这些配置到 /etc/rc.d/rc.local 或 systemd 服务,确保重启后不丢失:
1 | cat >> /etc/rc.d/rc.local << 'EOF' |
修复 irqbalance 服务
根源之一是 irqbalance 挂了没人知道,修复并加固:
1 | # 置入正式的中断绑定策略脚本 |
这里用 --banirq 把我们已经手动绑定好的网卡中断排除在 irqbalance 的自动管理之外,避免它反转我们的设置。
五、根因分析
这次问题的根本原因是一个三层连锁失效:
irqbalance 服务静默故障:5 月 14 日的安全更新重启了服务器,irqbalance 因依赖的某些内核模块加载时序问题启动失败,但没有触发任何告警。此后 47 天,服务器一直在”IRQ 全部绑在 CPU 0”的单核中断处理模式下运行。
缺乏软中断监控:我们的 Prometheus 监控采集了 CPU 使用率、内存、磁盘、网络流量等指标,但没有采集
/proc/softirqs的数据。node_exporter默认的--collector.interrupts参数只采集硬中断(/proc/interrupts),不采集软中断(/proc/softirqs)。这导致 CPU 0 的软中断使用率从 2% 慢慢爬到 92% 的过程中,监控面板一片风平浪静。业务流量自然增长越过临界点:Q2 日均请求量从 180 万涨到 310 万(+72%),而”单核处理所有网络中断”这个架构的极限大约在日均 250 万请求左右。超过这个临界点后,软中断开始抢占用户态 CPU 时间,性能呈断崖式下跌,而不是线性退化。
六、预防措施
1. 补齐软中断监控
在 node_exporter 启动参数中加入软中断采集:
1 | # /etc/systemd/system/node_exporter.service 修改 |
对应地,在 Prometheus 中添加告警规则:
1 | # softirq_alerts.yml |
2. 建立服务器上线标准模板
今后所有新上线的物理服务器或大规格虚机,必须在初始化阶段完成以下配置:
1 | # 标准网络优化脚本(纳入装机 Kickstart/PXE 模板) |
3. irqbalance 存活监控
在基础监控中加入 irqbalance 进程存活检测:
1 | # 利用 node_exporter 的 textfile collector |
4. 定期性能基线巡检
每季度对核心服务器做一次性能基线检查,包括中断分布、软中断速率、NUMA 亲和性等——用脚本自动化,而不是靠人记住。
七、总结
这次排查给我上了深刻的一课:**”CPU 整体使用率不高”绝对不等于”CPU 没有瓶颈”**。top 那行 %Cpu(s) 里面的 si(软中断)字段,平时看起来永远是个位数,以至于大多数人(包括之前的我)都习惯性忽略它。但恰恰是这个不起眼的指标,一旦集中在单核上,就能把整台服务器拖垮。
回顾整个过程,工具链用得并不复杂——top、mpstat、/proc/interrupts、/proc/softirqs、perf top——都是 Linux 自带的基础工具。真正的难点在于思维路径:当 CPU 不高、内存够用、磁盘不忙、网络带宽也充足的时候,你的排查方向应该往哪里走?这次经历让我意识到,中断处理(硬中断 + 软中断)是一个经常被遗漏的排查维度。现代服务器动辄几十甚至上百核,如果中断亲和性配置不当,”一核有难、多核围观”的场面会比想象中更容易出现。
另外,监控的盲区就是事故的土壤。我们花了大量精力监控应用指标、基础设施指标,却偏偏漏掉了软中断这个看似”底层”的指标。47 天的时间里,CPU 0 的软中断从正常慢慢走到极限,如果有哪怕一条与此相关的告警,都不至于等到业务方投诉才发现。
最后抛一个问题给各位同行:你们线上服务器的 /proc/softirqs 分布是什么样的?不妨现在就打开看看,说不定有惊喜。
本文涉及的 /proc 文件系统和 sysfs 接口均为 Linux 标准接口,配置示例基于 CentOS 7.9 和 Intel X540 网卡,其他发行版和网卡型号请根据实际情况调整。