Linux 内核网络多队列全栈调度——从 RSS 硬件分流到 XPS 发射路径的工程实践
引言:为什么单队列网卡已经是过去时
2024年数据中心网卡已经全面进入200G/400G时代,单核处理6.4Mpps早已成为瓶颈。现代NIC普遍提供128+硬件队列,但如果配置不当,90%的流量可能卡在单一RX队列上。本文将从硬件RSS哈希、内核NAPI调度、RPS/XPS软件分流、irq_affinity手工绑核,一直到驱动层ndo_select_queue的完整路径,给出一个可落地的多队列网络性能工程方案。
一、硬件 RSS 分流:决定包去哪个队列的第一战场
1.1 RSS 的工作原理
Receive Side Scaling在网卡硬件层面完成:网卡根据包的header计算哈希,通过Redirect Table选择目标RX队列。
/* 典型的 RSS Key (Intel driver 默认40-byte Toeplitz key) */
static const u8 rss_key_default[RSS_HASH_KEY_SIZE] = {
0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,
0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,
0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,
0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,
};
RSS支持的哈希输入组合: - Toeplitz哈希:最常用,可配置对称性(symmetric-hash使五元组正反流进同一队列) - 基于字段:src_ip+dst_ip+src_port+l4proto(UDP/TCP),VXLAN内层报文支持需要额外配置
1.2 查看与配置 RSS
# 查看当前队列数
ethtool -l enp1s0
Channel parameters for enp1s0:
Pre-set maximums:
RX: 0
TX: 0
Other: 1
Combined: 32
Current hardware settings:
RX: 0
TX: 0
Other: 1
Combined: 8
# 设置 Combined 队列数
ethtool -L enp1s0 combined 16
# 查看 RSS 哈希 key
ethtool -x enp1s0
# 查看 RSS 重定向表
ethtool -x enp1s0 | head -40
# 使用 ntuple 规则精确分流: 将目标端口 443 的流量强制分配到 queue 2
ethtool -N enp1s0 flow-type tcp4 dst-port 443 action 2
1.3 Flow Director 与 ntuple 精确分流
对于需要精确控制某条流走哪个队列的场景,ntuple和FDir是关键:
# 添加NTuple规则:src_ip=10.0.0.5 的流量走 queue 3
ethtool -N enp1s0 flow-type ip4 src-ip 10.0.0.5 action 3
# 查看已有规则数(通常受限于硬件TCAM,如Intel X710支持8K条)
ethtool -n enp1s0
# 删除规则
ethtool -N enp1s0 delete 126
关键工程取舍:RSS(哈希分流)弹性好但可能不平衡(incast场景哈希碰撞),ntuple(精确分流)可控但TCAM有限。实战中常用混合方案:用RSS做粗分流,对少数大流做ntuple精确控制。
1.4 virtio-net 多队列配置
在虚拟化场景:
<!-- QEMU virtio-net 多队列配置 -->
<interface type='bridge'>
<model type='virtio'/>
<driver queues='8'/>
</interface>
# 在 Guest OS 内查看队列数
ethtool -l eth0
# 若配置8队列只显示1个,需确保:
# 1. QEMU启动参数带 mq=on
# 2. vhost=on 且对应线程数
# 3. Guest内核 CONFIG_VIRTIO_NET=m
二、内核 NAPI 调度:每个队列一个软中断世界
2.1 NAPI 与 per-queue softirq 的关系
每个RX队列在初始化时注册一个 napi_struct,对应一个独立的NET_RX softirq处理。这意味着多队列同时触发软中断完全并行。
关键数据结构关系:
struct napi_struct {
struct list_head poll_list; /* 挂在 per_cpu的 softnet_data.poll_list */
unsigned long state; /* NAPI_STATE_SCHED */
int weight; /* 默认64,预算上限 */
int (*poll)(struct napi_struct *, int); /* 驱动的poll函数 */
};
struct softnet_data {
struct list_head poll_list; /* 本CPU待处理的napi队列 */
struct napi_struct napi; /* backlog 处理的 napi */
...
} ____cacheline_aligned_in_smp;
每CPU一个 softnet_data,保证了不同队列的poll不会在同一个CPU上串行。
2.2 软中断亲和性:ksoftirqd 绑定
# 查看当前各队列的软中断处理CPU
cat /proc/interrupts | grep enp
195: 123456 0 0 0 0 0 0 0 IR-PCI-MSI 524288-edge enp1s0-TxRx-0
196: 0 234567 0 0 0 0 0 0 IR-PCI-MSI 524289-edge enp1s0-TxRx-1
197: 0 0 345678 0 0 0 0 0 IR-PCI-MSI 524290-edge enp1s0-TxRx-2
三、RPS:软件层面的二级分流
当网卡硬件队列数不足(或没有硬件RSS),用RPS模拟多队列:
# 对 enp1s0 的 RX queue 0,用 rps_cpus 指定哪些 CPU 处理
echo f > /sys/class/net/enp1s0/queues/rx-0/rps_cps
# rps_flow_cnt:flow cache 每条流记录的预算,避免cache抖动
echo 4096 > /sys/class/net/enp1s0/queues/rx-0/rps_flow_cnt
# 启用 RPS 还需要打开 sysctl
sysctl net.core.rps_sock_flow_entries=32768
rps_cpus 掩码(十六进制,CPU掩码格式与irq_affinity_hint相同但位含义不同):
- f = 二进制1111 = CPU 0-3
- ff = CPU 0-7
工程注意:RPS在已有RSS的场景下不需要额外启用。RPS的主要场景: 1. 低端网卡只有单队列 2. container网络栈中 veth pair 的流量需要软件分流 3. 链路聚合(bonding)场景
四、XPS:发射队列的精准映射
4.1 XPS 解决什么问题
默认情况下,内核TX选queue依赖 skb->queue_mapping(由 ndo_select_queue 设置)。XPS则将当前CPU直接映射到指定TX队列,使得 "发射这个包的CPU" 与该CPU处理对应RX队列绑在同一核心上,最大化 cache 局部性。
# CPU 0-3 绑定到 tx-0 ... tx-3
echo f > /sys/class/net/enp1s0/queues/tx-0/xps_cpus
echo f0 > /sys/class/net/enp1s0/queues/tx-1/xps_cpus
echo f00 > /sys/class/net/enp1s0/queues/tx-2/xps_cpus
echo f000 > /sys/class/net/enp1s0/queues/tx-3/xps_cpus
# 或者使用宏自动计算(如 onboard 脚本)
mask=$(($(echo $((0x1 << cpu)))))
4.2 TX 路径中的 ndo_select_queue
驱动通过 ndo_select_queue API 覆盖默认选queue行为:
/* drivers/net/ethernet/intel/ice/ice_txrx.c 简化版 */
static u16 ice_select_queue(struct net_device *netdev, struct sk_buff *skb,
struct net_device *sb_dev)
{
/* 对于硬件队列数 > 1 的场景,使用 skb->queue_mapping 即可 */
if (netdev->real_num_tx_queues > 1) {
return netif_get_tx_queue(netdev, skb->queue_mapping,
smp_processor_id())->queue_index;
}
return 0;
}
五、irq_affinity:绑核的艺术
5.1 理论基础
多队列性能优化的核心假设:同一队列的 RX 与 TX 处理在同一 NUMA 节点内的 CPU 上,这样数据在 cache 中流转,无需跨 NUMA 访问 NIC 的 DMA 内存。
5.2 自动化脚本
#!/bin/bash
# set_irq_affinity.sh - 自动为每个队列绑定对应CPU
# Usage: ./set_irq_affinity.sh enp1s0
IFACE=$1
NUM_QUEUES=$(ls /sys/class/net/$IFACE/queues/ | grep -c rx)
for ((i=0; i<NUM_QUEUES; i++));
# 获取对应中断号
IRQ=$(cat /proc/interrupts | grep "$IFACE-TxRx-$i" | awk -F: '{print $1}')
IRQ=$(echo $IRQ | tr -d ' ')
# 绑定到 CPU i(确保在同一NUMA节点)
MASK=$(printf "%x" $((1 << i)))
echo $MASK > /proc/irq/$IRQ/smp_affinity_list 2>/dev/null || \
echo $MASK > /proc/irq/$IRQ/smp_affinity
echo "Queue $i IRQ $IRQ -> CPU $i (mask $MASK)"
done
5.3 irqbalance 的工程取舍
系统服务 irqbalance 会动态调整中断亲和性,可能与手工绑核冲突:
# 对于 DPDK-style 确定性绑核场景,关闭 irqbalance
systemctl disable --now irqbalance
# 或使用 irqbanace-list 排除特定网卡
# /etc/sysconfig/irqbalance:
# IRQBALANCE_ARGS="--banirq=195 --banirq=196 --banirq=197 --banirq=198"
六、NUMA 局部性工程
6.1 NUMA-aware 队列分配
# 查看网卡通的 NUMA 节点
cat /sys/class/net/enp1s0/device/numa_node
# 输出 0 -- 插在 NUMA node 0 的 PCIe 插槽上
# 确保绑核的目标CPU也在同一 NUMA node
lscpu | grep NUMA
NUMA node0 CPU(s): 0-15,32-47
NUMA node1 CPU(s): 16-31,48-63
6.2 性能影响实测数据
跨 NUMA 访问 NIC DMA 内存的代价:
| 场景 | 延迟影响 | 吞吐影响 |
|---|---|---|
| 同 NUMA 绑核 | baseline | 100% |
| 跨 NUMA 绑核 | +30-50ns | -15~25% |
| CPU 与 NIC 不在同一 socket | +80-150ns | -30~40% |
七、容器网络多队列:virtio-net 与 veth pair
7.1 Pod 多队列 virtio-net
在 Kubernetes 场景,virtio-net 的多队列配置通过 Multus + CNI 完成:
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: virtio-net-8q
spec:
config: |
{
"cniVersion": "0.3.1",
"type": "bridge",
"bridge": "kbr0",
"ipam": {"type": "host-local"},
"metadata": {
"annotations": {
"k8s.v1.cni.cncf.io/networks": "virtio-net",
"k8s.v1.cni.cncf.io/networkStatus": ""
}
}
}
7.2 veth pair 流量分发
veth 设备天然单队列,如果 Pod 出口流量大,要用 RPS:
# 查看 veth 设备(从 host 端看到的 Pod 网卡)
ip link show type veth
# 在 veth 上启用 RPS 让流量分散到多个 CPU
echo ff > /sys class/net/veth1234/queues/rx-0/rps_cpus
八、性能排错工具箱
8.1 关键指标采集
# 1. 各队列包数/字节统计
ethtool -S enp1s0 | grep queue_
# 2. 软中断在CPU上的分布
watch -d -n 1 'cat /proc/softirqs | grep NET'
# 3. 使用 dropwatch 定位丢包位置
dropwatch -l kas
# 4. BPF 工具 softirqs 分析处理耗时
BPF_BUILD_ID_DIR=/usr/lib64/bpf softirqs 1 10
# 5. 查看 NAPI 被 throttle 的频次
cat /sys/kernel/debug/tracing/trace | grep napi
8.2 用 BPFtrace 监控队列选择行为
kprobe:ndo_select_queue {
printf("dev=%s queue_mapping=%d cpu=%d\n",
args[0]->name, args[1]->queue_mapping, cpu);
}
九、可落地的调优检查清单
| 检查项 | 预期状态 | 工具 |
|---|---|---|
| 网卡队列数 = NIC 最大支持或 = CPU 核数 | 一致 | ethtool -l |
| irq_affinity 绑核在同 NUMA 节点 | 无跨 NUMA | /proc/irq/*/smp_affinity |
| RPS 仅在需要时启用 | RSS 够用自己关 | sysctl net.core.default_qdisc |
| XPS 与 RX-queue 配对 | 发射与接收同一CPU | /sys/class/net/*/queues/tx-*/xps_cpus |
| irqbalance 不干扰绑核 | 关闭或排除关键中断 | systemctl status irqbalance |
| 单队列流量倾斜度 < 20% | 均衡 | ethtool -S |
十、总结
Linux 网络多队列调度是贯穿硬件 RSS → 内核 NAPI → 软件 RPS/XPS → 中断亲和 → NUMA 局部性的系统工程。正确的配置在 400G 网络环境下能带来 30%-50% 的吞吐提升以及显著的延迟抖动改善。
核心认知只有三条: 1. 先硬件后软件:优先利用 RSS,其次 RPS 2. 先 NUMA 后队列:确保 CPU-NIC 在同一物理 socket 3. 监控驱动调优:队列分布倾斜是性能隐形杀手
参考文档
- Linux Documentation:
networking/scaling.rst - Intel ICE Datasheet: Multiple Queue Support
- Kernel Source:
include/linux/netdevice.h/net/core/dev.c - BPF & XDP Reference:
Documentation/networking/xdp-rx-metadata.rst

发表评论 取消回复