4、中断与轮询:NAPI机制详解、硬中断与软中断平衡、中断合并(Coalescing)调优
网络包从网线到应用层,中间要经历多少次「打扰」?
答案是:一次硬中断 + 一次软中断。如果处理不好,CPU 会被打断到怀疑人生。
我早年调一个 10G 网卡项目,流量刚上来,CPU 软中断就飙到 80%。查了半天,发现是硬中断没做亲和性绑定,所有流量都怼在 CPU0 上。嗯,这就是我们今天要聊的核心——NAPI 机制、中断平衡和合并调优。
4.1 传统中断模型的痛点
早期 Linux 收包流程很简单:
- 网卡收到一个包,触发 IRQ。
- CPU 暂停当前任务,进入硬中断上下文。
- 硬中断把包拷贝到内存,然后触发软中断(NET_RX_SOFTIRQ)。
- 软中断在 ksoftirqd 或当前进程上下文中处理协议栈。
问题在哪?高吞吐下,每个包都触发一次硬中断。你想想看,万兆网卡每秒千万级小包,CPU 光响应中断就忙不过来了。这叫 中断风暴(interrupt storm)。
4.2 NAPI 机制:中断 + 轮询的混合体
NAPI(New API)从 Linux 2.6 开始引入,核心思路就一句话:先中断通知,再轮询收割。
4.2.1 工作流程
- 中断触发:第一个包到达,网卡产生硬中断。
- 关闭中断:驱动在中断处理中关闭该网卡的 RX 中断,并将设备加入 poll 列表。
- 轮询模式:软中断轮询 poll 列表中的设备,批量收包(每次最多收
budget个包)。 - 退出轮询:当没有更多包时,重新开启中断,回到步骤 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(关闭) |
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 上,提高缓存命中率。
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 模式下,一个网络包从网卡到内核协议栈的完整路径:
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",看合并前后包数变化。
ethtool -C 不生效。后来才知道,virtio 网卡不支持硬件合并,得靠宿主机调。所以调优前先确认网卡型号和驱动能力。
4.7 小结
NAPI 是现代 Linux 网络性能的基石。它用「一次中断 + 批量轮询」解决了中断风暴问题。但光有 NAPI 还不够,你得学会:
- 把硬中断绑到正确的 CPU(亲和性)
- 用 RPS/RFS 分散软中断(如果硬件不支持 RSS)
- 根据场景调中断合并参数(延迟 vs 吞吐的取舍)
这些技巧,我在生产环境里反复验证过。调好了,CPU 降一半,吞吐翻倍,不是梦。