一次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 ↓ Queue21RSS认为:
很好。
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 WorkerDispatcher只负责:
- 收包
- 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 ↓ ReloadCPU真正执行计算的时间没有增加。
增加的是:
等待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架构的重要原因。只有将硬件并行能力与软件状态一致性解耦,系统才能在扩容、升级和演进过程中始终保持稳定、高性能和可维护。
