4. 网卡调优:RSS、多队列、中断合并与忙轮询(NAPI)
网卡调优这事儿,我做了快十年了。每次跟新人聊,他们总觉得网卡就是个收发数据的管道,没啥好调的。嗯,这种想法在普通业务系统里可能没错,但在自营交易系统里,网卡就是你的第一道防线。
说白了,网卡处理数据包的速度,直接决定了你的交易信号能多快到达应用层。我见过太多团队花大把时间优化策略代码,结果发现瓶颈其实在网卡上——数据包在驱动层排队等了几十微秒,策略写得再快也白搭。
4.1 RSS:让每个CPU核心都有活干
RSS(Receive Side Scaling)是个好东西。它的核心思想很简单:把网卡收到的数据包,分散到多个CPU核心上去处理。
你想想看,如果没有RSS,所有数据包都挤在一个CPU核心上处理。那个核心忙得要死,其他核心在旁边闲着。这不浪费吗?
RSS的工作原理是:网卡硬件根据数据包的哈希值(通常是IP地址和端口号的组合),决定把数据包送到哪个队列。每个队列绑定一个CPU核心。这样,不同连接的数据包就能被不同核心并行处理。
关键点:RSS的哈希算法要选对。我建议用Toeplitz哈希,别用对称哈希。对称哈希虽然能保证双向流量到同一个核心,但在交易场景下,我们更关心的是均匀分布。
配置RSS其实不难。以Intel的网卡为例,用ethtool就能搞定:
# 查看当前RSS配置
ethtool -x eth0
# 设置RSS哈希字段(使用IP和端口)
ethtool -N eth0 rx-flow-hash udp4 sdfn
# 设置RSS队列数(假设有8个核心)
ethtool -L eth0 combined 8
我在项目中遇到过一个问题:RSS配置好了,但流量分布不均匀。查了半天,发现是哈希字段没选对。默认只哈希IP地址,不哈希端口号。结果所有到同一个IP的流量都挤到一个队列里了。加上端口号之后,分布就均匀多了。
4.2 多队列:不只是数量问题
多队列和RSS是孪生兄弟。RSS负责把流量分散到队列,多队列负责提供足够的队列来承载这些流量。
但这里有个坑:队列不是越多越好。每个队列都需要消耗内存(ring buffer),也需要CPU去轮询。队列太多,反而会因为缓存不命中导致性能下降。
| 队列数 | 适用场景 | 注意事项 |
|---|---|---|
| 1-2 | 低吞吐、低延迟 | 适合单核心处理,延迟最低 |
| 等于CPU核心数 | 高吞吐、均衡负载 | 最常用配置,注意IRQ亲和性 |
| 超过CPU核心数 | 极端高吞吐 | 可能引入额外开销,不推荐 |
我个人习惯的做法是:队列数等于CPU物理核心数(不是超线程数)。然后每个队列绑定一个物理核心。这样既保证了并行度,又避免了超线程带来的资源争抢。
小技巧:配置完多队列后,一定要检查IRQ亲和性。用cat /proc/interrupts查看中断分布,确保每个队列的中断确实落在了对应的核心上。我见过有人配了8个队列,结果所有中断都跑到核心0上去了。
4.3 中断合并:延迟与吞吐的博弈
中断合并(Interrupt Coalescing)是个双刃剑。它的原理是:网卡收到数据包后,不立即发中断,而是等一会儿,攒一批包再一起发中断。
这样做的好处是:减少中断次数,降低CPU开销,提高吞吐量。坏处是:增加了延迟。每个包都要等一会儿才能被处理。
在交易系统里,我们通常选择关闭中断合并,或者把合并时间设到极低。为什么呢?因为交易系统对延迟极度敏感,哪怕多等1微秒,都可能错过行情。
# 关闭中断合并(Intel网卡)
ethtool -C eth0 rx-usecs 0 tx-usecs 0
# 或者设置极低的合并时间(1微秒)
ethtool -C eth0 rx-usecs 1 tx-usecs 1
# 查看当前中断合并设置
ethtool -c eth0
我曾经在一个项目中,发现行情数据偶尔会出现几微秒的抖动。排查了很久,最后发现是中断合并的锅。网卡默认的合并时间是125微秒,虽然平均延迟不高,但偶尔会等到125微秒才发中断。关掉之后,抖动就消失了。
注意:关闭中断合并会显著增加CPU开销。如果你的系统CPU已经接近饱和,可能需要权衡一下。我建议先监控CPU使用率,如果低于60%,可以放心关闭中断合并。
4.4 NAPI:忙轮询的艺术
NAPI(New API)是Linux内核提供的一种混合模式:既有中断,又有轮询。它的工作流程是这样的:
- 网卡收到第一个数据包,触发中断
- 中断处理程序启动,但不清除中断状态
- 内核进入轮询模式,持续从网卡读取数据包
- 直到没有数据包了,才退出轮询,重新开启中断
这样做的好处是:在高流量时,用轮询代替中断,避免中断风暴。在低流量时,用中断节省CPU资源。
但NAPI有个问题:轮询的权重(budget)决定了每次轮询能处理多少个包。如果设得太小,处理不完的包要等到下次轮询;设得太大,其他任务可能被饿死。
# 查看NAPI的budget设置
cat /proc/sys/net/core/netdev_budget
# 修改budget(建议值:300-600)
echo 600 > /proc/sys/net/core/netdev_budget
# 修改轮询时间(单位:jiffies)
echo 2 > /proc/sys/net/core/netdev_budget_usecs
我个人习惯把budget设到600,轮询时间设到2个jiffies(大约2毫秒)。这样在行情爆发时,能一次性处理大量数据包,不会因为频繁中断而增加延迟。
核心思路:NAPI的精髓在于「用轮询代替中断」。在交易系统里,我们其实希望网卡一直处于轮询模式,因为行情数据是持续不断的。中断反而成了累赘。
4.5 实战配置清单
说了这么多,我整理一份实战配置清单。这是我经过多次测试后,觉得最适合交易系统的配置:
- RSS:开启,使用Toeplitz哈希,哈希字段包含IP和端口
- 多队列:队列数等于物理核心数,绑定IRQ亲和性
- 中断合并:关闭(rx-usecs=0, tx-usecs=0)
- NAPI budget:600
- NAPI轮询时间:2 jiffies
- Ring buffer大小:4096(根据流量调整)
# 一键配置脚本(Intel网卡示例)
#!/bin/bash
ETH=eth0
CORES=$(grep -c ^processor /proc/cpuinfo)
# 设置队列数
ethtool -L $ETH combined $CORES
# 关闭中断合并
ethtool -C $ETH rx-usecs 0 tx-usecs 0
# 设置ring buffer
ethtool -G $ETH rx 4096 tx 4096
# 设置NAPI
echo 600 > /proc/sys/net/core/netdev_budget
echo 2 > /proc/sys/net/core/netdev_budget_usecs
# 设置RSS哈希
ethtool -N $ETH rx-flow-hash udp4 sdfn
echo "网卡调优完成"
避坑指南:我曾经在配置完网卡后,发现延迟反而变高了。排查了半天,发现是BIOS里把网卡的PCIe链路设成了Gen2而不是Gen3。PCIe带宽不够,导致数据包在DMA传输时排队。所以,别忘了检查硬件层面的配置。
4.6 验证调优效果
配置完了,怎么知道有没有效果?我一般用两个工具:
- perf:查看中断分布和CPU使用率
- tcpdump + 时间戳:抓包分析延迟
# 查看中断分布
cat /proc/interrupts | grep $ETH
# 抓包并打印时间戳(微秒级)
tcpdump -i eth0 -ttt -c 1000
# 使用perf stat查看中断开销
perf stat -e irq_vectors:local_timer_entry -a sleep 1
嗯,到这里,网卡调优的核心内容就讲完了。记住一句话:网卡是交易系统的第一道关卡,调好了,后面的优化才有意义。