4、中断与轮询:NAPI机制详解、硬中断与软中断平衡、中断合并(Coalescing)调优

网络包从网线到应用层,中间要经历多少次「打扰」?

答案是:一次硬中断 + 一次软中断。如果处理不好,CPU 会被打断到怀疑人生。

我早年调一个 10G 网卡项目,流量刚上来,CPU 软中断就飙到 80%。查了半天,发现是硬中断没做亲和性绑定,所有流量都怼在 CPU0 上。嗯,这就是我们今天要聊的核心——NAPI 机制、中断平衡和合并调优。

4.1 传统中断模型的痛点

早期 Linux 收包流程很简单:

  1. 网卡收到一个包,触发 IRQ。
  2. CPU 暂停当前任务,进入硬中断上下文。
  3. 硬中断把包拷贝到内存,然后触发软中断(NET_RX_SOFTIRQ)。
  4. 软中断在 ksoftirqd 或当前进程上下文中处理协议栈。

问题在哪?高吞吐下,每个包都触发一次硬中断。你想想看,万兆网卡每秒千万级小包,CPU 光响应中断就忙不过来了。这叫 中断风暴(interrupt storm)。

核心矛盾: 中断是异步的,但频繁中断导致 CPU 上下文切换开销 > 实际处理开销。

4.2 NAPI 机制:中断 + 轮询的混合体

NAPI(New API)从 Linux 2.6 开始引入,核心思路就一句话:先中断通知,再轮询收割。

4.2.1 工作流程

  1. 中断触发:第一个包到达,网卡产生硬中断。
  2. 关闭中断:驱动在中断处理中关闭该网卡的 RX 中断,并将设备加入 poll 列表。
  3. 轮询模式:软中断轮询 poll 列表中的设备,批量收包(每次最多收 budget 个包)。
  4. 退出轮询:当没有更多包时,重新开启中断,回到步骤 1。

说白了,NAPI 把「每个包一个中断」变成了「一批包一次中断」。我调优时经常看 /proc/net/softnet_stat,如果 processed 远大于 received,说明 NAPI 在高效工作。

4.2.2 关键参数

参数 位置 说明
budget /proc/sys/net/core/netdev_budget 每次轮询最多处理的包数,默认 300
weight 驱动层(如 igb/ixgbe) 每个 NAPI 实例每次轮询的配额,通常等于 budget
gro_flush_timeout /sys/class/net/ethX/gro_flush_timeout GRO 超时时间,单位纳秒,默认 0(关闭)
我的习惯: 在纯转发场景(如 LVS/DPVS),我会把 netdev_budget 调到 600-900,减少软中断唤醒次数。但注意不要太大,否则其他协议栈(如 TCP)可能饿死。

4.3 硬中断与软中断的平衡

中断处理分两步:硬中断(上半部)和软中断(下半部)。硬中断要快,软中断可以慢。但两者跑在哪个 CPU 上,直接影响性能。

4.3.1 中断亲和性(IRQ Affinity)

通过 /proc/irq/{IRQ_NUM}/smp_affinity 设置。比如把 eth0 的硬中断绑定到 CPU2-3:

# 查看 eth0 的 IRQ 号
cat /proc/interrupts | grep eth0

# 绑定到 CPU2(位掩码 0x04)
echo 04 > /proc/irq/130/smp_affinity

# 或者用 irqbalance 自动分配
systemctl start irqbalance

我曾经踩过一个坑:irqbalance 在 NUMA 环境下会把中断分配到远端 CPU,导致跨 NUMA 访问内存,延迟飙升。后来我手动绑到同一个 NUMA 节点上的 CPU,延迟降了 30%。

4.3.2 RPS/RFS:软中断的负载均衡

如果硬件不支持多队列(RSS),可以用 RPS(Receive Packet Steering)把软中断分散到多个 CPU:

# 开启 RPS,把 eth0 的软中断分散到 CPU0-3
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus

RFS(Receive Flow Steering)更进一步,根据流哈希把包送到处理该流的 CPU 上,提高缓存命中率。

注意: RPS/RFS 会增加 CPU 间 IPI(核间中断),在万兆以上场景建议直接用硬件 RSS。我见过有人开了 RPS 后性能反而下降,就是因为 IPI 开销太大。

4.4 中断合并(Interrupt Coalescing)调优

中断合并就是让网卡攒一批包再发中断。参数一般在 ethtool 里调:

# 查看当前合并参数
ethtool -c eth0

# 设置 RX 合并:每 125 微秒或每 256 个包发一次中断
ethtool -C eth0 rx-usecs 125 rx-frames 256

# 关闭合并(低延迟场景)
ethtool -C eth0 rx-usecs 0 rx-frames 0

4.4.1 参数详解

参数 含义 典型值
rx-usecs 最大延迟(微秒),攒够时间发中断 0(低延迟)~ 125(吞吐优先)
rx-frames 最大包数,攒够数量发中断 0 ~ 256
tx-usecs 发送侧合并延迟 通常与 rx 对称
adaptive-rx 自适应合并(网卡自动调节) on/off

4.4.2 调优策略

  • 低延迟场景(如高频交易、Redis):rx-usecs 0,每个包立即中断。代价是 CPU 开销高。
  • 高吞吐场景(如文件服务器、视频流):rx-usecs 125 + rx-frames 256,减少中断次数。
  • 自适应模式:适合流量波动大的场景,网卡自动调整。

我记得有一次帮客户调 CDN 边缘节点,他们抱怨 CPU 软中断高。我一看,rx-usecs 设成了 0。改成 50 微秒后,CPU 降了 40%,吞吐反而涨了 15%。因为 CPU 不再被中断淹没,有更多时间处理协议栈。

4.5 核心流程:从硬中断到协议栈

下面这张图展示了 NAPI 模式下,一个网络包从网卡到内核协议栈的完整路径:

NAPI 收包流程 NAPI 收包流程:从硬中断到协议栈 ① 网卡收包 ② 硬中断触发 ③ 关闭中断 / 加入 poll 列表 ④ 软中断轮询(NAPI poll) ⑤ 批量收包(budget) ⑥ 协议栈处理 还有包? → 继续轮询 无包 → 重新开启中断

4.6 监控与调优工具

实战中我常用这几个命令:

  • 中断分布:cat /proc/interrupts | grep eth0,看每个 CPU 的中断数是否均衡。
  • 软中断统计:cat /proc/net/softnet_stat,第 2 列是 processed,第 3 列是 dropped(CPU 来不及处理)。
  • 延迟测量:perf record -e irq:irq_handler_entry -a -- sleep 10,分析中断延迟。
  • 合并效果:ethtool -S eth0 | grep -E "rx_packets|rx_bytes|rx_coalesce",看合并前后包数变化。
避坑指南: 我曾经在 KVM 虚拟机里调中断合并,发现 ethtool -C 不生效。后来才知道,virtio 网卡不支持硬件合并,得靠宿主机调。所以调优前先确认网卡型号和驱动能力。

4.7 小结

NAPI 是现代 Linux 网络性能的基石。它用「一次中断 + 批量轮询」解决了中断风暴问题。但光有 NAPI 还不够,你得学会:

  • 把硬中断绑到正确的 CPU(亲和性)
  • 用 RPS/RFS 分散软中断(如果硬件不支持 RSS)
  • 根据场景调中断合并参数(延迟 vs 吞吐的取舍)

这些技巧,我在生产环境里反复验证过。调好了,CPU 降一半,吞吐翻倍,不是梦。