网卡驱动与中断调优:NAPI、中断合并与RSS/RPS
各位好,这一章我们聊聊网卡驱动和中断。说实话,很多搞网络的人把精力都放在协议栈上层,觉得驱动是内核的事。但我个人经验是——性能瓶颈往往就卡在中断处理这个环节。你想想看,数据从网线进来,第一站就是驱动和中断,这里要是堵了,上层再优化也白搭。
我当年接手过一个项目,千兆网卡跑满就丢包,CPU 占用率却不到 30%。查了半天,发现是中断处理太频繁,CPU 光顾着响应中断,没时间收包。后来调了 NAPI 和中断合并,问题就解决了。嗯,咱们今天就系统讲讲这几个关键机制。
NAPI 机制:从「中断风暴」到「轮询+中断」
先说说 NAPI。这玩意儿全称是 New API,其实已经不新了,但它是现代网卡驱动的基石。
传统方式是这样的:每个数据包到达,网卡就触发一次中断。如果流量小还好,一旦流量大了——比如万兆网卡每秒几百万个包——CPU 就被中断淹没了。这叫中断风暴(interrupt storm)。CPU 大部分时间都在保存上下文、恢复上下文,根本没空处理数据。
NAPI 的思路很简单:中断来了先关掉,然后轮询收包。等包收完了再开中断。这样在流量大的时候,中断次数大幅减少,CPU 可以专心收包。
核心流程:
- 网卡触发中断 → 驱动注册的 NAPI 回调被调用
- 驱动关闭该网卡的中断(不再产生新中断)
- 驱动在软中断上下文中轮询(poll)收包
- 收完所有包或达到预算(budget)后,重新开启中断
这里有个关键参数叫 budget,就是一次轮询最多收多少个包。默认一般是 300。我建议你根据实际流量调整——如果包很小(比如 64 字节),可以调大 budget;如果包很大,调小一点,避免长时间占用 CPU。
我的习惯:在 /sys/class/net/eth0/napi_defer_hard_irqs 和 gro_flush_timeout 这两个参数上花点时间。前者控制延迟多少微秒才触发硬中断,后者控制轮询超时。配合好了,能显著降低 CPU 开销。
中断合并(Coalescing):用延迟换吞吐
中断合并,说白了就是让网卡攒一批包再通知 CPU。你想想看,如果每个包都中断一次,CPU 累死;如果攒够 10 个包再中断,CPU 就轻松多了。代价是延迟增加了。
这个机制在网卡硬件层面实现,驱动通过 ethtool 来配置。常用的参数有:
| 参数 | 含义 | 典型值 |
|---|---|---|
| rx-usecs | 接收中断延迟(微秒) | 0-100 |
| rx-frames | 接收帧数阈值 | 0-255 |
| tx-usecs | 发送中断延迟(微秒) | 0-100 |
| tx-frames | 发送帧数阈值 | 0-255 |
| adaptive-rx | 自适应接收合并(硬件自动调节) | on/off |
配置命令示例:
# 查看当前中断合并设置
ethtool -c eth0
# 设置接收延迟为 50 微秒,帧数阈值为 8
ethtool -C eth0 rx-usecs 50 rx-frames 8
# 开启自适应接收合并
ethtool -C eth0 adaptive-rx on
我曾经踩过的坑:有一次为了追求低延迟,把 rx-usecs 设成了 0。结果万兆网卡跑满时,CPU 软中断占用率飙到 90%,系统几乎不可用。后来调到 10 微秒,延迟只增加了不到 10%,CPU 占用率却降到了 40%。所以别一味追求极端值,要实测。
自适应合并(adaptive-rx)是个好东西。网卡硬件会根据当前流量动态调整合并策略。流量大时多合并,流量小时少合并。我个人习惯在大多数场景下开启它,除非你有严格的延迟要求。
RSS(Receive Side Scaling):让多核分担收包
RSS 是网卡硬件特性。它把收到的包根据哈希值(通常是四元组:源IP、目的IP、源端口、目的端口)分配到不同的接收队列,每个队列绑定一个 CPU 核心。这样多个核心可以并行收包,避免单核成为瓶颈。
配置 RSS 主要看两个东西:队列数和哈希策略。
# 查看网卡支持的队列数
ethtool -l eth0
# 设置队列数为 4(假设硬件支持)
ethtool -L eth0 combined 4
# 查看当前哈希策略
ethtool -n eth0 rx-flow-hash tcp4
# 设置 TCP/IPv4 使用源IP+目的IP+源端口+目的端口做哈希
ethtool -N eth0 rx-flow-hash tcp4 sdfn
哈希策略的字母含义:
- s:源IP
- d:目的IP
- f:源端口
- n:目的端口
我建议你把四元组都用上,这样同一个连接的数据包会落到同一个队列,避免乱序。但要注意,有些老网卡不支持全四元组哈希,那就退而求其次用 IP 对。
关键点:RSS 的队列数和 CPU 核心数要匹配。不是越多越好。比如你有 16 核,设 16 个队列没问题;但如果只有 4 核,设 16 个队列反而会因为上下文切换降低性能。我一般设为核心数的一半到等量。
RPS(Receive Packet Steering):软件版的 RSS
RPS 是内核提供的软件方案。当你的网卡不支持 RSS,或者队列数不够时,可以用 RPS 在软件层面把包分发到不同 CPU。
RPS 的工作原理:在软中断处理中,根据包的哈希值选择一个目标 CPU,然后通过 IPI(核间中断)把包发给那个 CPU 处理。
配置方式:
# 查看当前 RPS 配置(每个接收队列一个文件)
cat /sys/class/net/eth0/queues/rx-0/rps_cpus
# 设置 RPS 使用 CPU 0-3(bitmask 为 0x0f)
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
bitmask 的每一位代表一个 CPU。比如 0x0f 表示 CPU 0-3,0xff 表示 CPU 0-7。
我的建议:RPS 是软件方案,有额外开销(IPI 中断)。如果网卡支持 RSS,优先用 RSS。RPS 适合以下场景:
- 网卡不支持 RSS 或队列数太少
- 虚拟化环境,宿主机限制了网卡队列
- 需要更精细的 CPU 亲和性控制
RFS(Receive Flow Steering)与 aRFS
RFS 是 RPS 的增强版。RPS 只保证包被不同 CPU 处理,但不保证同一个流(flow)的包始终在同一个 CPU 上。RFS 解决了这个问题——它根据应用程序所在的 CPU 来分发包,让数据包尽量在处理它的应用程序所在的 CPU 上被接收。
aRFS(硬件加速 RFS)需要网卡支持,效果更好。配置方式:
# 开启 RFS(设置每个队列的流表大小)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
# 设置每个接收队列的流表大小
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
这里有个经验值:rps_sock_flow_entries 设为最大并发连接数,rps_flow_cnt 设为队列数除以上面那个值。比如你预计有 32768 个并发连接,4 个队列,每个队列设 8192。
注意:RFS 和 aRFS 会增加 CPU 之间的 IPI 通信。如果 CPU 之间 NUMA 跨度大,性能可能反而下降。我在 NUMA 架构的服务器上吃过亏,后来只在同一个 NUMA 节点内做 RFS。
知识体系总览
下面这张图总结了本章的核心逻辑:
调优实战建议
说了这么多,最后给一套我常用的调优步骤:
- 先看硬件能力:用
ethtool -l eth0看最大队列数,用lscpu看 NUMA 拓扑 - 设置 RSS 队列数:匹配 CPU 核心数,注意 NUMA 亲和性
- 调整中断合并:低延迟场景关自适应,高吞吐场景开自适应
- 开启 NAPI:现代驱动默认开启,检查
gro_flush_timeout是否合理 - 如果队列不够:用 RPS 补充,配合 RFS 保证流亲和性
- 实测验证:用
perf top看软中断分布,用mpstat -P ALL 1看 CPU 负载是否均衡
最后说一句:没有万能配置。同样的参数,在 Intel 82599 上跑得好,换到 Mellanox ConnectX-5 上可能就翻车。每次调整后都要做 A/B 测试,用真实流量验证。我习惯在压测环境先跑 30 分钟,观察 CPU 软中断占比和丢包率,再决定是否上线。