一次RSS配置错误引发的百万级乱序事故——深入理解DPDK RSS、Toeplitz Hash、Queue Affinity与Flow一致性(下)

一次RSS配置错误引发的百万级乱序事故——深入理解DPDK RSS、Toeplitz Hash、Queue Affinity与Flow一致性(下)

接上文:一次RSS配置错误引发的百万级乱序事故——深入理解DPDK RSS、Toeplitz Hash、Queue Affinity与Flow一致性(上)-CSDN博客

八、真正决定Queue归属的,并不是Hash,而是RETA

很多DPDK开发者都有一个误区:

RSS Hash是多少,就进入哪个Queue。

实际上,这是错误的。

RSS真正的流程是:

五元组 │ ▼ Toeplitz Hash │ ▼ 32bit Hash Value │ ▼ RETA(Redirection Table) │ ▼ RX Queue

也就是说,Hash只是索引,RETA才是真正决定Queue归属的映射表。

Intel大部分网卡(82599、X710、XXV710、E810等)都维护着一张硬件RETA。

例如:

假设RSS Hash最终得到:

0x35

并不会直接进入Queue53。

而是:

RETA[0x35] ↓ Queue 7

如果:

0x36 ↓ Queue 7

那么两个完全不同的Flow,也可能进入同一个Queue。

因此:

Hash决定Flow是否一致。

RETA决定负载是否均衡。

这是两个完全不同的问题。


九、问题终于出现了

继续检查升级脚本。

工程师发现了这样一段初始化代码。

以前:

16 Queue RETA: 0 1 2 ... 15 循环映射

升级以后:

32 Queue 重新生成RETA

代码没有任何问题。

但是。

Dispatcher没有修改。

系统架构仍然保持:

Queue0 ↓ Worker0 Queue1 ↓ Worker1 …… Queue15 ↓ Worker15

新增出来:

Queue16~31

全部重新绑定到了另一组CPU。

理论上,这也没有问题。

因为:RSS就是为了多Queue。

可是,真正的问题藏在Session里面。


十、Session Owner 与 RSS Queue,不是一回事

系统的数据面采用的是典型状态机架构。

每一个Session,都有唯一Owner。

例如:

Session100 ↓ Worker5

以后。

无论哪个方向的数据包。

都必须交给:

Worker5

因为:

Worker5维护:

  • Session状态
  • TCP状态
  • NAT状态
  • Statistics
  • Timer

所有运行时数据。

这就是:

Session Affinity。

也是所有高性能网关、UPF、防火墙共同遵循的原则。

但是RSS并不知道Session。

RSS只知道:

Hash。

于是:

同一个Session。

第一次:

RSS ↓ Queue5

第二次:

RSS ↓ Queue21

第三次:

RSS ↓ Queue7

为什么?

因为升级以后:

RETA重新分布。

虽然Hash没有变化。

但是:

Hash对应的Queue已经发生变化。

于是。

Dispatcher收到的数据包开始来自不同Queue。

如果软件设计是:

Queue ↓ Worker

那么。

同一个Flow。

真的可能进入不同Worker。


十一、为什么会出现TCP乱序?

这里很多人容易产生疑问。

同一个Hash不是应该一直对应同一个Queue吗?

答案是:

对于同一个时间点来说,是的。

但是现场的问题并不是:

Hash变化。

而是:

软件线程模型错误。

很多项目直接这样设计:

Queue0 ↓ Worker0 Queue1 ↓ Worker1 ……

Worker既负责:

  • 收包

又负责:

  • Session

又负责:

  • Forward

看起来效率最高。

实际上。

这种架构有一个巨大隐患。

例如:

Queue5负载突然升高。

开发人员为了均衡CPU。

修改RETA。

把部分Hash迁移:

Queue5 ↓ Queue21

RSS认为:

很好。

CPU更均衡了。

但是:

Session仍然属于:

Worker5

于是:

新的数据包开始进入:

Worker21

旧的数据包仍然在:

Worker5

结果就是:

同一TCP连接。

两个CPU同时处理。

TCP包自然开始乱序。


十二、真正的问题不是RSS,而是软件架构

很多人一直认为:

RSS负责:

Flow Affinity

实际上。

RSS只能保证:

Flow ↓ Queue

却不能保证:

Flow ↓ Session Owner

因为:

Session属于软件。

Queue属于硬件。

二者没有任何关系。

这也是为什么现代DPDK项目越来越少采用:

RSS ↓ Worker

而更多采用:

RSS ↓ Dispatcher ↓ Session Lookup ↓ Owner Worker

Dispatcher只负责:

  • 收包
  • Parser
  • Session Lookup

找到:

session*

然后读取:

session->owner

最后:

Enqueue ↓ 真正Owner Worker

这样。

无论RSS怎样变化。

Session永远不会迁移。

Flow一致性也不会被破坏。


十三、PMU为什么会出现大量Cache Miss?

故障解决以后。

工程师继续分析:

为什么吞吐下降这么明显?

Perf结果:

perf stat

发现:

LLC Miss 增加2.8倍

继续:

perf c2c

结果更加明显。

热点集中:

Session结构 Cache Line Bounce

原因非常简单。

以前:

Session100 ↓ Worker5

整个生命周期。

只有:

CPU5

访问。

Cache始终留在:

CPU5。

升级以后:

CPU5 ↓ CPU21 ↓ CPU7 ↓ CPU5

每一个包。

Session Cache Line都要重新迁移。

Intel MESI协议开始不停:

Invalidate ↓ Transfer ↓ Reload

CPU真正执行计算的时间没有增加。

增加的是:

等待Cache一致性的时间。

于是:

Backend Stall开始暴涨。

CPU依旧100%。

但是。

有效指令数却下降了。

吞吐自然下降。


十四、为什么Linux内核很少遇到这个问题?

很多人会问:

Linux开启RSS几十年了。

为什么很少听说这种问题?

原因就在于:

Linux网络栈本身就是:

RSS ↓ SoftIRQ ↓ 协议栈

Socket最终并不绑定:

某一个CPU。

TCP状态也允许:

不同CPU参与处理。

再加上:

  • RPS
  • RFS
  • XPS
  • Flow Limit

Linux拥有完整的软件负载均衡机制。

因此:

RSS只是第一层。

而DPDK追求的是:

每秒数千万甚至上亿数据包的极致性能。

任何:

  • 原子操作
  • Cache Bounce

都会严重影响吞吐。

因此。

DPDK几乎都会采用:

Single Owner(单拥有者)

Session只能属于:

一个Worker。

这也是为什么:

现代高性能:

  • 防火墙
  • UPF
  • vRouter
  • DPI
  • IPS

几乎都采用:

Dispatcher ↓ Owner Worker

而不是:

RSS ↓ Worker

十五、生产环境的最佳实践

经过这次事故,团队重新梳理了整个数据面架构,并总结出以下原则:

第一,RSS只负责负载均衡,不负责Session归属。

RSS的职责是把流量尽可能均匀地分配到多个RX Queue,提高网卡接收能力;Session的拥有权必须由软件统一管理。

第二,不要把RX Queue与Worker做永久绑定。

Queue只是数据进入系统的入口,而不是业务处理线程的身份标识。任何依赖"Queue = Worker"的设计,在RETA调整、RSS Key修改或网卡更换时,都可能埋下隐患。

第三,Dispatcher负责建立Flow与Session之间的联系。

Dispatcher完成协议解析、Session查找以及Owner确认后,再把数据包投递给真正的业务Worker,保证状态始终由唯一线程维护。

第四,Worker永远不迁移Session。

Session一旦创建完成,其Owner应保持稳定。即使RETA发生变化、RX Queue数量调整或NIC升级,也不应影响Session归属。

第五,把RSS当成硬件能力,而不是软件架构。

RSS只是提升接收并行度的工具,它不能替代Dispatcher,也不能解决状态一致性问题。


十六、写在最后

很多性能问题,并不会表现为CPU不足、网卡丢包或算法退化。

真正复杂的故障,往往出现在硬件调度机制与软件架构设计之间的边界

RSS、Toeplitz Hash和RETA共同解决的是数据包如何进入系统;Dispatcher、Session Owner和Worker解决的是数据包进入系统之后,由谁负责处理

两者职责不同,却必须彼此配合。

这次事故最终恢复的关键,并不是修改RSS参数,也不是优化ACL或Hash算法,而是重新建立了**"RSS负责接收并行,Dispatcher负责状态归属,Worker负责唯一处理"**这一清晰的数据面职责划分。

对于设计高性能DPDK系统而言,一个值得长期坚持的原则是:

硬件负责把包送进来,软件负责把状态管起来。不要让RX Queue决定Session归属,也不要让RSS替代软件架构。

这也是当前主流高性能网络设备(交换机、UPF、vRouter、防火墙等)普遍采用Dispatcher + Owner Worker架构的重要原因。只有将硬件并行能力软件状态一致性解耦,系统才能在扩容、升级和演进过程中始终保持稳定、高性能和可维护。