4 CPU包处理 - 1.Toeplitz 哈希库

DPDK 提供了一个 Toeplitz 哈希库(Toeplitz Hash Library),用于计算 Toeplitz 哈希函数以及利用其特性进行相关操作。
Toeplitz 哈希函数是一种常见的散列算法,被广泛用于多种网络接口卡(NIC)中,用于计算 接收端散列(RSS,Receive Side Scaling) 的哈希值。该哈希值用于将网络流量均衡地分配到多个接收队列中,从而实现多核并行处理,提高网络数据包处理效率。
RSS 技术背景:现代多核 CPU 系统中,为了避免所有数据包都落到同一个核上造成瓶颈,网络接口卡通过 RSS 机制,根据数据包五元组(源/目的 IP、端口、协议)计算出一个哈希值,决定将数据包送入哪一个接收队列。
Toeplitz 哈希的特点

  • 可以高效计算;
  • 对输入数据分布较敏感,有利于分散流量;
  • 使用一个固定的“key”作为种子,具有可配置性。
    DPDK 中的作用
  • DPDK 提供的该哈希库,既可以让用户自定义 key 来模拟或复现 NIC 的行为;
  • 也可以用于软件模拟 RSS、进行流量特征哈希等操作。
    https://doc.dpdk.org/guides/_images/rss_queue_assign.svg

1.1 Toeplitz 哈希函数 API

DPDK 提供了四个函数用于计算 Toeplitz 哈希值:

rte_softrss()
rte_softrss_be()
rte_thash_gfni()
rte_thash_gfni_bulk()

rte_softrss() && rte_softrss_be()

这两个函数是 标量实现(scalar implementation),参数包括:

一个指向元组(tuple)的指针,该元组中包含从数据包中提取的字段(如源 IP、目的 IP、源端口、目的端口等);

元组的长度,以双字(double word,4 字节)为单位;

一个指向 RSS 哈希密钥(RSS hash key)的指针,该密钥需与网卡上配置的密钥一致。

两者要求的元组需为 主机字节序(host byte order),且长度为 4 字节的倍数。

  • rte_softrss():对 RSS 密钥的要求更严格,密钥必须与 NIC 上使用的完全一致
  • rte_softrss_be():是一个更快的实现,但要求传入的 RSS 密钥已 转换为主机字节序

rte_thash_gfni() && rte_thash_gfni_bulk()

这两个函数是基于 GFNI(Galois Fields New Instructions)向量化实现(vectorized implementation),用于加速计算。只有在平台支持 rte_thash_gfni_supported == true 时才能使用。它们要求元组使用 网络字节序(network byte order)

  • rte_thash_gfni():对单个元组计算哈希值,参数包括:
    • 一个指向哈希矩阵的指针,该矩阵由 rte_thash_complete_matrix() 根据 RSS 密钥生成;
    • 一个指向元组的指针;
    • 元组的字节长度。
  • rte_thash_gfni_bulk():是 rte_thash_gfni()批量实现,适合多流并发哈希,参数包括:
    • 一个指向哈希矩阵的指针;
    • 最大元组的字节长度;
    • 一个指针数组,指向待哈希的数据;
    • 一个 uint32_t 数组,用于存储每个元组对应的哈希值;
    • 元组数量。

rte_thash_complete_matrix()

此函数用于从 RSS 哈希密钥生成 GFNI 实现所需的矩阵。其参数包括:

  • 一个指向内存区域的指针,用于存放生成的矩阵;
  • 一个指向 RSS 哈希密钥的指针;
  • 哈希密钥的长度(以字节为单位)。

GFNI 加速:GFNI 是 Intel 新增的指令集,用于高效执行有限域上的代数运算,适合向量化散列。

主机字节序 vs 网络字节序:对于 IPv4/IPv6 这类协议,字节序很关键。使用 rte_thash_gfni() 系列时,元组应保持网络传输格式。

用途对比

  • rte_softrss() 适合对照网卡行为、测试一致性;
  • rte_thash_gfni_bulk() 更适合高性能、多流环境下的哈希运算。

1.2 可预测的 RSS(Predictable RSS)

在某些应用场景中,我们希望能够找到与原始数据产生 Toeplitz 哈希部分冲突(Partial Collision) 的数据组合。
例如,在 RSS(Receive Side Scaling)中,通常只使用哈希值中 最低的几位(Least Significant Bits, LSB) 来索引 RSS 重定向表(ReTa),从而决定数据包最终落入哪个接收队列。

因此,在这种场景下,如果我们能构造另一个数据元组,使其哈希值与原始元组在 LSB 部分相同,就可以实现 不同连接共享同一接收队列 的目标。

举例说明:

  • SNAT(源地址转换)场景
    在做源地址转换时,我们可以在转换过程中精确选择一个特定的 源端口号,使得返回方向的数据包在 Toeplitz 哈希后,哈希值的 LSB 与原始连接一致,从而命中同一个接收队列。
  • MPLS(多协议标签交换)场景
    如果 MPLS 标签被包含在哈希计算中,那么标签交换路由器(LSR)可以在建立 LSP(标签交换路径) 时,选择一个特定的 MPLS 标签,使其哈希结果落入特定队列。类似方法也可应用于:
    • IPSec 的 SPI(安全参数索引)
    • VXLAN 的 VNI(虚拟网络标识)
    • GRE 的 Key 值等
  • TCP 协议栈优化场景
    在 TCP 栈中,可以为出站连接选择一个特定的源端口号,使得返回包的哈希值能够进入预期的接收队列,从而实现连接在多核系统上的绑定。
API 功能说明:

DPDK 提供了 API 用于实现上述 可预测哈希行为,主要包括以下三个部分:

  1. 创建 thash 上下文(thash context)
    初始化哈希计算所需的上下文结构,通常包含 RSS key 及控制参数。
  2. 创建与上下文绑定的 thash 辅助器(thash helper)
    用于后续动态计算目标哈希值所需的可调字段(如源端口、MPLS 标签等)。
  3. 运行时使用 helper,动态计算元组的可调字段
    在实际运行中,通过 helper 计算出能使哈希结果的 LSB 匹配目标值的字段,达到控制包分配队列的目的

这种 “反向 RSS 控制” 技术并不改变硬件 RSS 的行为,而是通过构造数据内容(如修改源端口或标签字段)使其哈希结果落入指定队列,是一种典型的 利用哈希函数特性进行优化的技巧,在网络负载均衡、加速路径绑定、协议栈优化中有广泛应用。(相当于DPDK的软件RSS算法,是通过Toeplitz算法的特性来构造冲突人为干预网卡的RSS分发)

1.2.1 Thash 上下文

函数 rte_thash_init_ctx() 用于初始化一个与特定网卡或一组网卡关联的 Toeplitz 哈希上下文结构(thash context struct),该上下文结构在后续构造可预测哈希值时起关键作用。

该函数需要以下参数:

  • RSS 重定向表大小的 log2 值
    表示网卡的 ReTa 表的大小(以 log₂ 表示),对应哈希值中用于计算冲突的最低有效位数。例如,如果 ReTa 表大小为 128,则该参数为 7,表示使用哈希值的低 7 位来选择队列。
  • 预定义的 RSS 哈希密钥(可选)
    如果提供为 NULL,则库会自动初始化一个随机的哈希密钥。若需模拟实际硬件行为,推荐使用与网卡一致的 RSS 密钥。
  • RSS 哈希密钥的长度(以字节为单位)
    该长度通常依赖于具体网卡型号或驱动,具体数值可参考对应网卡的数据手册(datasheet)。例如,Intel 网卡常见为 40 字节。
  • 可选标志位(flags)
    控制 thash 上下文初始化行为的附加选项,具体支持如下:
支持的标志位(flags):
  • RTE_THASH_IGNORE_PERIOD_OVERFLOW
    默认情况下,为了安全考虑,库禁止生成具有周期性重复序列的哈希密钥(以防止哈希碰撞攻击)。启用该标志可以关闭此检测机制。
    主要用于测试场景**,当测试流量本身具有均匀分布时,该标志有助于生成分布均匀的哈希值。
  • RTE_THASH_MINIMAL_SEQ
    默认行为是为子元组(subtuple)中的所有位生成特定的比特序列,用以支持碰撞控制。
    启用该标志后,将仅为需要控制的 log₂(RETA_SZ) 个 LSB 位生成最小必要的序列。
    适用场景:当存在多个 thash helper 并且它们可能共享 RSS 密钥的特定位段时,该选项有助于减少比特序列重叠,避免干扰。

总结:

rte_thash_init_ctx() 是构造 Predictable RSS 的第一步,用于建立一个用于哈希冲突控制的上下文环境。该环境明确了:

  • 需要控制的哈希位数;
  • 所用 RSS key;
  • 控制策略(通过标志位);

后续的 thash helper 构造、可控字段计算(如动态选端口)等都依赖于此上下文。

1.2.2 Thash 辅助器(Thash Helper)

函数 rte_thash_add_helper() 用于初始化一个 辅助结构体(helper struct),该结构体绑定于特定的 Toeplitz 哈希上下文(thash context),并针对一个可以修改的目标元组字段(subtuple)进行设置,以达到 哈希值碰撞 的目的。

成功调用后,该函数将:

  • 在 RSS 哈希密钥中写入一段特别计算出的比特序列
  • 同时生成一张 XOR 映射表,用于在运行时快速计算可变字段的取值,从而控制哈希结果。

输入参数:

  • thash 上下文指针(context)
    该 helper 将绑定到指定的 rte_thash_ctx 上下文实例中。
  • 可变子元组的长度(以比特为单位)
    指定可以进行修改的字段的长度。例如,若希望控制源端口(16 位),则该值为 16
  • 可变子元组的偏移(以比特为单位)
    从整个元组(tuple)的起始位置起,待修改字段的偏移量。仍以 bit 计。例如,如果 tuple 结构是 [src_ip][dst_ip][src_port][dst_port],想修改 src_port,则该偏移可能为 96(假设每个 IP 为 32 位,port 为 16 位)。

注意:

调用 rte_thash_add_helper()修改绑定上下文中的 RSS 哈希密钥内容,以支持碰撞控制。

因此,在创建所有所需的 helper 后,必须将上下文中的更新版 RSS key 上传到对应网卡中(NIC),以使其生效。


扩展说明:
  • 该 helper 机制是实现 “反向计算” Toeplitz 哈希的关键部分。
  • 系统可以根据 helper 计算得到一组满足哈希 LSB 值匹配条件的字段值(如端口、标签等)。
  • 结合实际网络场景,可实现如 “为某连接动态分配源端口,使其落入特定 RSS 队列” 等优化策略。
1.2.3 计算用于调整子元组的补码位(Complementary Bits)

函数 rte_thash_get_complement() 用于生成一段特殊的 补码比特序列(complement bit sequence),其长度为 N = log₂(rss_reta_sz),对应于上下文初始化时指定的 RSS 重定向表大小。这段比特序列可以用来对目标元组中的特定位(即子元组)进行 按位异或(XOR)操作,从而实现可控的哈希值 LSB 输出。

输入参数:
  • 关联的 helper 实例
    指向之前通过 rte_thash_add_helper() 为某个子元组创建的 helper 结构体。
  • 原始元组的哈希值(32-bit)
    即对当前未修改元组计算得到的 Toeplitz 哈希值。
  • 目标哈希值的 LSB 部分(desired LSBs)
    用户希望最终得到的哈希值在低位的目标值。典型用途是使最终哈希值映射到目标队列。
应用示例:

假设 RSS 重定向表大小为 128(log₂(128) = 7),表示只使用哈希值的最低 7 位来决定数据包入队位置。

你可以:

  1. 使用 rte_thash_get_complement() 计算出某个子字段(如源端口)需要变更的 XOR 补码;
  2. 将该补码与原字段进行 XOR 得到新的字段值;
  3. 构造修改后的 tuple;
  4. 最终哈希值将具有预期的低 7 位,从而定向发送至期望的 RSS 队列。
总结:
  • 此函数使得用户可以精确控制 Toeplitz 哈希值的低位输出,实现数据流与队列绑定(flow-to-core pinning)
  • 是实现 Predictable RSS 的最后一步,配合 context + helper 完成全过程;
  • 在需要分流精度、内核亲和性优化的系统中非常实用,如:NFV、SDN、容器网络优化等场景。

1.2.4 调整元组的 API(Adjust Tuple API)

rte_thash_get_complement() 函数也提供了一个更高级的封装接口,用于自动修改输入元组,使其在哈希运算后能满足预期的哈希值低位(LSB)条件。该接口封装了多个底层步骤,提供了更为用户友好的方式来实现 可预测 RSS

输入参数:
  • Thash 上下文和 helper
    必须提供已初始化的 rte_thash_ctx 和为目标字段创建的 rte_thash_helper
  • 指向待修改元组的指针
    这是包含多个字段的结构体(如源 IP、目的 IP、端口等),其中某一部分将被修改。
  • 元组的长度(以字节计)
  • 回调函数及其用户数据(callback + userdata)
    提供一个用户自定义的回调函数,用于在每次元组被修改后进行校验(例如校验该元组是否有效、是否符合业务需求等)。
    如果回调函数返回错误,系统将尝试下一个可选解。
  • 最大尝试次数
    指定最多尝试多少次修改元组。这个参数在启用回调函数时尤其有意义,用于限制搜索空间避免死循环。

该 API 会在每次尝试时:

  1. 调用底层的 rte_thash_get_complement() 获取需要用于 XOR 的比特序列;
  2. 将该序列应用于目标子元组;
  3. 将修改后的完整元组传入回调函数进行有效性验证;
  4. 如果回调返回错误,则尝试下一组可能的 XOR 变换,直到达到设定的最大尝试次数。

使用场景示例:
  • 在源端口需要动态分配的场景(如 SNAT),可以使用此接口为每个连接寻找一个能命中指定 RSS 队列的端口号;
  • 在构造 tunnel 时(如 VXLAN、IPSec),可以自动为某个标签字段选择一个最优值;
  • 可结合业务策略判断,使用回调函数限制某些不合法或保留字段值。

总结:
RSS 是 NIC(网卡)提供的硬件特性,用于将收到的数据包分发到多个接收队列,实现 多核负载均衡

它如何决定放到哪个队列?
  • 对数据包的五元组(src_ip, dst_ip, src_port, dst_port, protocol)提取成一个 tuple
  • 使用 Toeplitz 哈希函数 + 一个 固定的 RSS key 进行哈希,生成 32bit 哈希值;
  • 然后根据哈希值的 低 N 位(如 log₂(128) = 7),取模得到队列号(queue id):

那么反向控制是怎么做到的?
Toeplitz 哈希的数学特性:,它是一种 线性可加 的函数(就像异或一样):如果你改变输入元组中的一部分(比如源端口),它对最终哈希值的影响是“可预测的”:
即某些 bit 变了 → 哈希值中的某些 bit 会有线性变化(异或关系)

所以可以逆向:
  • 假设你知道当前 hash = H0
  • 想让它变成 H1,你只需要找到一个 bit mask M 作用在 tuple 的某字段上;

步骤如下:

  1. 用你已有的 src_ip, dst_ip, dst_port 等构造 tuple;
  2. rte_softrss() 算出它的原始哈希值;
  3. rte_thash_get_complement()
    • 告诉它:我想把这个哈希的低 N 位改成 X(目标队列);
    • 它就会给你一个 XOR 补码
  4. 你把这个补码 M 和某个字段(如 src_port)XOR,就得到了新的值;
  5. 把这个新值填回 tuple,重新算一次哈希,哈希的低位就一定是你想要的!
┌────────────────────────────┐
│ 开始:构造五元组(Tuple)   │
│ 包括 src_ip, dst_ip, dst_port │
│ 以及一个基础 src_port       │
└────────────┬─────────────┘
             │
             ▼
┌────────────────────────────┐
│ 使用 rte_softrss() 计算原始哈希值 │
│ 得到当前队列号(hash % RETA_SZ) │
└────────────┬─────────────┘
             │
             ▼
┌────────────────────────────┐
│ 设定目标队列(例如 queue 4)   │
│ → 得到目标哈希的 LSB 值        │
└────────────┬─────────────┘
             │
             ▼
┌────────────────────────────────────┐
│ 使用 rte_thash_get_complement() 计算  │
│ 让哈希值变为目标 LSB 所需的“补码”     │
└────────────┬─────────────────────┘
             │
             ▼
┌────────────────────────────┐
│ 将原始 src_port 与补码 XOR   │
│ 得到新的 src_port           │
└────────────┬─────────────┘
             │
             ▼
┌────────────────────────────┐
│ 构造新的 tuple,替换 src_port │
│ 再次计算 hash 验证队列是否命中 │
└────────────┬─────────────┘
             │
             ▼
┌────────────────────────────┐
│ 落入目标队列?               │
│ 是 → 成功绑定                 │
│ 否 → 回滚或重试其他端口        │
└────────────────────────────┘

posted @ 2025-04-22 22:46  Tohomson  阅读(315)  评论(0)    收藏  举报