一次持续三天才出现的丢包故障——深入解析 DPDK Memory Ordering、rte_ring 与 CPU Memory Barrier (下)

接上文:一次持续三天才出现的丢包故障------深入解析 DPDK Memory Ordering、rte_ring 与 CPU Memory Barrier (上)-CSDN博客

八、DPDK Ring 真正依赖的不是 CAS,而是 Memory Ordering

很多开发者第一次阅读 lib/ring 源码时,都会把注意力集中在 CAS(Compare-And-Swap)上。

例如:

复制代码
head = atomic_compare_exchange(...);

于是得出结论:

CAS 保证了线程安全。

事实上,这是一个非常普遍的误区。

CAS 能保证的是:

某一个共享变量的原子更新。

例如:

复制代码
Producer A

tail = 100

↓

CAS

↓

tail = 101

Producer B:

复制代码
tail = 100

↓

CAS失败

↓

重新获取Tail

CAS 保证:

不会有两个线程同时把:

复制代码
tail

100

↓

101

更新成功。

但是:CAS 并不能保证数据已经准备好了。

举一个简单例子:

复制代码
obj->session = session;
obj->pkt = pkt;

atomic_store(&ring->tail, next);

如果:

没有任何 Memory Barrier。

CPU 完全可能:

复制代码
Tail

已经更新

↓

Object

还没有完全写入

Consumer:看到Tail已经变化。

立即开始读取Object。

结果:拿到半初始化对象。

这就是为什么:很多 Crash都是随机。

而且几年才出现一次。


九、Release 与 Acquire:无锁编程真正的核心

现代 DPDK 已经大量采用 Acquire/Release 语义来保证内存可见性。

Producer 的正确执行顺序应当是:

复制代码
初始化对象

↓

Release Store

↓

更新Tail

Release 的含义是:

在 Release 之前,对对象的所有写操作,都必须先对其它 CPU 可见。

Consumer读取Tail使用Acquire。

流程:

复制代码
Acquire Load

↓

读取Object

Acquire保证后面的所有读取。

一定发生在Acquire之后。

也就是说:

Producer:

复制代码
Object

↓

Release

↓

Tail

Consumer:

复制代码
Acquire Tail

↓

Object

两边共同形成完整的Memory Ordering。

这也是现代Lock-Free算法最重要的思想。


十、为什么 tail 一定要最后更新?

假设:

Ring有1024 个槽位。

Producer准备放入第257 个对象。

如果:

执行顺序变成:

复制代码
Tail++

↓

Object

Consumer:

看到:

复制代码
Tail

257

自然认为:

第257已经准备好了。

于是立即读取对象。

但是:

此时Producer实际上还没有完成对象初始化。

Consumer读到可能是:

复制代码
NULL

旧对象

垃圾数据

所以:

对于所有Lock-Free Queue。

都有同一条原则:

对象必须先完成初始化,然后才能发布(Publish)给其它线程。

而Tail:

就是整个对象对外发布的那个动作。

这也是为什么源码里面所有Barrier都围绕Head/Tail更新。

而不是对象初始化。


十一、DPDK 为什么要区分 rte_smp_wmb()rte_smp_rmb()rte_smp_mb()

很多初学者:

看到三个 Barrier。

都会觉得:

复制代码
都是:

Memory Barrier

其实它们作用完全不同。

rte_smp_wmb()

Write Memory Barrier。

保证之前所有Store必须完成之后才能继续后面的Store。

适用于:Producer。

例如:

复制代码
obj->pkt = pkt;
obj->len = len;

rte_smp_wmb();

ring->tail = next;

目的:

保证Tail最后才可见。


rte_smp_rmb()

Read Memory Barrier。

Consumer读取Tail以后。

必须保证后面的Object读取。

不会跑到Tail之前。

例如:

复制代码
tail = ring->tail;

rte_smp_rmb();

obj = ring[idx];

否则:

CPU可能提前读取Object。

然后:

Tail还是旧值。

造成逻辑混乱。


rte_smp_mb()

Full Barrier。

读写全部禁止重排序。

性能也是最昂贵。

通常只在确实需要完全同步时使用。

DPDK绝大多数场景都会优先选择Acquire/Release。

因为成本更低。


十二、为什么 x86 上很多 Bug 永远复现不了?

很多开发者都会说:

我的代码在 Intel 上跑了三年,没有任何问题。

然后部署ARM。

第一天:Crash。

第二天:继续Crash。

原因就在于:

x86采用TSO。

它天然保证很多Store不会乱序。

所以很多隐藏 Bug被掩盖。

而ARM更加宽松。

很多StoreLoad都可以重新排序。

于是所有隐藏问题全部暴露。

这也是为什么:DPDK源码所有Lock-Free算法都不会依赖x86行为。

而是统一使用Memory Barrier。


十三、如何定位 Memory Ordering 问题?

这种 Bug最大的特点就是:几乎不能依靠日志定位。

因为日志打印的时候。

Memory Ordering已经改变。

真正有效的方法包括:

① Thread Sanitizer(TSAN)

对于非 DPDK 部分,可借助 TSAN 检测普通数据竞争。但需要注意,DPDK 大量无锁代码和轮询模型并不一定适合直接开启 TSAN,因此更多用于业务层共享数据的检查。


② perf + 火焰图

确认是不是大量时间花在Spin或者Barrier。

虽然无法直接证明 Memory Ordering 错误,但可以辅助分析同步热点。


③ Code Review

Memory Ordering最大的特点就是源码一眼看不出来。

必须逐句分析:

复制代码
Object

什么时候:

初始化

↓

什么时候:

Publish

↓

什么时候:

Consumer:

开始:

读取

真正定位90%靠源码。

不是日志。


十四、从这个故障中,我们真正应该学到什么?

回到文章开头。

为什么系统连续运行三天才Crash?

答案其实非常简单。

因为必须满足三个条件同时发生。

第一:Producer发生Store Buffer延迟。

第二:Consumer刚好提前读取。

第三:竞争窗口刚好成立。

这种概率非常低。

但是每天处理几千亿Packet。

最终一定会发生。

这也是高性能系统最可怕的一类Bug。

它不是必现。

而是迟早出现。


十五、总结

很多人认为:Lock-Free编程最重要的是CAS。

实际上:CAS只是解决共享变量更新。

真正保证正确性的是:

Memory Ordering。

DPDK 的 rte_ringrte_mempool、部分 rte_hash 以及大量无锁数据结构,都是建立在 正确的内存可见性模型 之上的。

一个对象只有在:

  1. 数据全部初始化完成;
  2. 通过 Release 语义发布;
  3. 消费者通过 Acquire 语义获取;

之后,才能保证其它 CPU 看到的是一个完整、一致的对象。

因此,Memory Barrier 并不是为了"让代码变慢",而是为了告诉 CPU:哪些顺序绝对不能改变。

对于 DPDK 开发者而言,真正需要建立的思维方式不是"加不加锁",而是理解:

对象什么时候构造完成?什么时候发布?什么时候允许其它核心观察到?

当你开始用这种视角阅读 rte_ringrte_mempool、RCU、连接跟踪表乃至整个数据平面的无锁设计时,就会发现,它们背后的设计思想高度一致------先构造,再发布;先获取,再访问;依靠 Memory Ordering,而不是侥幸依赖某一种 CPU 架构的默认行为。

相关推荐
wabs66616 小时前
关于图论【深度优先搜索的理论基础】
算法·深度优先·图论
锋行天下16 小时前
打造企业内部知识库系统RAG全栈项目
前端·后端·架构
先吃饱再说17 小时前
LeetCode 101. 对称二叉树
算法
是Dream呀17 小时前
人眼看不出的伪造,AI如何识别?实测合合信息跨模态鉴伪与去反光技术
算法
这个DBA有点耶17 小时前
索引碎片与统计信息维护:执行计划突然变差的隐形杀手
mysql·程序员·架构
Discipline~Hai17 小时前
ARM01-ARM体系架构
linux·c语言·arm开发·架构
XUHUOJUN17 小时前
AKS 不是安装在 Windows Server 上,而是运行在 Windows Server 之上的 Azure 平台能力
windows·架构·k8s·azure local·azure stack
QN1幻化引擎17 小时前
SFA 信号场注意力:用8KB参数换248x KV Cache压缩,边缘设备也能跑长序列
人工智能·算法·gitee·gitlab
段一凡-华北理工大学18 小时前
向量数据库实战:选型、调优与落地~系列文章03:向量相似度算法全解:余弦、欧氏、内积,到底该用哪个?
大数据·数据库·人工智能·算法·机器学习·向量相似度·高炉炼铁