2. RSS 基础原理:哈希计算、Toeplitz 算法、RSS 队列映射机制

RSS(Receive Side Scaling)是网卡硬件卸载中最基础、最广泛使用的流分发技术。其核心思想是:利用对称哈希算法将网络流均匀映射到多个接收队列,从而实现多核 CPU 并行处理。本节将深入剖析 RSS 的三个核心技术环节:哈希计算、Toeplitz 算法、队列映射机制。

2.1 RSS 整体工作流程

RSS 的处理流水线分为四个阶段,每个阶段由网卡硬件完成,对软件透明:

  1. 报文解析:网卡解析 L2/L3/L4 头部,提取五元组(源 IP、目的 IP、源端口、目的端口、协议类型)。
  2. 哈希计算:将五元组作为输入,使用 Toeplitz 哈希算法计算出一个 32 位哈希值。
  3. 队列索引映射:取哈希值的低 N 位(N 由队列数决定),通过重定向表(RETA)映射到具体的接收队列编号。
  4. 报文分发:网卡将报文直接 DMA 写入对应队列的 Ring Buffer,触发中断或轮询。
关键指标:RSS 的均匀性取决于哈希算法的雪崩效应和 RETA 表的配置。理想情况下,每条流应唯一映射到一个队列,且各队列负载偏差 < 10%。
RSS 硬件卸载流水线 RSS 硬件卸载流水线 报文解析 L2/L3/L4 头部 Toeplitz 哈希 32-bit 哈希值 RETA 映射 队列索引 DMA 分发 Ring Buffer 说明: • 五元组:源IP(32bit) + 目的IP(32bit) + 源端口(16bit) + 目的端口(16bit) + 协议(8bit) = 104bit 输入 • 哈希值低 N 位决定队列:队列数 = 2^N,例如 8 队列时取低 3 位 • RETA 表可编程:支持将特定哈希值映射到指定队列,实现流量重定向 ← 硬件流水线方向 →

2.2 Toeplitz 哈希算法详解

Toeplitz 哈希是 RSS 规范(Microsoft 定义)中强制使用的哈希算法。与通用哈希不同,Toeplitz 具有硬件友好、低延迟、可预测的特点,非常适合在网卡硬件中实现。

2.2.1 算法数学原理

Toeplitz 哈希本质上是一个线性反馈移位寄存器(LFSR)的变体。其核心是一个 32×N 的 Toeplitz 矩阵,矩阵的每一行是上一行的右移一位。哈希计算过程如下:

  1. 初始化:使用一个 40 字节的随机密钥(RSS Key)填充移位寄存器。
  2. 逐位计算:对于输入数据的每一位,与寄存器对应位进行 AND 运算,结果累加(XOR)。
  3. 输出:最终得到一个 32 位哈希值。
排障提示:DPDK 中可通过 rte_eth_dev_rss_hash_conf_get() 获取当前使用的 RSS Key。如果多台服务器使用不同 Key,同一五元组会映射到不同队列,导致流量分布不一致。

2.2.2 DPDK 中的 Toeplitz 实现

DPDK 提供了软件 Toeplitz 哈希计算函数,用于验证硬件行为或模拟 RSS 分发:

/* DPDK Toeplitz 哈希计算核心代码 */
static inline uint32_t
rte_softrss_be(uint32_t *input_tuple, uint32_t input_len,
               const uint8_t *rss_key)
{
    uint32_t hash = 0;
    uint32_t key_be[10]; /* 40字节 Key 转为大端 */
    int i, j;

    /* 将 Key 转换为大端序 */
    for (i = 0; i < 10; i++)
        key_be[i] = rte_be_to_cpu_32(
            ((uint32_t *)rss_key)[i]);

    for (i = 0; i < (int)input_len; i++) {
        for (j = 0; j < 32; j++) {
            if (input_tuple[i] & (1 << (31 - j))) {
                hash ^= key_be[(i * 32 + j) / 32];
            }
        }
    }
    return hash;
}
性能对比:硬件 Toeplitz 计算延迟约 10-20 纳秒,软件模拟延迟约 200-500 纳秒。生产环境必须依赖硬件卸载。

2.3 RSS 队列映射机制

哈希值到队列的映射并非简单的取模运算,而是通过重定向表(RETA, Redirection Table)实现。RETA 提供了更灵活的映射策略,支持非均匀分布和动态调整。

2.3.1 RETA 表结构

参数 典型值 说明
RETA 大小 128 / 256 / 512 条目 由网卡型号决定,Intel XL710 为 512 条目
条目宽度 6-8 bit 支持最多 64-256 个队列
索引计算 hash_value % RETA_SIZE 取哈希值的低 N 位作为 RETA 索引
条目内容 队列编号 可编程,支持任意映射

2.3.2 映射流程示例

假设网卡配置 8 个接收队列,RETA 大小为 128:

  • 步骤 1:计算哈希值 0x3A7B9C1D
  • 步骤 2:取低 7 位(128=2^7)得到 RETA 索引 0x1D = 29
  • 步骤 3:读取 RETA[29] 条目,假设值为 5
  • 步骤 4:报文分发到队列 5
RETA 队列映射流程 RETA 队列映射流程 哈希值 0x3A7B9C1D 32-bit 取低 7 位 0x1D → 索引 29 RETA[29] 条目值 = 5 队列 5 RX Ring RETA 表内容示例(128 条目): 索引 0: 队列 0 | 索引 1: 队列 1 | ... | 索引 28: 队列 4 | 索引 29: 队列 5 | 索引 30: 队列 6 | ... 映射规则:RETA[hash & (RETA_SIZE-1)] = queue_id DPDK 配置:rte_eth_dev_rss_reta_update(port_id, reta_conf, reta_size)

2.4 RSS 配置与验证

2.4.1 DPDK RSS 配置流程

在 DPDK 应用程序中启用 RSS 需要完成以下步骤:

/* DPDK RSS 配置示例 */
struct rte_eth_rss_conf rss_conf = {
    .rss_key = NULL,          /* NULL 表示使用硬件默认 Key */
    .rss_hf = ETH_RSS_IP | ETH_RSS_TCP | ETH_RSS_UDP,
};

/* 步骤 1: 配置 RSS 哈希类型 */
rte_eth_dev_rss_hash_update(port_id, &rss_conf);

/* 步骤 2: 配置 RETA 表(可选,默认均匀分布) */
struct rte_eth_rss_reta_entry64 reta_conf[2];
/* 初始化 RETA,将 128 个条目均匀映射到 8 个队列 */
for (int i = 0; i < 128; i++) {
    reta_conf[i / 64].mask |= (1ULL << (i % 64));
    reta_conf[i / 64].reta[i % 64] = i % 8;
}
rte_eth_dev_rss_reta_update(port_id, reta_conf, 128);

/* 步骤 3: 启动设备时 RSS 自动生效 */
rte_eth_dev_start(port_id);

2.4.2 故障排查检查清单

检查项 命令/API 预期结果
RSS 是否启用 rte_eth_dev_info_get() → dev_info.reta_size reta_size > 0
哈希类型 rte_eth_dev_rss_hash_conf_get() 包含 IP/TCP/UDP 标志
队列数 rte_eth_dev_configure() 的 nb_rx_q ≥ 2 且 ≤ 硬件支持
RETA 均匀性 读取 RETA 表统计各队列出现次数 各队列次数偏差 < 5%
实际流量分布 rte_eth_stats_get() 查看各队列 q_ipackets 各队列报文数接近
常见故障:如果所有流量都落在队列 0,通常是因为 RSS 未正确启用,或者哈希类型配置错误(例如只配置了 ETH_RSS_IP 但流量是 TCP)。检查 rss_hf 是否包含正确的协议类型。

2.5 多队列与 NUMA 亲和性

在 NUMA 架构下,RSS 队列映射需要结合CPU 亲和性进行优化:

  • 队列绑定:将接收队列绑定到同一 NUMA 节点上的 CPU 核心。
  • 内存本地化:确保 mbuf 池和队列描述符分配在队列所在 NUMA 节点。
  • 跨 NUMA 代价:跨 NUMA 访问会导致 20-30% 的性能损失。
/* NUMA 感知的 RSS 配置 */
uint16_t nb_rx_q = 8;
uint16_t nb_rx_desc = 2048;

for (int q = 0; q < nb_rx_q; q++) {
    int socket_id = rte_lcore_to_socket_id(q);
    /* 在每个 NUMA 节点上创建独立的 mbuf 池 */
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
        "mbuf_pool", nb_mbuf, 256, 0, RTE_MBUF_DEFAULT_BUF_SIZE,
        socket_id);
    /* 配置接收队列,指定 socket_id */
    rte_eth_rx_queue_setup(port_id, q, nb_rx_desc, socket_id,
                           &rx_conf, mbuf_pool);
}
最佳实践:在 DPDK 18.11+ 中,使用 rte_eth_dev_set_rx_queue_stats_mapping() 可以精确监控每个队列的硬件统计,辅助验证 RSS 均匀性。