在现代多核服务器的网络性能调优中, 单队列网卡一核有难多核围观 始终是最大的瓶颈。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 该出场的信号。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部