3. 数据包接收路径的核间分流设计

在 Marvell DPU 这类多核 SoC 上,收包性能的上限往往不取决于单核处理速度,而取决于“包被分到了哪个核、这个核是否已经过载”。本节围绕 RX 队列、RSS 哈希、流表分发与核亲和性四条主线,给出一个可落地的多核收包分流调优方案。

一、先看清收包路径上的四个分流决策点

一个数据包从网口进入 DPU,到被某个 CPU 核上的应用处理,中间会经过多次“归属决策”。理解这些决策点的先后顺序,是调优的前提:

  • RX 队列(RXQ):网卡按队列号把包写入不同的描述符环,队列是硬件层面的分流单元,也是后续所有分流的物理载体。
  • RSS 哈希:网卡对包的五元组(源/目的 IP、源/目的端口、协议)做 Toeplitz 哈希,取低若干位映射到某个 RXQ,实现无状态流级分流。
  • 流表分发:软件层(如 DPDK rte_flow、OVS 或自研流表)根据更细的规则把包定向到特定队列或特定核,用于处理需要状态跟踪的流。
  • 核亲和性:通过中断亲和性(IRQ affinity)或轮询线程绑定(CPU affinity),把某个 RXQ 固定交给某个核,避免核间迁移带来的缓存失效。

这四者不是并列关系,而是“硬件先分、软件再分、最后绑定到核”的串联链路。调优的本质,是让这条链路上的每一级都尽量均匀、稳定、可预测。

DPU 收包分流链路:硬件分流 → 软件分发 → 核绑定 网口 / MAC 入向数据包 RSS 哈希 Toeplitz(五元组) 取低 N 位 → 队列号 RX 队列组 RXQ0 RXQ1 RXQ2 RXQ3 流表分发 rte_flow / 流规则 定向到指定队列 核 亲和 绑定 CPU 核阵列(示例:8 核,每核绑定 1 个 RXQ) Core 0 RXQ0 Core 1 RXQ1 Core 2 RXQ2 Core 3 RXQ3 Core 4 RXQ4 Core 5 RXQ5 Core 6 RXQ6 Core 7 RXQ7 RSS 哈希分布示意:不同流经哈希后落入不同 RXQ 流A: 10.0.0.1:80 RXQ2 → Core2 流B: 10.0.0.2:443 RXQ5 → Core5 流C: 10.0.0.3:8080 RXQ7 → Core7 流D: 10.0.0.4:22 RXQ0 → Core0 注:哈希结果取决于 RSS key 与哈希域配置,图中为示意分布

二、RSS 哈希:决定“流”落到哪个队列

RSS 的核心是哈希函数与重定向表。以常见的 40 字节 RSS key 为例,网卡对选定的字段做 Toeplitz 哈希,得到一个 32 位结果,再取低 7 位(对应 128 个表项)查重定向表,得到最终队列号。调优时关注三点:

  1. 哈希域选择:默认 IPv4 TCP 使用“源IP+目的IP+源端口+目的端口”。如果业务中大量流的目的端口相同(如都访问 443),哈希熵会下降,可考虑加入 VLAN 或协议字段增加区分度。
  2. RSS key 一致性:多队列场景下,同一 key 才能保证同一条流始终落到同一队列。若中途更换 key,已建立的流会“跳队列”,导致乱序和状态丢失。
  3. 重定向表均衡:128 个表项要均匀映射到 N 个队列。若手工配置不当,可能出现某队列占用 40 个表项、另一队列只有 4 个,造成先天不均。

一个可参考的检查方法是:用 ethtool -x <ifname> 查看当前重定向表,统计每个队列被映射的表项数,偏差超过 ±20% 就应重新分配。

三、流表分发:处理 RSS 覆盖不了的场景

RSS 是无状态的,它无法保证“同一会话的双向包落到同一队列”,也无法处理需要精确匹配的隧道解封装、ACL 或负载均衡场景。这时需要流表分发介入。以 DPDK 为例,典型做法是:

/* 示例:将目的端口 8080 的流定向到 RXQ3 */
struct rte_flow_attr attr = { .ingress = 1, .group = 0 };
struct rte_flow_item pattern[] = {
    { .type = RTE_FLOW_ITEM_TYPE_ETH },
    { .type = RTE_FLOW_ITEM_TYPE_IPV4 },
    { .type = RTE_FLOW_ITEM_TYPE_TCP,
      .spec = &(struct rte_flow_item_tcp){
          .hdr.dst_port = rte_cpu_to_be_16(8080) } },
    { .type = RTE_FLOW_ITEM_TYPE_END },
};
struct rte_flow_action actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_QUEUE,
      .conf = &(struct rte_flow_action_queue){ .index = 3 } },
    { .type = RTE_FLOW_ACTION_TYPE_END },
};
rte_flow_create(port_id, &attr, pattern, actions, &error);

流表分发的调优要点是规则优先级与命中率。规则越多,查表开销越大;应把高频流放在高优先级组(group 0),低频或兜底规则放到低优先级组。同时要监控流表 miss 计数,miss 率过高说明规则覆盖不足,包会退回 RSS 默认路径。

四、核亲和性:把队列“钉”在核上

队列分好了,还要决定由哪个核来收。两种主流模式:

  • 中断模式:网卡收到包后触发中断,由绑定该中断的核处理。通过 /proc/irq/<irq>/smp_affinity 设置亲和性,让每个 RXQ 的中断落到不同核。
  • 轮询模式(PMD):核持续轮询 RXQ,无中断开销,适合高吞吐。此时用 rte_lcore 与队列一一绑定,避免多核争抢同一队列。

亲和性配置的核心原则是“一队列一核、核不跨队列”。如果两个核同时轮询同一个 RXQ,会出现锁竞争和缓存行乒乓;如果一个核绑定多个高流量队列,则会成为瓶颈。建议先用 lscpu 确认物理核与超线程布局,再把 RXQ 绑定到物理核,避免同一物理核的两个超线程争抢。

调优提示:NUMA 感知不可忽略。网卡插在哪个 NUMA 节点,RXQ 就应绑定到该节点内的核,跨节点访问内存会显著增加收包延迟。可用 cat /sys/class/net/<ifname>/device/numa_node 确认网卡所在节点。

五、一个可执行的调优闭环

把上述四点串起来,形成如下调优流程:

  1. 基线测量:用 sar -n DEV 1 或 DPDK testpmd 观察各 RXQ 的收包速率,找出是否存在“零流量队列”或“过载队列”。
  2. 调整 RSS 重定向表:让表项均匀覆盖所有队列,偏差控制在 ±20% 以内。
  3. 补充流表规则:对哈希不均的“大象流”用 rte_flow 显式指定队列,把热点流打散。
  4. 绑定核亲和性:按“一队列一核”原则设置 IRQ affinity 或 PMD 线程绑定,并确认 NUMA 同节点。
  5. 复测与对比:重新测量各队列速率与 CPU 利用率,若最大队列与最小队列的速率比从 5:1 降到 1.5:1 以内,且无核利用率超过 80%,即可认为分流基本均衡。
常见误区:只调 RSS 不调亲和性,或只调亲和性不调 RSS,都只能解决一半问题。RSS 决定“包去哪”,亲和性决定“谁来看”,两者必须匹配。另外,不要为了追求绝对均匀而频繁改动 RSS key,运行中改 key 会破坏已有流的队列一致性。

实际工程中,建议把上述参数固化为一份配置基线:RSS key、重定向表、流表规则集、IRQ affinity 映射、NUMA 节点,每次变更后重新跑一遍基线测量。这样,多核收包分流就从“凭感觉调”变成了“有据可依的工程闭环”。