3. 多核网络数据平面架构
在多核服务器上,网络性能瓶颈往往不在 CPU 主频,而在“数据包如何被分配到各个核心、以及各核心如何把包发出去”。如果所有软中断都落在 CPU0,即使机器有 64 个核,吞吐也会被单核锁死;如果接收队列与发送队列的亲和关系错乱,还会出现跨 NUMA 节点访问、缓存失效和内存带宽浪费。本节围绕一条完整的数据通路,把多核处理器、网卡多队列、RSS、RPS、RFS、XPS 和中断亲和性串起来,说明它们各自解决什么问题、在什么位置生效,以及如何配合使用。
一、从硬件到协议栈:数据包经过哪些“分流点”
先建立整体视图。一块支持多队列的网卡(如 Intel X710、Mellanox ConnectX 系列)通常提供多个 RX/TX 队列对。数据包进入网卡后,硬件根据哈希算法把包分散到不同 RX 队列,这一步是 RSS(Receive Side Scaling)。每个 RX 队列可以绑定一个独立的中断向量,通过 中断亲和性 把该向量的中断固定到某个 CPU 核,于是该核的软中断(NET_RX_SOFTIRQ)处理这个队列的包。若网卡不支持多队列或队列数少于核数,内核可用 RPS(Receive Packet Steering) 在软件层把包再分发到其他核;RFS(Receive Flow Steering) 则进一步按“流”把包送到正在处理该流的应用线程所在核,提升缓存命中。发送方向由 XPS(Transmit Packet Steering) 决定哪个核用哪个 TX 队列,从而让发送中断和回收也落在合适的核上。
二、RSS 与中断亲和性:硬件分流的第一道闸门
RSS 的哈希输入通常是四元组或五元组(源/目的 IP、源/目的端口、协议号),网卡据此计算一个 32 位哈希值,再取低若干位索引到 RX 队列。以 8 队列网卡为例,哈希低 3 位决定队列号,因此同一条 TCP 流始终落在同一队列,保证包序不乱。查看队列数可用 ethtool -l eth0,调整可用 ethtool -L eth0 combined 8。
队列建好后,每个队列对应一个中断号。用 grep eth0 /proc/interrupts 可以看到类似 eth0-TxRx-0 到 eth0-TxRx-7 的条目。把中断绑定到核,就是写 /proc/irq/<N>/smp_affinity。例如把 IRQ 130 绑到 CPU3:
echo 8 > /proc/irq/130/smp_affinity # 位掩码 0x8 = 第3位,对应 CPU3
cat /proc/irq/130/smp_affinity_list # 查看可读形式:3
更常用的做法是用 irqbalance 自动分配,但在追求确定性的场景(如 DPDK 旁路、高频交易、NFV 转发)通常关闭 irqbalance,手工把每个队列的中断固定到不同物理核,并让这些核避开调度器干扰。判断是否生效,可观察 /proc/interrupts 中各中断的计数是否只落在目标核上,以及 mpstat -P ALL 1 中各核的 %soft 是否均衡。
三、RPS 与 RFS:软件层的补充与定向
当网卡队列数少于 CPU 核数,或虚拟化环境中网卡只暴露一个队列时,RSS 无法把负载铺开。RPS 在 netif_receive_skb 之后、进入协议栈之前,用软件计算哈希并把 skb 投递到目标核的 backlog 队列。配置方式:
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus # 允许 CPU0-3 参与
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
RPS 只按哈希分散,不关心应用在哪个核。RFS 则维护一张全局流表(rps_sock_flow_entries)和每队列流表(rps_flow_cnt),记录“某条流最近被哪个核上的应用读取”,后续包直接送到该核,显著提升 CPU 缓存命中率。典型配置:
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
注意 RFS 的流表容量要大于并发流数量,否则会退化为 RPS。判断 RFS 是否有效,可对比开启前后同一压测下的 perf stat -e cache-misses 与软中断分布。
四、XPS:发送方向的对称设计
接收侧理顺后,发送侧若不管,应用线程可能在 CPU5 上运行,却使用与 CPU0 关联的 TX 队列,导致发送完成中断落在 CPU0,引发跨核唤醒和缓存抖动。XPS 让每个 CPU 优先使用指定的 TX 队列:
echo 20 > /sys/class/net/eth0/queues/tx-0/xps_cpus # 0x20 = CPU5
经验做法是让 XPS 的核与处理该流接收的核一致,形成“收—处理—发”同核闭环。在 NUMA 系统上,还要保证网卡所在 PCIe 插槽与所用 CPU 属于同一 NUMA 节点,否则跨节点内存访问会额外增加约 40~80ns 延迟。
五、组合配置示例与验证
假设一台双路服务器,每路 8 核,网卡 8 队列,插在 NUMA0。目标是把 8 个队列分别绑到 NUMA0 的 CPU0~CPU7,并让 XPS 与之一一对应。可参考如下脚本片段:
# 关闭 irqbalance,避免绑定被覆盖
systemctl stop irqbalance
# 队列中断绑定(IRQ 号以实际 /proc/interrupts 为准)
for i in $(seq 0 7); do
irq=$(grep "eth0-TxRx-$i" /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
echo $((1 << i)) > /proc/irq/$irq/smp_affinity
done
# XPS 与接收核对齐
for i in $(seq 0 7); do
echo $((1 << i)) > /sys/class/net/eth0/queues/tx-$i/xps_cpus
done
验证分三步:一是 cat /proc/interrupts | grep eth0,确认各队列中断计数只增长在目标核;二是 mpstat -P ALL 1,观察 %soft 是否均匀铺在 8 个核上;三是用 iperf3 -c <server> -P 8 做多流压测,对比调整前后的总吞吐与单核软中断占比。若某核 %soft 接近 100% 而其他核空闲,说明该队列的中断未正确分散,或 RPS/RFS 未启用。
rps_sock_flow_entries 明显大于并发流数。