硬件基础:网卡(NIC)工作原理与中断机制(MSI-X、NAPI)
网卡这东西,说白了就是服务器和外界打交道的「大门」。我刚开始做性能优化那会儿,总觉得网卡就是个收发数据的傻硬件,后来被一次线上事故狠狠教育了一顿——才发现这扇门的开关方式,直接决定了整个系统的吞吐能力。
今天咱们就聊聊网卡的核心工作原理,以及两个绕不开的机制:MSI-X 和 NAPI。嗯,这两个名字听起来有点唬人,但拆开看其实没那么复杂。
网卡到底在干什么?
网卡的工作流程,我习惯把它分成三块:
- 接收数据:网线传来电信号,网卡把它转成数字帧,校验无误后扔到内存里
- 发送数据:内核把要发的数据放到内存,网卡自己 DMA 取走,转成电信号发出去
- 通知 CPU:数据到了或者发完了,得告诉 CPU 一声——这就是中断的活
你想想看,如果每次来一个包都要 CPU 停下手里的活去处理,那高并发下 CPU 基本就废了。所以中断机制的设计,是网卡性能的关键。
核心观点:网卡优化的本质,就是减少 CPU 被「无关紧要」的中断打扰的次数,把 CPU 的精力留给真正需要处理的数据。
中断机制的演进:从 IRQ 到 MSI-X
早期的网卡用传统 IRQ 中断。每个网卡共享一根中断线,来了中断,CPU 得一个个去问「是不是你的包?」——这叫中断轮询。我在项目中遇到过,千兆网卡跑满时,CPU 软中断占用能到 80% 以上,业务响应慢得像蜗牛。
后来有了 MSI(Message Signaled Interrupt),每个设备可以有自己的中断向量。但还不够,因为一个网卡可能只有一个中断号,所有队列的包都走同一个中断,CPU 0 忙死,其他核闲死。
MSI-X 的出现解决了这个问题。它允许一个网卡有多个中断向量,每个队列可以绑定到不同的 CPU 核上。说白了,就是给每个收包队列配一个专属的「门铃」,按门铃的人不同,去开门的 CPU 也不同。
| 中断类型 | 特点 | 适用场景 |
|---|---|---|
| 传统 IRQ | 共享中断线,CPU 需轮询 | 低负载、老设备 |
| MSI | 每个设备独立中断向量 | 中等负载 |
| MSI-X | 每个队列独立中断,可绑定 CPU | 高并发、多队列网卡 |
我的经验:配置 MSI-X 时,记得把中断亲和性(irqbalance 或手动绑核)搞好。我曾经见过一个案例,MSI-X 开了但没绑核,结果所有中断还是落在 CPU 0 上,性能跟没开一样。
NAPI:把中断和轮询结合起来
MSI-X 解决了「谁来处理」的问题,但没解决「处理方式」的问题。高流量下,每个包都触发中断,CPU 频繁上下文切换,性能照样崩。
NAPI(New API)的思路很巧妙:
- 第一个包来了,正常触发中断
- 中断处理程序不急着收完所有包,而是告诉网卡:「你先别发中断了,我待会自己来取」
- 然后切换到轮询模式,一次性收完一批包
- 收完了再重新开启中断
这样做的好处很明显:中断次数从「每个包一次」降为「每批包一次」。我做过测试,在 10Gbps 流量下,NAPI 能把 CPU 软中断开销降低 60% 以上。
注意:NAPI 在低流量下反而有副作用。因为轮询会占用 CPU,如果流量很小,轮询的空转比中断处理还浪费。好在现代网卡驱动都会动态调整——流量低时用中断,流量高时切轮询。
一张图看懂整体流程
下面这张 SVG 图,是我自己梳理的网卡收包全链路。从网线到应用,每一步的延迟点都标出来了。
实际配置怎么做?
光说不练假把式。我分享几个常用的配置手段。
1. 确认网卡支持 MSI-X
# 查看网卡中断信息
cat /proc/interrupts | grep eth0
# 如果看到多个中断号,且每个队列一个,说明 MSI-X 已启用
# 例如:
# CPU0 CPU1
# 45: 12345 6789 IR-PCI-MSI eth0-TxRx-0
# 46: 9876 54321 IR-PCI-MSI eth0-TxRx-1
2. 绑定中断到指定 CPU
# 查看当前中断亲和性
cat /proc/irq/45/smp_affinity
# 将中断 45 绑定到 CPU 0(十六进制 1)
echo 1 > /proc/irq/45/smp_affinity
# 将中断 46 绑定到 CPU 1(十六进制 2)
echo 2 > /proc/irq/46/smp_affinity
我个人的习惯:把网卡中断绑在物理核上,业务进程绑在超线程核上。这样中断处理不会抢业务进程的 L1/L2 缓存,命中率能高不少。
3. 调整 NAPI 轮询参数
# 查看当前 NAPI 权重(每次轮询最多收包数)
cat /sys/class/net/eth0/gro_flush_timeout
# 调整权重(默认 64,高吞吐场景可调大)
echo 128 > /sys/class/net/eth0/gro_flush_timeout
我曾经踩过的坑:把 NAPI 权重调得太大,结果一次轮询收了几千个包,导致其他网络事件被饿死。后来我学乖了,一般控制在 64~128 之间,配合 busy_poll 一起用效果更好。
小结一下
网卡优化的核心就两件事:减少中断次数,分散中断负载。MSI-X 解决了「谁来处理」的负载均衡问题,NAPI 解决了「怎么处理」的效率问题。两者配合,才能让 10Gbps、25Gbps 甚至 100Gbps 的网卡跑出应有的性能。
嗯,这里要注意一点:硬件优化只是第一步。下一层的内核协议栈、Socket 缓冲区、应用层的事件循环,每个环节都可能成为瓶颈。但那是后面章节的事了,今天先把网卡这关过了再说。
公众号:蓝海资料掘金营,微信deep3321