第4章:网卡高级特性:硬件卸载、RSS与RPS、多队列、中断调节
各位好,欢迎来到第四章。
前几章我们聊了CPU、内存、内核协议栈。但说实话,数据从网线进来,到应用层拿到,中间最关键的瓶颈往往就在网卡这一层。你想想看,网卡是数据进入系统的第一道门,如果这道门设计得不好,后面CPU再快也是白搭。
这一章,我们来拆解网卡的那些高级特性。我个人习惯把这些特性分成四类:硬件卸载(Offloading)、多队列与RSS/RPS、以及中断调节。咱们一个一个来。
4.1 硬件卸载(Offloading)——让网卡替你干活
什么叫硬件卸载?说白了,就是把本来CPU该干的活,扔给网卡去干。网卡上有专用的硬件电路,干这些活比CPU快得多,还省电。
常见的卸载类型:
- TSO(TCP Segmentation Offload):发送大包时,TCP层要切分成MTU大小的段。TSO让网卡硬件来做这个切分。CPU只需要把一个大包丢给网卡,网卡自己切成小段发出去。
- GSO(Generic Segmentation Offload):TSO的软件版本。如果网卡不支持TSO,内核用GSO模拟,但效果差一些。
- LRO(Large Receive Offload):接收端把多个小包合并成大包,减少上层协议栈的处理次数。但注意,LRO会破坏包的边界,对某些应用(比如路由器、防火墙)不友好。
- GRO(Generic Receive Offload):LRO的改进版,更安全,更通用。我建议你优先用GRO。
- RXCSUM / TXCSUM:硬件计算和校验TCP/UDP的Checksum。这个基本是标配了,省掉CPU算校验和的功夫。
核心观点: 对于低延迟交易系统,TSO和GRO通常是开启的。但LRO我建议关掉,因为它会破坏包边界,导致延迟抖动。
怎么查看当前网卡的卸载设置?用 ethtool -k 命令:
# ethtool -k eth0 | grep offload
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off
rx-checksumming: on
tx-checksumming: on
关闭某个卸载功能:
# ethtool -K eth0 gro off
# ethtool -K eth0 tso off
我在项目中遇到过一个问题:某次压测,发现CPU的软中断飙升到100%,但吞吐量上不去。排查了半天,发现是GRO没开,导致大量小包直接冲击CPU。开了GRO之后,CPU利用率直接降了30%。嗯,这个坑我踩过。
4.2 多队列技术——让多个CPU一起干活
早期的网卡只有一个队列。所有数据包都往这一个队列里塞,然后触发一个中断,CPU核心A去处理。如果流量大,CPU核心A就忙死,其他核心闲着。这叫「单队列瓶颈」。
多队列技术,就是网卡内部有多个硬件队列。每个队列可以绑定到不同的CPU核心上。这样,多个核心可以并行处理网络数据。
多队列的两种模式:
- 每个队列一个中断:每个队列独立触发中断,绑定到不同CPU。这是最常见的做法。
- 每个队列一个NAPI轮询线程:用轮询代替中断,减少中断开销。适合超高吞吐场景。
查看网卡支持多少个队列:
# ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 8
TX: 8
Other: 1
Combined: 8
Current hardware settings:
RX: 4
TX: 4
Other: 1
Combined: 4
这里显示最大支持8个队列,当前用了4个。你可以用 ethtool -L 调整队列数量:
# ethtool -L eth0 combined 8
我的建议: 队列数量不要超过CPU物理核心数。比如你有16个物理核心,设16个队列就够了。设太多反而增加调度开销。
4.3 RSS与RPS——数据包该去哪个队列?
有了多队列,下一个问题就是:数据包怎么分配到不同的队列?
RSS(Receive Side Scaling):硬件层面的负载均衡。网卡根据数据包的IP地址、端口号等信息,计算一个哈希值,然后根据哈希值决定放到哪个队列。同一个TCP连接的所有包,哈希值相同,所以会进入同一个队列,保证有序性。
查看RSS的哈希配置:
# ethtool -x eth0
RX flow hash indirection table for eth0 with 8 RX ring(s):
0: 0 1 2 3 4 5 6 7
8: 0 1 2 3 4 5 6 7
...
这个表叫「重定向表」,决定了哈希值到队列的映射。你可以调整它,让某些队列绑定到特定CPU。
RPS(Receive Packet Steering):软件层面的RSS。如果网卡不支持RSS,或者你想更灵活地分配,可以用RPS。RPS在内核里做哈希,然后把包分发到不同的CPU软中断队列。
RPS的配置在 /sys/class/net/eth0/queues/rx-0/rps_cpus 里。比如把队列0绑定到CPU0和CPU1:
# echo "3" > /sys/class/net/eth0/queues/rx-0/rps_cpus
这里3的二进制是11,表示CPU0和CPU1。
注意: RPS会增加CPU之间的缓存一致性开销。因为包在CPU之间传递,会导致L1/L2缓存失效。我个人建议:如果网卡支持RSS,优先用RSS。RPS是备选方案。
4.4 中断调节(Interrupt Coalescing)——别让中断淹死CPU
网卡收到包后,会触发一个中断告诉CPU「有数据来了」。如果每个包都触发中断,高流量下CPU会被中断淹没,啥也干不了。
中断调节就是让网卡攒一批包,再统一触发一次中断。这样减少了中断次数,提升了吞吐量。但代价是增加了延迟——因为包要等一会儿才被处理。
查看当前的中断调节参数:
# ethtool -c eth0
Coalesce parameters for eth0:
Adaptive RX: on TX: on
stats-block-usecs: 0
sample-interval: 0
pkt-rate-low: 0
pkt-rate-high: 0
rx-usecs: 100
rx-frames: 64
rx-usecs-irq: 0
rx-frames-irq: 0
tx-usecs: 100
tx-frames: 64
tx-usecs-irq: 0
tx-frames-irq: 0
关键参数解释:
- rx-usecs:最多等多少微秒,才触发一次中断。值越大,延迟越高,吞吐越高。
- rx-frames:最多攒多少个包,才触发一次中断。值越大,延迟越高。
- Adaptive RX:自适应模式。网卡根据流量动态调整参数。低流量时延迟低,高流量时吞吐高。
对于低延迟交易系统,我建议关闭自适应模式,手动设置极小的值:
# ethtool -C eth0 adaptive-rx off
# ethtool -C eth0 rx-usecs 1
# ethtool -C eth0 rx-frames 1
这样每个包都立即触发中断,延迟最低。但代价是CPU开销会大一些。你需要根据业务场景做权衡。
避坑指南: 我曾经在一个高频交易项目中,把rx-usecs设成了0,以为这样延迟最低。结果发现网卡驱动直接把中断调节禁用了,导致每个包都产生中断,CPU软中断飙升到80%。后来我设成1微秒,效果反而更好。嗯,有时候「0」并不是最优解。
4.5 知识体系总览
下面这张图,我把本章的核心逻辑画出来了。你可以看到数据从网线进来,经过硬件卸载、多队列分发、RSS/RPS分配、中断调节,最后到达CPU的完整路径。
4.6 实战检查清单
最后,给你一份我平时调优网卡时的检查清单。你可以照着做一遍:
| 检查项 | 命令 | 推荐值(低延迟场景) |
|---|---|---|
| 卸载状态 | ethtool -k eth0 |
GRO=on, TSO=on, LRO=off |
| 队列数量 | ethtool -l eth0 |
等于物理核心数 |
| RSS哈希 | ethtool -x eth0 |
均匀分布到所有队列 |
| 中断亲和性 | cat /proc/interrupts |
每个中断绑定到不同CPU |
| 中断调节 | ethtool -c eth0 |
rx-usecs=1, rx-frames=1 |
| 软中断分布 | cat /proc/softirqs |
各CPU负载均衡 |
一个小技巧: 调优完成后,用 perf top 观察软中断(NET_RX)的CPU占比。如果某个核心的软中断占比超过30%,说明队列绑定可能不均匀,需要调整RSS重定向表。
好了,这一章的内容就到这里。网卡调优是个细活,每个参数背后都有 trade-off。你需要在延迟和吞吐之间找到那个平衡点。下一章我们会聊更深入的内容,但今天先把这些基础打牢。
公众号:蓝海资料掘金营,微信deep3321