4. RSS 配置与初始化:RETA 表与哈希 Key 设置

RSS(Receive Side Scaling)的生效依赖于两个核心数据结构:RETA(Redirection Table,重定向表)和哈希 Key。RETA 决定了哈希值到接收队列的映射关系,而哈希 Key 则是 Toeplitz 哈希算法的种子参数。DPDK 通过 rte_eth_dev_rss_reta_update 和 rte_eth_dev_rss_hash_update 两个 API 完成配置。本章将深入解析这两个函数的参数、使用步骤以及底层硬件行为。

核心要点: RETA 表是“查表映射”,哈希 Key 是“算法种子”。两者共同决定了数据流如何被分发到不同的 CPU 核心。

4.1 RETA 表结构与更新机制

RETA 表是一个由硬件维护的查找表,其索引由哈希值的低 N 位决定。每个表项存储一个目标队列 ID。DPDK 使用 rte_eth_rss_reta_entry64 结构体来描述 RETA 表项。

4.1.1 数据结构定义

struct rte_eth_rss_reta_entry64 {
    uint64_t mask;   /* 位掩码,指示哪些表项需要更新 */
    uint16_t reta[64]; /* 64 个 RETA 表项,每个表项对应一个队列 ID */
};

该结构体以 64 个表项为一组进行管理。通过 mask 字段,可以只更新组内的部分表项,避免全量写入。

4.1.2 rte_eth_dev_rss_reta_update 函数详解

int rte_eth_dev_rss_reta_update(uint16_t port_id,
                                struct rte_eth_rss_reta_entry64 *reta_conf,
                                uint16_t reta_size);
参数 类型 说明
port_id uint16_t 目标网卡端口 ID
reta_conf struct rte_eth_rss_reta_entry64 * 指向 RETA 配置结构体数组的指针
reta_size uint16_t RETA 表的总表项数,通常为 128 或 512

返回值: 0 表示成功,负数表示错误码。

4.1.3 配置步骤与示例

  1. 查询硬件能力: 通过 rte_eth_dev_info_get 获取 reta_size。
  2. 分配并填充 reta_conf 数组: 数组长度为 reta_size / 64。
  3. 设置 mask 和 reta 值: 将需要更新的表项对应的 mask 位置 1,并写入目标队列 ID。
  4. 调用更新函数: 传入配置数组。
struct rte_eth_rss_reta_entry64 reta_conf[2]; // 假设 reta_size = 128
uint16_t i;

// 初始化所有表项,将哈希值均匀映射到 4 个队列
for (i = 0; i < 128; i++) {
    uint16_t group_idx = i / 64;
    uint16_t entry_idx = i % 64;
    reta_conf[group_idx].mask |= (1ULL << entry_idx);
    reta_conf[group_idx].reta[entry_idx] = i % 4; // 队列 0-3
}

// 更新端口 0 的 RETA 表
int ret = rte_eth_dev_rss_reta_update(0, reta_conf, 128);
if (ret != 0) {
    printf("RETA update failed: %d\n", ret);
}
实践建议: 在 NUMA 架构下,建议将同一 CPU socket 的队列 ID 映射到相邻的 RETA 表项,以减少跨 NUMA 访问延迟。

4.2 RSS 哈希 Key 的设置

哈希 Key 是 Toeplitz 哈希算法的 40 字节(320 位)种子。不同的 Key 会导致相同的五元组计算出不同的哈希值,从而改变数据流的分布。

4.2.1 默认 Key 与自定义 Key

Intel 网卡通常使用一个固定的默认 Key。但在生产环境中,为了防止哈希碰撞攻击或实现流量一致性,建议使用自定义 Key。

Key 类型 特点 适用场景
默认 Key 硬件出厂预设,所有网卡相同 快速原型验证
自定义 Key 用户指定,可随机生成 生产环境、安全敏感场景

4.2.2 rte_eth_dev_rss_hash_update 函数

int rte_eth_dev_rss_hash_update(uint16_t port_id,
                                struct rte_eth_rss_conf *rss_conf);

rte_eth_rss_conf 结构体定义:

struct rte_eth_rss_conf {
    uint8_t *rss_key;      /* 指向 40 字节 Key 的指针 */
    uint8_t rss_key_len;   /* Key 长度,通常为 40 */
    uint64_t rss_hf;       /* 哈希字段掩码,如 ETH_RSS_IP | ETH_RSS_TCP */
};

4.2.3 设置自定义 Key 的步骤

  1. 生成或指定 40 字节 Key 数组。
  2. 填充 rss_conf 结构体: 设置 rss_key 指针、rss_key_len = 40、rss_hf 为所需字段。
  3. 调用更新函数。
uint8_t custom_key[40] = {
    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
};

struct rte_eth_rss_conf rss_conf = {
    .rss_key = custom_key,
    .rss_key_len = 40,
    .rss_hf = ETH_RSS_IP | ETH_RSS_TCP | ETH_RSS_UDP,
};

int ret = rte_eth_dev_rss_hash_update(0, &rss_conf);
if (ret != 0) {
    printf("RSS hash key update failed: %d\n", ret);
}
注意: 哈希 Key 的更新必须在网卡启动前(rte_eth_dev_start 之前)或停止后进行。在运行中更新可能导致短暂的数据包乱序。

4.3 RETA 与哈希 Key 的协同工作流程

下图展示了从数据包到达网卡到最终分配到接收队列的完整流程:

RSS 配置与数据包分发流程 RSS 数据包分发流程:RETA + 哈希 Key 数据包到达 提取五元组 Toeplitz 哈希计算 使用自定义 Key 取哈希值低 N 位作为索引 查询 RETA 表获取队列 ID RETA 表项映射 分发到目标接收队列

4.4 配置验证与调试

配置完成后,可以通过以下方法验证 RSS 是否生效:

  • 查看队列统计: 使用 rte_eth_stats_get 检查各队列的收包计数是否均匀。
  • 硬件寄存器读取: 通过 DPDK 的 rte_eth_dev_get_reg_info 读取 RETA 表内容。
  • 抓包验证: 使用 dpdk-pdump 工具捕获不同队列的数据包,确认五元组分布。
常见问题: 如果所有流量都落在同一个队列,请检查:1) RETA 表是否全部映射到同一个队列 ID;2) 哈希 Key 是否全零;3) 网卡是否支持 RSS 且已正确使能。

4.5 本章小结

RSS 的配置核心在于 RETA 表和哈希 Key 的协同设置。RETA 表决定了“哈希值→队列”的静态映射,而哈希 Key 则影响“五元组→哈希值”的动态计算。通过 DPDK 提供的 rte_eth_dev_rss_reta_update 和 rte_eth_dev_rss_hash_update 函数,开发者可以精确控制流量分发行为,实现多队列的负载均衡。