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 队列,从而让发送中断和回收也落在合适的核上。

多核网络数据平面:接收与发送的分流链路 多队列网卡 RX Queue 0..N TX Queue 0..N 硬件哈希分流 RSS 硬件层分流 按五元组哈希 决定包进哪个 RX 队列 中断亲和性 IRQ → CPU 绑定 /proc/irq/N/smp_affinity 决定软中断在哪个核跑 CPU 核心 CPU0 / CPU1 / ... NET_RX_SOFTIRQ 协议栈处理 RPS 软件层分流 rps_cpus 位图 网卡队列不足时补位 RFS 按流定向 rps_flow_cnt 送到应用线程所在核 XPS 发送队列选择 xps_cpus 位图 核 → TX 队列映射 发送中断回收 TX completion 与 XPS 同核更优 减少跨核唤醒 接收路径 RSS → 中断亲和性 → RPS → RFS 硬件优先,软件补位 发送路径 XPS 选 TX 队列 中断回收同核 避免跨 NUMA 抖动

二、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 未启用。

调优顺序建议:先确认网卡队列数与 NUMA 位置,再绑中断亲和性,然后按需开启 RPS/RFS,最后配置 XPS。每一步只改一个变量并复测,避免多个参数同时变动导致无法归因。
RPS/RFS 会引入额外的跨核 IPI 和队列锁开销,在网卡队列充足时开启反而可能降低性能。RFS 流表过小会导致频繁换核,务必让 rps_sock_flow_entries 明显大于并发流数。