荷兰大带宽服务器多队列网卡如何绑定CPU?
今年年初帮一个客户调试位于阿姆斯特丹数据中心的大带宽服务器,到现在我还记得那个场景。客户的业务是面向整个欧洲市场的在线教育平台,每天有大量视频课程需要推送到用户端。服务器配置不低,带宽也买得很足,但运维负责人一直跟我抱怨一件事——每天晚上用户高峰时段,服务器的CPU软中断占用率能飙到百分之七八十,明明CPU整体负载不算太高,网络吞吐量却怎么也上不去,用户端的播放体验时好时坏。
我登录到服务器上看了几分钟,基本就有数了。问题不在硬件不够,而在于网卡和CPU之间的关系没理顺——多队列网卡的硬件能力摆在那里,但它的中断请求几乎全部压在了一两个CPU核心上,其他核心在旁边闲着看热闹。这就是典型的“网卡没绑好CPU”导致的性能瓶颈。后来花了不到半天时间做完多队列网卡绑核优化,软中断占用率从百分之七十降到了百分之三十以下,峰值吞吐量翻了接近一倍。
这件事让我意识到,很多朋友租了大带宽服务器之后只关注带宽够不够大、CPU够不够多,却忽略了网络数据包在服务器内部是怎么流转的这个关键问题。这篇文章就把荷兰大带宽服务器多队列网卡绑定CPU这件事从头到尾说清楚,希望能帮到正在被类似问题困扰的朋友。
先弄明白多队列网卡是什么,以及为什么要绑CPU
在聊具体怎么操作之前,先花几分钟理解一下底层原理。说几个技术名词可能有点枯燥,我用一个类比来帮忙理解。
传统的单队列网卡就像一家超市只有一个收银台。不管有多少顾客,都只能在这个收银台前面排队。这个收银台对应的是一个CPU核心,所有网络数据包的处理压力都集中在它身上。当流量上来的时候,这个核心忙得不可开交,其他核心在旁边闲着——这就是软中断飙升的根本原因。
多队列网卡就不同了。它内部有多个接收队列和发送队列,每个队列都有自己的中断号。网卡收到数据包之后,会根据包的源IP、目的IP、源端口、目的端口和协议类型这些信息,通过哈希算法把不同的数据流分配到不同的队列上去。每个队列的中断可以绑定到不同的CPU核心,相当于超市开了多个收银台,顾客可以分流到不同的通道去结账。
硬件层面的这一机制,在技术圈里叫RSS。它的核心价值就在于让多个CPU核心能够并行处理网络数据包,从根本上解决单核瓶颈的问题-。
为什么要强调“绑定”呢?因为即便网卡有多队列能力,如果操作系统的中断分配策略把所有的中断请求都丢给了同一个CPU,那多队列的优势就完全发挥不出来。荷兰机房的服务器大多数是多路CPU架构,NUMA节点之间的内存访问延迟差异不小。如果不做绑核优化,数据包可能在一个NUMA节点上被网卡接收,却被跨节点送到另一个NUMA节点的CPU去处理,这个过程会引入额外的内存访问延迟,性能反而比不优化还差。
所以多队列网卡绑核的核心目标其实就四个字:各就各位。每个网卡队列的中断固定在一个CPU核心上,每个核心专注处理自己队列里的数据包,避免了中断抢占和缓存抖动,系统的网络处理能力才能真正释放出来。
第一步:确认网卡是否支持多队列
拿到一台荷兰机房的大带宽服务器之后,第一件事就是确认网卡到底能不能用多队列。这一步不难,用几条命令就能看明白。
先用lspci -vvv | grep -A50 "Ethernet controller" | grep -E "Capabilities|Ethernet controller"这条命令查看网卡的详细信息。重点关注输出里面有没有“MSI-X: Enable+ Count=xx”这一行。如果有,而且Count的数值大于1,就说明这张网卡支持多队列。MSI-X是PCIe设备的多中断机制,多队列网卡必须依赖它才能给每个队列分配独立的中断号。
接下来用ethtool -l eth0查看网卡当前的队列配置。把eth0换成你服务器上的实际网卡名称。输出会显示两段内容——Pre-set maximums这一行告诉您网卡硬件最多支持多少个队列,Current hardware settings这一行显示当前实际启用了多少个队列。如果“Combined”这一项的最大值大于1,但当前值等于1,就说明网卡支持多队列却没有启用,这是很多服务器默认配置里常见的情况。荷兰这边不少IDC提供的服务器,网卡驱动默认只开了一个队列,需要手动打开。
还有一个快速检查的方法——直接查看队列目录。执行ls /sys/class/net/eth0/queues/,如果看到rx-0、rx-1、tx-0、tx-1等以数字递增命名的目录,就说明多队列已经启用了。
第二步:设置网卡队列数量
确认网卡支持多队列之后,接下来要做的就是设置合理的队列数量。这里面有一个关键原则:队列数量一般设置成等于物理CPU核心数,而不是越多越好。
为什么这么说呢?多队列网卡的设计初衷就是为了让每个队列匹配一个CPU核心,每个核心专门处理一个队列里的数据包。如果队列数超过了CPU核心数,多出来的队列因为没有对应的CPU去处理它的中断,反而会造成资源的浪费和调度的混乱。相反,如果队列数少于CPU核心数,那就意味着有些核心闲着,没能充分参与网络数据的并行处理。所以理想的比例是一比一。
设置队列数量用ethtool -L命令。如果网卡支持收发队列共用的模式,执行sudo ethtool -L eth0 combined 8,其中8替换成你服务器的CPU核心数。有些网卡允许单独设置接收队列和发送队列的数量,那就用sudo ethtool -L eth0 rx 8 tx 8来分别指定。设置完成后再用ethtool -l eth0验证一下,确保配置已经生效。
执行完之后再看一眼/sys/class/net/eth0/queues/目录,里面应该会出现对应数量的rx-N和tx-N子目录。这一步做完,网卡层面的硬件多队列就准备好了。
第三步:把每个队列的中断绑定到指定CPU
这是整个优化过程中最核心的一步。前面已经把队列打开了,但还需要告诉操作系统,哪个队列的中断由哪个CPU来处理。
先关掉irqbalance服务。这个服务是Linux系统里负责自动均衡中断的守护进程,平时挺好的,但在我们做手动绑核优化的时候,它会跟手动配置“打架”,频繁重置你设置的中断亲和性,让配置失效。所以需要先停掉它,并取消开机自启动:
sudo systemctl stop irqbalance
sudo systemctl disable irqbalance
接下来找出网卡每个队列对应的中断号。执行cat /proc/interrupts | grep eth0,会看到类似这样的输出——每个队列对应一个中断号,格式通常是网卡名称加上rx、tx之类的标识。
有了中断号之后,就可以把每个中断绑定到指定的CPU核心上了。绑定的配置写入/proc/irq/<中断号>/smp_affinity_list文件。比如想把中断号145绑定到CPU0,就执行echo 0 > /proc/irq/145/smp_affinity_list。这里有个小技巧:尽量让队列编号和CPU编号一一对应,rx-0绑CPU0,rx-1绑CPU1,以此类推。这种对应关系对CPU缓存友好,数据包从网卡到CPU之间的路径最短。
绑定完之后再次查看cat /proc/interrupts,确认每个队列的中断处理CPU列已经变成了你设置的那些核心编号。
第四步:配置软中断分发
硬中断绑好之后,数据包从网卡DMA到内存、触发中断这个阶段已经做到了多核并行。但还有一个环节不能忽视——硬中断处理完之后,真正的协议栈处理是在软中断上下文里完成的,这部分仍然可能集中在某些CPU上。
这时候就需要RPS出马了。RPS的全称是Receive Packet Steering,它可以在软中断层面把一个接收队列的数据包再次分发到多个CPU核心上去处理。注意,RPS不是用来替代RSS的,而是在RSS基础上做的补充。当硬件队列数少于CPU核心数的时候,RPS能把软中断负载更均匀地分散开。
配置RPS的方法是给每个接收队列设置一个CPU掩码。假设服务器有4个CPU核心,想让队列rx-0的数据包被CPU0到CPU3共同处理,就用十六进制掩码f(二进制1111)来配置:
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
以此类推,对每个rx-N队列都做同样的配置。
如果再进一步追求更好的性能,可以考虑开启RFS。RFS是RPS的一个增强版,它不光把软中断分散到多个CPU,还会尽量让软中断处理和应用层收包在同一个CPU上完成,从而充分利用CPU的L1/L2/L3缓存,减少跨核缓存同步带来的性能损耗。
配置RFS需要先设置全局流表容量。推荐值是CPU核心数乘以4096,比如4核就设16384:
echo 16384 > /proc/sys/net/core/rps_sock_flow_entries
然后给每个接收队列设置流表容量:
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
最后开启RFS的全局开关:
echo 1 > /proc/sys/net/core/netdev_rfs_threshold
第五步:协议层面的辅助优化
队列和CPU的关系理顺之后,还有几个协议层面的参数值得顺手调整一下,它们虽然不直接涉及绑核,但能和绑核后的配置形成良好的协同。
TCP拥塞控制算法这块,传统的cubic算法在面对高带宽、高延迟的网络环境时表现不太理想。BBR算法在这方面要优秀得多,它能更准确地估计可用带宽,在高吞吐场景下表现稳定。在荷兰这种连接欧洲各国、跨境流量占比不小的场景里,开启BBR往往能带来立竿见影的改善。开启方式很简单,修改系统配置并重启即可。
GRO/GSO这些硬件卸载功能也要检查一下是否启用。GRO把多个小包合并成一个大包再交给协议栈,能显著减少协议栈处理的次数;GSO则是让网卡硬件分担分片和分段的工作。这些功能一般默认是开启的,但有些发行版出于兼容性考虑可能把它们关掉了。用ethtool -k eth0可以查看各项卸载功能的启用状态。
还有一个参数值得留意——netdev_budget,它控制了内核软中断在单次调度中最多处理的包数量。默认值在中小流量下够用,但在大带宽服务器的场景里,适当提高这个值可以减少软中断的调度频率,提高CPU在处理网络包时的亲和性。但调得太高也有风险,可能导致其他任务得不到及时的CPU时间,建议在测试环境里小幅度逐步增加,找到最适合业务负载的那个平衡点。
第六步:NUMA对齐,让数据“就近”访问
荷兰的大带宽服务器很多都是双路甚至四路CPU的配置,NUMA架构在这里面扮演了重要角色。简单来说,在NUMA架构下,每个CPU都有自己“本地”的内存插槽。CPU访问本地内存的速度很快,访问远端CPU的内存就要慢不少。
网卡通过PCIe插槽连接到主板上,而不同的PCIe插槽归属于不同的NUMA节点。如果网卡物理上插在节点0的插槽上,但它的中断被绑定到了节点1的CPU,那每个数据包都要跨NUMA节点访问内存,性能损失可不小。
绑定之前先用cat /sys/class/net/eth0/device/numa_node看看网卡挂在哪个NUMA节点上。然后用lscpu或者numactl --hardware查看每个节点有哪些CPU核心。绑定中断的时候,尽量把网卡的中断都绑在同一个NUMA节点的CPU上。如果业务进程本身也跑在同一个NUMA节点,那就更完美了——整个数据路径从网卡到内存到CPU全在本地,延迟最低、吞吐最高。
Netflix的工程团队在这方面有过很深入的实践。他们通过对NUMA架构的精细化调优,在一台服务器上把视频传输性能从105Gb/s提升到了191Gb/s,NUMA带宽利用率从百分之四十降到了百分之十三-。这个案例很有说服力——NUMA对齐带来的性能提升,往往比单纯增加核心数要有效得多。
荷兰本地的一个实操案例
回到文章开头提到的那家在线教育平台。客户服务器在阿姆斯特丹Equinix数据中心,网卡是一块英特尔XL710 40GbE,服务器配置了双路英特尔金牌处理器,共32个物理核心。业务高峰时,视频流的并发连接数超过两万条,但服务器的实际吞吐量一直在12Gb/s到15Gb/s之间徘徊,远没到网卡的理论上限。
我当时做的排查和优化步骤是这样的。
先用cat /proc/interrupts | grep eth0看了一眼中断分布,发现40个网卡队列的中断几乎全部集中在CPU0和CPU1两个核心上,其他30个核心几乎没收到任何网络中断。难怪吞吐上不去——入口就堵了,后面再有劲也使不出来。
然后检查irqbalance服务,果然是开启状态。它在不停地把中断在各个核心之间搬来搬去,完全打乱了我们想做的手工绑定。
第一步先把irqbalance停掉并禁用自启动。然后用ethtool -L eth0 combined 16把队列数从40降到了16。因为这台服务器的两个CPU各有16个物理核心,跨NUMA节点绑核会导致性能下降,所以我决定只用其中一个NUMA节点的16个核心专门处理网络流量,另一个NUMA节点的核心专注业务计算。这样可以避免跨节点访问内存带来的延迟开销。
接下来按顺序把rx-0到rx-15的中断依次绑定到CPU0到CPU15上。RPS掩码设置成对应的CPU位图,让每个队列的软中断和硬中断在同一个CPU上处理。XPS的配置也同步跟上,确保发送方向的数据包从同样的CPU发出。
所有配置完成之后重启网络服务和服务器。再压测的时候,服务器的网络吞吐量稳定在了36Gb/s以上,软中断占用率从之前的百分之七十降到了百分之二十以下。用户高峰时段的视频卡顿投诉量在接下来的一周里下降了百分之六十。
这个案例给我的体会是:多队列网卡绑核这件事本身不复杂,但它的效果好不好,取决于你对服务器硬件拓扑的理解有多深。不了解NUMA就去盲目绑核,有时候还不如不绑。
验证优化效果的方法
配置做完之后,怎么判断效果好不好?我习惯用这么几个方法来验证。
看中断分布是最直接的。执行cat /proc/interrupts | grep eth0,观察每个队列的中断计数是不是在各自的绑定CPU上均匀增长。如果发现某个中断长时间没有增长,可能队列本身没有收到足够的流量;如果某个中断的计数明显高于其他的,说明RSS的哈希算法可能导致了流量偏斜,可以考虑调整RETA表来优化。
看软中断分布也很重要。执行cat /proc/softirqs | grep NET_RX,观察NET_RX这一列在各CPU上的数值。如果分布相对均匀,说明RPS配置起到了作用;如果发现某些CPU的数值明显偏低,检查一下rps_cpus的掩码设置有没有覆盖到它们。
吞吐量测试也不能少。用iperf3从外部客户端向服务器打流,对比优化前后的吞吐量和CPU使用率。大带宽服务器的优势最终要体现在吞吐数据上,测试结果是最有说服力的。
最后
荷兰大带宽服务器多队列网卡绑定CPU这件事,说到底是在做一个精密的调度工作。把网络数据流从网卡到队列、从队列到中断、从中断到CPU、从CPU到应用进程的整条路径理顺,让每个环节都各司其职、互不干扰。
具体操作可以归纳为这样几个关键步骤:确认网卡支持多队列并合理设置队列数量;关闭irqbalance服务,把每个队列的中断手动绑定到对应CPU;配合RPS和RFS配置让软中断处理更均衡;开启XPS优化发送方向的数据路径;注意NUMA对齐,让网卡、内存和CPU在同一个节点上协同工作;最后别忘了检查TCP协议栈的相关参数,让上层协议也能配合底层的绑核优化。
网络性能优化不是一个“一把梭”的过程,它需要根据服务器的硬件配置、业务类型和流量特征做精细化的调整。希望这篇文章里提到的思路和案例,能给正在使用或计划使用荷兰大带宽服务器的朋友一些实实在在的参考。在阿姆斯特丹机房里把那台服务器的软中断从百分之七十降到百分之二十的那个下午,客户的一句话让我印象很深——“原来这台机器本来的性能是这样的”。我想,这就是做优化的意义所在。


