Linux 中的软中断与硬中断:高负载下为什么软中断会成为瓶颈

本文从硬中断与软中断的分工讲起,分析高负载下网络协议栈处理集中在 NET_RX 等软中断的原因,梳理定位方法、常见误区和 RPS、中断合并等优化手段的适用场景,帮助后端和运维工程师排查 si 高与延迟抖动问题。

硬中断和软中断是怎么分工的

处理高负载服务时,很多人会遇到一个现象:业务进程 CPU 占用似乎不高,但整机延迟在涨,top 里的 si 一栏已经超过 30%,或者某个 ksoftirqd 进程占满了整颗 CPU。这种时候,问题通常不在业务代码,而在 Linux 中断处理路径上的软中断。

AI technology illustration

硬中断是硬件设备发给 CPU 的异步信号。比如网卡收到数据包后,通过中断通知 CPU 有数据要处理。CPU 会暂停当前任务,跳去执行驱动注册的中断处理函数。这个函数运行在中断上下文里,优先级极高,它会关中断、禁止抢占,所以必须短平快:拷贝数据、清中断标识、唤醒必要的内核逻辑,然后就返回。

如果所有协议栈处理都在硬中断里做,一次网络收包就会让整机长时间暂停响应。所以内核把工作拆成两段:硬中断阶段只做开始的动作,把耗时部分丢给软中断。软中断不是一个线程,而是一组挂在内核全局队列上的回调逻辑,它会在当前 CPU 从异常或中断返回用户态之前被检查一遍。如果处理不完,内核会唤醒 ksoftirqd 内核线程继续处理。

这里要区分两个容易混淆的概念:ksoftirqd 是软中断的执行载体之一,而不是全部。大多数时候,软中断直接在中断返回路径上执行,只有持续不断到达的包才会触发 ksoftirqd 接管。也就是说,你看到 ksoftirqd 高,意味着软中断处理已经跟不上网络速率了。

在实际系统里,最常见的软中断是网络收包对应的 NET_RX,以及定时器 TIMER。很多高负载问题的根源都在这两个现场里。

高负载下,软中断为什么先顶不住

软中断容易成为瓶颈,不是因为内核写得差,而是因为它把大量的协议栈处理集中到了 CPU 的内核态。网络收包是一个明显案例:网卡支持 NAPI 时,硬中断触发频率会被降下来,驱动通过轮询批量拿数据,然后把 sk_buff 交给协议栈。之后 IP 校验、TCP 分片重组、socket 队列、内存分配基本都在 NET_RX 软中断里完成。

当机器跑的是 Nginx 或 Java 网关这类网络型服务时,每秒几十万个包并不夸张。每个包都走一遍协议栈,CPU 的 si 时间自然上涨。问题在于软中断抢占优先级高于普通进程调度,流量一大,用户态业务进程就不断被打断。QPS 还没到上限,P99 延迟先上来了。

另一个细节容易被忽略:软中断处理是局部性的。硬中断可以靠中断亲和性绑定到多个核,但如果驱动不是多队列,或 RPS 没有配置,软中断依然会集中在同一个 CPU 上。多核机器的优势会被这种不均衡吃掉不少。具体你可以观察 /proc/softirqs,很多情况下会看到某个 CPU 上的 NET_RX 计数远远高于其他核。

虚拟化环境里还有一层复杂性。物理网卡产生的中断会先被宿主机处理,再由 vhost 线程投递给 guest。guest 看到的网络软中断,本质上是宿主机给它制造的税。如果宿主机中断绑定不合理,guest 里即使调整内核参数,效果也有限。

如果你维护长连接服务,另一个典型场景是大批量定时器到期。TIMER 软中断会在某个时间点冲高,直接把 CPU 打满。这种问题单看网络参数往往定位不到,因为触发源在应用层的定时器管理。

怎么定位软中断瓶颈

定位软中断问题,不需要上来就用动态追踪。普通命令已经能看到大部分情况。

# 查看各类软中断从开机到现在的累计计数,对比各 CPU 分布
cat /proc/softirqs

# 查看 CPU 时间占比,重点关注 si 和 ksoftirqd 进程
top -1

# 确认热点是否在收包路径上
perf top -G -e cycles

这几个命令组合起来,能回答三个问题:是不是软中断高,是哪种类型的软中断高,分布在哪些 CPU 上。比如 /proc/softirqs 里 NET_RX 在 CPU0 上累计值远高于其他核,说明收包路径集中;top 里 si 时间高但 ksoftirqd 不高,说明软中断多数在中断返回路径上直接消耗 CPU;如果 ksoftirqd 也拉满,说明当前软中断执行已经系统化地霸占了核。

如果确认是 NET_RX 高,下一步看网卡队列:ethtool -l eth0 可以查看当前和最大队列数。多队列网卡配合中断亲和性才能把流量分散到多个核。

三个容易踩的误区

很多团队在处理这类问题时,会陷入几个固定思路。

  • 误区一:把硬中断当成主要开销。实际上对于网络包,硬中断只做搬运,大头在软中断的协议栈处理。只调长大网卡 coalesce 参数,减少硬中断频率,不一定能缓解软中断。
  • 误区二:硬中断均衡到多个核就够了。硬中断分散,不等于软中断分散。尤其驱动单队列时,所有包仍会进入同一个队列并由同一个队列所在核心处理,后续的软中断自然集中。
  • 误区三:si 高就加 CPU。如果软中断只集中在一个核,多核机器加 CPU 并不会让问题自动消失,必须先解决负载均衡。

这些误区的共同点,是把中断数量和中断处理成本混为一谈。真正消耗 CPU 的是处理逻辑在软中断上下文里的执行时间,不是中断触发的次数。

优化手段怎么选

软中断优化的核心思路不是消灭它,而是让处理负载更合理地分布,并减少协调代价。不同的手段适应不同阶段和硬件。

手段 机制 适用场景 代价与注意点
中断亲和性 把网卡硬件中断绑定到指定 CPU 低负载或队列数小于核数时 只影响硬中断,软中断可能仍集中
RPS 接收路径上按哈希把 skb 分到不同 CPU 网卡不支持下发多队列 增加调度和缓存一致性开销
RFS 按流维持 CPU 亲和,提升缓存命中 长连接多、单连接流量大 需要额外 hash 表,内存占用略升
中断合并 攒批再硬中断,降低触发频率 追求吞吐,延迟不敏感 会提高单包延迟,不适合低延迟服务
Busy Polling 用户态轮询 socket 收包 低延迟高性能中间件 CPU 占用高,使用场景受限
XDP/eBPF 在驱动侧或早期路径处理/丢弃包 DDoS 过滤、高吞吐转发 开发成本高,需匹配驱动支持

你不需要一开始就上 XDP。多数团队首先要做的是确认硬中断队列数量、配置正确的多队列和 RPS,再把延迟敏感服务的 coalesce 调小一些。这样就能解决大部分软中断集中问题。

落地时值得注意的几个原则

基于实际排查经验,有几个原则值得写下来。

  1. 先量化再改动。记录 /proc/softirqs 和 top si 的基线,否则无法判断优化是否有效。
  2. 队列数不要超过物理核数,也不要绑到超线程核上,否则缓存局部性反而变差。
  3. 硬中断亲和性与 RPS 的设置不要重叠。硬中断已经绑定到的 CPU,不要再参与 RPS 分摊,否则会同时处理物理中断和远程 socket 队列,形成竞争。
  4. 修改 sysctl 或 ethtool 参数后要做 A/B 对比,重点观察 P99 延迟而不是平均 CPU。
  5. 在容器环境里,内核参数通常是宿主共享的,排序问题会变成租户隔离问题,需要提前规划。

另外要提一下 scheduler 相关参数。内核里有 net.core.busy_poll,也有调整 ksoftirqd 时间片的参数,但这些只能暂时压低症状,不能改变软中断负载总量。如果确认是某个特定业务导致的软中断高,从应用层减包、合并请求、或减少定时器频率,往往更有效。

怎么理解软中断这个瓶颈

软中断是 Linux 为了兼顾实时响应和系统吞吐设计出来的机制。它在高负载下成为瓶颈,本质上是因为内核态的协议栈处理速度跟不上硬件到达速度。优化时可以把它看成另一个 CPU 消费者:它有自己的优先级、亲和性和配额,需要在业务进程、硬中断和软中断之间做资源分配。

回到最初的问题:为什么高负载下软中断先顶不住?因为它既继承了硬件中断的紧急属性,又没有线程层面的睡眠能力,必须一口气处理完一批包。当到达速率超过处理速率,它就开始霸占 CPU,以牺牲用户态进程为代价换取网络栈向前推进。理解了这条因果链,再做 RPS、中断亲和性、或者上 eBPF,心里就有数了。

如果你下次遇到某个核 si 高、业务延迟抖动,不要第一时间改业务代码。拉着 /proc/softirqs 和 ethtool -l 先看几分钟,大概率能少走很多弯路。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/626/

(0)
上一篇 47分钟前
下一篇 45分钟前

相关推荐