在现代多核服务器的网络性能调优中, 单队列网卡一核有难多核围观 始终是最大的瓶颈。Linux 内核提供了一整套从硬件多队列到软件流量分发的完整框架——RSS (Receive Side Scaling)、RPS (Receive Packet Steering)、RFS (Receive Flow Steering)、XPS (Transmit Packet Steering)。它们协同工作,把网络数据包高效地分发给目标 CPU,实现线速处理。
然而,这套机制的配置涉及 /sys/class/net、irq affinity、rps_cpus 掩码、rps_flow_cnt、xps_cpus 等多个调优参数,加上网卡Ring Buffer 深度、CPU C-State latency、NUMA 拓扑之间的耦合关系,使得生产环境中的尾延迟抖动问题排查极为困难。本文将从硬件网卡多队列机制出发,深入剖析 RPS/RFS/XPS 的内核实现原理,并给出可落地的工程调优方案。
一、从硬件队列到内核分发:RSS → RPS → XPS 的分层模型
Linux 网络栈的数据接收路径本质上是一个 多对多的流量分配问题:
+----------+ +----------+ +----------+ +----------+
| NIC | | RSS | | RPS | | App |
| HW | --> | Hardware| --> | Soft | --> | Thread |
| Queue | | ToS/ | | Flow | | Core |
| (Rx0-7) | | 5-tuple | | Lookup | | (NAPI) |
+----------+ +----------+ +----------+ +----------+
| |
| Software Fallback |
v v
+------------------------+ +--------------------------+
| RPS (per-queue mask) | | Flow Table (hash->cpu) |
| rps_cpus = "f0" | | rps_dev_flow_table |
+------------------------+ +--------------------------+
RSS (Receive Side Scaling):硬件网卡根据 5 元组 (src IP, dst IP, src port, dst port, protocol) 的哈希值,选择硬件接收队列 (Rx Queue)。每个 Rx Queue 绑定到不同 CPU 中断,实现了硬件层的并行接收。查看方式:
# 查看网卡接收队列与中断号绑定
$ cat /proc/interrupts | grep eth0
128: 1234567 IR-PCI-MSI 1048576-edge eth0-TxRx-0 # CPU0
129: 234567 IR-PCI-MSI 1048577-edge eth0-TxRx-1 # CPU1
130: 345678 IR-PCI-MSI 1048578-edge eth0-TxRx-2 # CPU2
# 查看 RSS Hash Key (40-byte Toeplitz key)
$ ethtool -x eth0
# 查看 RSS Nath 方程式
$ ethtool -n eth0 rx-flow-hash tcp4
RPS (Receive Packet Steering):当网卡没有多队列能力时,或者需要将同一队列的流量二次分发到不同 CPU,RPS 在中断处理函数 enqueue_to_backlog() 中根据数据包哈希值选择目标 CPU,通过 IPI (Inter-Processor Interrupt) 触发目标 CPU 的软中断处理。
XPS (Transmit Packet Steering):发送路径的对等机制,在 dev_queue_xmit() 中根据当前 CPU 选择硬件发送队列 (Tx Queue),避免多 CPU 竞争同一 Tx Queue 导致的 cache-line bouncing。
二、RPS 的内核实现:从 /sys 掩码到 SKB 重定向
RPS 的核心实现在 net/core/dev.c 的 get_rps_cpu() 函数中。它的输入是数据包 (sk_buff),输出是目标 CPU ID。调用链如下:
netif_receive_skb()
└─> netif_receive_skb_internal()
└─> get_rps_cpu(dev, skb, &rps_map)
└─> rps_map[hash & mask] -> next_cpu
└─> enqueue_to_backlog(cpu, skb)
└─> __napi_schedule() on target_cpu
RPS 的配置通过 sysfs 接口完成,映射关系存储在 struct rps_map 结构体中:
// include/linux/netdevice.h
struct rps_map {
unsigned int len;
struct rcu_head rcu;
struct rps_dev_flow flows[0]; // 可变长数组
};
// 内核哈希计算 (简化版)
static unsigned int rps_hash(struct sk_buff *skb) {
struct flow_keys keys;
skb_flow_dissect_flow_keys(skb, &keys, 0);
return jhash_2words(keys.addrs.v4addrs.dst,
keys.ports.dst,
JHASH_INITVAL);
}
实际的 rps_cpus 掩码是十六进制位图,每一位代表一个 CPU。编写脚本批量设置 RPS:
#!/bin/bash
# rps_setup.sh - 为 eth0 的所有 Rx 队列配置 RPS
DEV="eth0"
# 获取 Rx 队列数量
NUM_RX=$(ls /sys/class/net/$DEV/queues/ | grep -c "rx")
# 可用 CPU 掩码 (CPU0-7,掩码 = 0xFF)
CPU_MASK="ff"
# 对每个 Rx 队列的 rps_cpus 和 rps_flow_cnt 进行设置
for i in $(seq 0 $((NUM_RX - 1))); do
QUEUE="/sys/class/net/$DEV/queues/rx-$i"
echo $CPU_MASK > $QUEUE/rps_cpus # 允许分发到所有 CPU
echo 4096 > $QUEUE/rps_flow_cnt # 流表大小 4096
echo 3000 > $QUEUE/rps_flow_len # 活跃流数限制
echo "Configured $QUEUE with mask $CPU_MASK"
done
# XPS: CPU 队列绑定
TX_MASK="ff"
NUM_TX=$(ls /sys/class/net/$DEV/queues/ | grep -c "tx")
for i in $(seq 0 $((NUM_TX - 1))); do
echo $TX_MASK > /sys/class/net/$DEV/queues/tx-$i/xps_cpus
done
三、RFS 流亲核性:避免 CPU Migration 导致的 Cache 抖动
RPS 解决了分发问题,但默认的哈希分发会导致一个五元组在不同时刻被路由到不同 CPU,导致 TCP 控制块 (struct sock) 的 Cache-line 在 CPU 间反复 bounce。
RFS (Receive Flow Steering) 通过维护 flow table 记录每个 flow 上次被分配的 CPU,尽量将同一 flow 的数据包路由回同一 CPU:
// net/core/dev.c
static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
struct rps_dev_flow **rflowp) {
struct rps_map *map = rcu_dereference(dev->rps_map);
struct rps_dev_flow_table *flow_table;
int cpu = -1;
if (map) {
unsigned int hash = rps_hash(skb);
u32 tcpu = map->flows[hash & (map->len - 1)].cpu;
if (cpu_online(tcpu)) {
cpu = tcpu;
} else {
// Fallback: 取 map 中任意 CPU
cpu = map->flows[0].cpu;
}
}
return cpu;
}
// app 设置期望 CPU: /sys/class/net/eth0/flows/...
// SO_INCOMING_CPU -- sk_rcvbind 亲核绑定
生产环境中,Nginx、Envoy 等应用通过 SO_INCOMING_NAPI_ID 和 CPU Affinity 绑核来配合 RFS:
# Nginx worker 亲核绑定
worker_processes auto;
worker_cpu_auto_affinity 1; # 自动绑定
# Envoy: 通过 cpu_affinity 选项
# 查看内核识别的流数
$ cat /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
16384
当 RPS/RFS 协作时,应用的 TCP 连接状态 (sk->sk_rcvbuf、Ring Buffer 消费位置) 始终在同一 CPU 的 L1/L2 Cache 中,可减少 15%-30% 的网络处理延迟。
四、XPS 发送队列对称设计:避免 Tx Soft Lockup
在多 CPU 并发发送场景下(如 Redis Cluster、Memcached),多个 CPU 会竞争同一 Tx Queue 的 dev_queue_xmit() 锁,导致 netdev budget 耗尽和 soft lockup。
XPS 的解决方案是 建立 CPU → Tx Queue 的静态映射:
// 内核 XPS 选择逻辑 (drivers/net/intel/ixgbe/)
static netdev_tx_t ixgbe_xmit_frame_ring(...) {
struct ixgbe_ring *tx_ring;
// 根据当前 CPU 选择 Tx Ring
u16 q = skb->queue_mapping;
if (netif_is_multiqueue(dev)) {
q = cpu_to_txq[smp_processor_id()]; // XPS 映射
}
tx_ring = adapter->tx_ring[q];
}
推荐的 XPS 配置模式是 对称设计:CPU i 的 Tx Queue = CPU i,这样应用绑核后,发送路径完全无锁化:
# XPS 对称配置示例 (8核 8队列网卡)
# CPU0 -> TxQ0, CPU1 -> TxQ1, ...
for i in 0 1 2 3 4 5 6 7; do
MASK=$(printf "%x" $((1 << $i)))
echo $MASK > /sys/class/net/eth0/queues/tx-$i/xps_cps
done
# 或使用脚本计算十六进制掩码
python3 -c << 'EOF'
num_cpus = 8
for q in range(num_cpus):
# mask for CPU q only
mask = hex(1 << q)
print(f"tx-{q}: xps_cpus = {mask}")
EOF
五、NUMA 拓扑亲和性:避免跨 Socket 访问网卡
在双路服务器中,网卡通常通过 PCIe 连接到 CPU0 所在的 NUMA Node。如果流量被 RPS 分发到 CPU5(属于 Node1),会有跨 Socket 的 PCIe 带宽惩罚 (~10%-20% 吞吐损失)。
与网卡同一 NUMA Node 的 CPU 集合可以通过以下命令获取:
# 查看网卡所属 NUMA Node
$ cat /sys/class/net/eth0/device/numa_node
0
# 查看 Node0 的 CPU 列表
$ lscpu | grep "NUMA node0"
NUMA node0 CPU(s): 0-15,32-47
# 计算 Node0 的 CPU 掩码 (CPU0-15 => 0xffff, CPU32-47 => 0xffff0000)
# 合并掩码 = 0xffff | 0xffff0000 = 0xffff0000ffff
编写 NUMA-aware 的 RPS 配置脚本:
#!/bin/bash
DEV="eth0"
# 获取网卡 NUMA Node
NODE=$(cat /sys/class/net/$DEV/device/numa_node)
CPUS=$(lscpu | grep "NUMA node${NODE} CPU" | sed 's/.*: *//')
# 计算 CPU 掩码 -> 十六进制字符串
MASK=$(cset shield --ofprintf="%04x" -c "$CPUS" 2>/dev/null)
# 或使用 python 计算
MASK=$(python3 -c "
import os
def parse_cpu_list(s):
cpus = []
for part in s.split(','):
if '-' in part:
start, end = part.split('-')
cpus.extend(range(int(start), int(end)+1))
else:
cpus.append(int(part))
return cpus
cpus = parse_cpu_list('$CPUS')
mask = sum(1 << c for c in cpus)
print(f'{mask:x}')
")
for q in /sys/class/net/$DEV/queues/rx-*; do
echo $MASK > $q/rps_cpus
echo 4096 > $q/rps_flow_cnt
done
echo "RPS configured with NUMA-aware mask: $MASK"
六、完整工程落地:从单队列网卡到全栈优化的实践
对于 单队列网卡(如 vNIC、云主机通用型),RSS 不可用,必须依赖 RPS 实现多核分发。以下是一个完整的调优流程:
# Step 1: 检查网卡队列数
$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 1
Combined: 8 # 支持 8 组合队列
Current hardware settings:
RX: 0
TX: 0
Other: 1
Combined: 1 # 当前仅 1 队列
# Step 2: 尝试开启多队列
$ ethtool -L eth0 combined 8 # 如果硬件不支持则报错
# 或仅依赖 RPS 软件分发
# Step 3: 安装 irqbalance 并配置亲和性(可选) 1 = 启用
$ systemctl stop irqbalance # 手动绑核时禁用
$ echo 1 > /proc/irq/$(grep eth0-TxRx-0 /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')smp_affinity
# Step 4: RPS/RFS/XPS 完整配置
$ bash rps_setup.sh
# Step 5: Ring Buffer 溢出的排查
# 丢包计数
$ ethtool -S eth0 | grep -E "(rx_dropped|rx_missed_errors|rx_no_dma_resources)"
rx_dropped: 128
# 增加 Ring Buffer
$ ethtool -G eth0 rx 4096 tx 4096
$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 4096
TX: 4096
# Step 6: 关键内核参数调优 (sysctl)
# net.core.netdev_max_backlog -- NAPI backlog 队列大小
# net.core.dev_weight -- NAPI 单次轮询 budget
# net.core.flow_limit_table_len -- RPS flow table 大小
$ cat << EOF >> /etc/sysctl.d/99-network.conf
net.core.netdev_max_backlog = 10000
net.core.dev_weight = 256
net.core.flow_limit_table_len = 8192
net.core.rps_sock_flow_entries = 32768
EOF
# Step 7: 验证流量分发效果
$ mpstat -P ALL 5 # 查看各 CPU %soft 是否均衡
$ perf top -C 0-7 -e 'net:netif_receive_skb' # 软中断处理入口
组合调优之后典型性能提升:
# 调优前:单核瓶颈 ≈ 300K PPS
# 调优后:均衡分发 ≈ 1.2M PPS (8 核心服务器)
# 延迟 P99: 45μs → 18μs
# CPU 利用率: 100% (1核) / 12% (8核) → 18%/core (均衡)
$ iperf3 -c 10.0.0.1 -t 30 -P 8
[SUM] 0.00-30.00 sec 31.2 GBytes 8.92 Gbits/sec 0 sender
七、XDP 与 RPS 协同:用户态数据面加速
XDP (eXpress Data Path) 在网卡驱动层直接运行 eBPF 程序,可以提前决定转发/丢弃/重定向数据包,此时 RPS 的 CPU 选择机制仍然适用——通过 bpf_redirect_map() 重定向到其他 CPU 的 XDP socket:
// xdp_redirect.bpf.c
SEC("xdp")
int xdp_redirect_cpu(struct xdp_md *ctx) {
__u32 cpu = bpf_get_smp_processor_id();
__u32 target = cpu % bpf_map_lookup_elem(&cpus, &cpu);
// 将数据包重定向到目标 CPU 的 XDP socket
return bpf_redirect_map(&cpus_map, target, XDP_PASS);
}
XDP 与 RPS 共同实现了 用户态多路分发,彻底绕过内核网络栈,适合 DPDK 场景下不使用 DPDK 时的轻量级方案。
八、生产环境陷阱:尾延迟与 Soft Lockup
在实际调优中最常见的两个陷阱:
陷阱 1:NAPI budget 耗尽导致流量饥饿
当 RPS 将大量流量导向同一 CPU,该 CPU 的 NAPI 软中断在单次轮询中处理不完,后续流量除非软中断再次被调度,否则会一直 backlog。通过 /proc/sys/net/core/netdev_budget (默认 300) 和 netdev_budget_usecs (默认 2000μs) 控制。调优建议:
net.core.netdev_budget = 600 # 提高单次轮询包数
net.core.netdev_budget_usecs = 8000 # 延长轮询时间
net.core.netdev_max_backlog = 30000 # 扩大 backlog
陷阱 2:RPS 哈希碰撞导致的 flow 分布不均
当流数远小于流表大小时,哈希碰撞会让大量 flow 共享同一 flow entry 的 CPU。使用 ethtool -X 配合 ethtool -n 检查 RSS hash 分布:
$ watch -n 1 'cat /sys/class/net/eth0/queues/rx-/*/rps_flow_len'
# 如果某些 rx queue 的 rps_flow_len 接近 rps_flow_cnt,说明饱和
陷阱 3:LRO/GRO 与 RPS 的交互
Generic Receive Offload (GRO) 会将小数据包聚合成大包,但这会破坏 RPS 的 5-tuple 哈希一致性——合并后的 skb 只保留包头 5-tuple。在极端场景下需要关闭 GRO:
$ ethtool -K eth0 gro off # 关闭 GRO,保持零拷贝小包分发
$ ethtool -K eth0 lro off # 关闭 LRO (必须关闭,LRO 会合并跨 flow)
九、性能剖析工具链
mpstat:检查各 CPU 软中断负载是否均衡
$ mpstat -P ALL 1 | grep -E "(CPU|soft)"
Average: CPU %soft
Average: 0 45.2 ← 过载
Average: 1 12.3
Average: 2 10.1
Average: 3 11.7
perf/bpftrace:追踪 net_rx_action 延迟
$ bpftrace -e 'kprobe:net_rx_action { @start = nsecs; }
kretprobe:net_rx_action /@start/ {
@us = hist((nsecs - @start) / 1000);
delete(@start);
}'
@us:
[4, 8) 28 |@@ |
[8, 16) 1284|@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[16, 32) 598 |@@@@@@@@@@@@@@ |
[32, 64) 147 |@@@@ |
[64, 128) 23 |@ |
[128, 256) 4 | |
sar -n DEV + softnet_stat:检查每个 CPU 的 softnet_stat drops
$ awk '{for(i=2;i<=NF;i++) printf "%5s", $i; print ""}' /proc/net/softnet_stat | head
234 567 890 12
1234 456 789 45 ← Time_squeeze 列 > 0 说明饥饿
12 34 56 78
softnet_stat 各列:total packets, time_squeeze (backlog 预算用完次数), cpu_collision, received_rps。time_squeeze 持续 > 0 意味着该 CPU 需要调大 netdev_budget 或需要 RPS 更分散分发。
十、小结与工程决策树
RPS/RFS/XPS 是生产环境中网络性能优化的基础设施,总结一张调优决策树:
┌─────────────────────────┐
│ 网卡支持多队列 (RSS)? │
└──────────┬──────────────┘
Yes │ No
┌───────────────┴───────────────┐
▼ ▼
┌────────────────┐ ┌────────────────────┐
│ 配置 irqbalance │ │ 启用 RPS 软件分发 │
│ 或手动绑核 │ │ rps_cpus = all │
└────────┬───────┘ └─────────┬──────────┘
▼ ▼
┌────────────────┐ ┌────────────────────┐
│ 启用 XPS 对称 │ │ 启用 RFS 流表亲核 │
│ (cpu → txq) │ │ rps_flow_cnt=4096 │
└────────┬───────┘ └─────────┬──────────┘
▼ ▼
┌────────────────┐ ┌────────────────────┐
│ NUMA 亲和检查 │ │ 应用绑核 (SO_INCOMING_CPU)
│ (同一 node) │ └─────────┬──────────┘
└────────┬───────┘ ▼
▼ ┌────────────────────┐
┌────────────────┐ │ 验证 mpstat 均衡 │
│ Ring Buffer 大小│ │ 验证 P99 延迟下降 │
│ netdev_budget │ └────────────────────┘
└────────────────┘
核心要点:RSS 是硬件能力,RPS 是软件分发,RFS 是亲核保证,XPS 是发送对称。四层协同才能实现从网卡到应用的全路径无锁化,最终达成 接近线速的用户态网络处理。
在云原生环境下,容器通常看到的是 vNIC 且只有 1-2 个队列,此时仅靠 RPS 的多核分发能力,在不依赖 DPDK 的情况下也能将小包 PPS 提升 4-8 倍。当你发现某个 CPU 的 %soft 始终接近 100% 而其他核空闲时,这正是 RPS 该出场的信号。

发表评论 取消回复