在万兆、二十五兆网卡已经普及的今天,不少运维人员发现一个奇怪的现象:同样的硬件配置,有的服务器网络吞吐轻松跑满,有的却只能跑到六七成,延迟还抖动得厉害。排查网卡、交换机、驱动都没问题,最后往往发现症结出在NUMA架构的资源分配上。本文就来详细聊聊为什么要把网卡和CPU绑定在同一个NUMA节点,以及具体该怎么操作。

一、为什么NUMA架构会影响网络性能
要理解这个问题,先要从NUMA(Non-Uniform Memory Access,非统一内存访问)架构说起。早期的服务器是对称多处理器(SMP)架构,所有CPU通过同一条总线访问内存,访问延迟是一致的。但随着CPU核心数增加,总线成为瓶颈,于是厂商把CPU和内存划分成多个节点,每个节点内部的CPU访问本节点内存速度很快,跨节点访问则要通过互联总线(Intel的QPI或UPI,AMD的Infinity Fabric),延迟明显增加,带宽也会打折扣。
网卡作为PCIe设备,同样物理上挂载在某个NUMA节点下。当网卡收到数据包时,会产生硬件中断,内核的中断处理程序会在接收中断的CPU上执行,随后数据包会被复制到该CPU可以快速访问的内存区域,上层应用如果恰好在另一个节点上运行,就要跨节点读取这些数据。一次两次没什么感觉,但在每秒上百万包的高吞吐场景下,跨节点访问的累积开销就非常可观了,实测中跨NUMA节点的处理可能带来20%到40%的性能损失,延迟抖动也会加剧。
典型的不良状态是这样的:一块二十五兆网卡插在NUMA 0节点的PCIe插槽上,但中断亲和性设置不当或者irqbalance把中断分散到了NUMA 1的CPU上,应用进程又被调度器随机分配,结果每个数据包都要在两个节点之间来回搬运,QPI总线带宽被白白消耗,cache命中率也大幅下降。
二、如何确认网卡所属的NUMA节点
优化之前必须先摸清现状。Linux内核提供了/sys文件系统来查询PCIe设备的NUMA归属。假设网卡是eth0,先用以下命令找到它的PCIe地址:
# 查看网卡对应的PCIe设备地址 cat /sys/class/net/eth0/device/numa_node # 如果输出是-1,说明BIOS没有正确上报ACPI信息 # 可以通过lspci找到设备总线地址再查 lspci | grep -i ethernet cat /sys/bus/pci/devices/0000:19:00.0/numa_node # 查看当前系统的NUMA拓扑 numactl --hardware lscpu | grep NUMA
其中numa_node的输出就是网卡挂载的节点编号,通常为0或1。numactl --hardware会列出每个节点包含哪些CPU核心以及内存大小,这是后续绑核的依据。如果输出为-1,多半是BIOS中NUMA功能被关闭了,需要进BIOS开启NUMA选项,否则操作系统会把内存当作统一地址空间处理,性能调优无从谈起。
另外一个常见的坑是irqbalance服务。这个守护进程会自动在CPU之间迁移中断以实现负载均衡,出发点是好的,但对于高速网卡来说,它可能把同一个网卡的不同队列中断分散到不同NUMA节点上,破坏了 locality。在高性能网络场景下,通常建议直接关掉它:
systemctl stop irqbalance systemctl disable irqbalance
三、中断亲和性与进程绑核的具体操作
现代高速网卡普遍支持多队列(RSS,Receive Side Scaling),每个队列有独立的中断号。理想的做法是把每个队列的中断绑定到网卡所在NUMA节点的不同CPU核心上,实现中断分布与内存局部性兼顾。先看一下网卡的中断情况:
# 查看网卡队列数量 ls /sys/class/net/eth0/device/msi_irqs/ | wc -l ethtool -l eth0 # 查看中断号与亲和性掩码 grep eth0 /proc/interrupts cat /proc/irq/128/smp_affinity
假设网卡在NUMA 0节点,而该节点包含CPU 0到15,那么可以把队列中断依次绑定到这些核心上。注意smp_affinity写入的是十六进制位掩码,每个bit代表一个CPU:
# 将中断128绑定到CPU 2(掩码 0x4)
echo 4 > /proc/irq/128/smp_affinity
# 将中断129绑定到CPU 3(掩码 0x8)
echo 8 > /proc/irq/129/smp_affinity
# 批量绑定的简单脚本
irqs=$(grep eth0 /proc/interrupts | cut -d: -f1)
cpu=2
for irq in $irqs; do
mask=$(printf '%x' $((1 << cpu)))
echo $mask > /proc/irq/$irq/smp_affinity
cpu=$((cpu+1))
done中断绑定只是第一步,应用进程也要跟着绑定。对网络密集型应用(比如DPDK应用、Nginx、高性能网关),使用numactl或taskset把进程限制在网卡所在节点的CPU上,同时优先分配本节点内存:
# 使用numactl启动进程,绑定到节点0的CPU并使用节点0内存 numactl --cpunodebind=0 --membind=0 ./your_network_app # 对已运行的进程进行绑核 taskset -pc 4-11 $(pidof nginx) # 验证进程的NUMA内存分布 numastat -p $(pidof your_network_app)
对于DPDK这类内核旁路框架,绑核更是标配操作,通常会把网卡队列、工作线程一一对应地绑定到同一NUMA节点的核心上,再配合大页内存分配在本节点,性能才能完全释放。KVM虚拟化场景下则要额外注意vCPU的亲和性设置和vhost线程的绑核,保证虚拟机的收发包路径不跨节点。
四、验证优化效果与常见注意事项
优化完成后需要量化验证。常用的测试组合是用iperf3或qperf打流,配合mpstat观察各核心的软中断(%soft)分布,用numastat观察跨节点内存访问统计。如果优化到位,你会看到软中断集中在网卡所在节点的核心上,跨节点内存流量(numastat中的other_node字段)显著下降,吞吐提升且延迟更稳定。
# 打流测试 iperf3 -c 192.168.0.1 -t 60 -P 8 # 观察软中断分布 mpstat -P ALL 1 # 查看NUMA统计信息 numastat # 确认中断是否集中 cat /proc/interrupts | grep eth0
有几点注意事项需要提醒。第一,不要把中断绑到CPU 0上,内核的很多基础活动都在CPU 0上执行,绑上去反而互相干扰。第二,绑核要与业务负载匹配,如果网卡所在节点的CPU本身已经满载,绑定反而不如跨节点分担负载,这时应该考虑把网卡换插到另一个节点的插槽,或者调整业务部署。第三,超线程环境下,建议优先使用物理核心,把同一个物理核心的两个超线程留给同一队列的中断和处理线程。第四,容器化环境中要注意cpuset的限制,确保容器的CPU配额落在正确的NUMA节点上,否则在宿主机层面的优化会被cgroup配置抵消。
总的来说,NUMA感知的网络优化是一项收益明确、成本不高的工作,尤其在高吞吐、低延迟场景下效果立竿见影。核心思路就一句话:让数据包从网卡中断、内核处理到应用消费的整个链路,尽量留在同一个NUMA节点内。掌握这个原则,再结合本文的命令工具,基本就能把硬件性能榨取到位了。