一次持续三天才出现的丢包故障——深入解析 DPDK Memory Ordering、rte_ring 与 CPU Memory Barrier (上)
一、一个几乎无法复现的现网故障某运营商部署了一套基于 DPDK 25.11.2 开发的高性能交换平台。整体架构非常典型RX PMD │ ▼ Dispatcher │ ▼ rte_ring │ ▼ Worker │ ▼ TX PMD系统已经连续稳定运行半年。每天处理的数据量超过2.8 × 10^11 PacketsCPU 利用率稳定。没有丢包。没有异常日志。但是有一天凌晨监控突然报警Worker 17 Crash SIGSEGV NULL Pointer Dereference重启之后恢复正常。继续运行。三天以后再次 Crash。而且Crash 的位置并不固定。有时候session-far-action空指针。有时候pkt-userdata内容错误。还有时候整个 mbufMagic Cookie Error完全损坏。更奇怪的是日志里面Ring没有任何错误。enqueue OK dequeue OK Ring Count Normal所有统计信息全部正常。二、所有人都认为是并发问题第一次分析时。开发团队几乎一致认为一定是多线程竞争于是开始排查Session 是否加锁Hash 是否线程安全Ring 是否覆盖mbuf 是否重复释放mempool 是否耗尽RCU 是否使用错误连续排查一周。没有任何发现。于是又开始怀疑DPDK是不是Ring存在 Bug于是把rte_ring_enqueue()改成pthread_mutex_lock() queue.push() pthread_mutex_unlock()问题彻底消失。所有人都认为DPDK Ring有问题。事实上恰恰相反。真正的问题是程序本身。三、为什么加锁以后问题就消失了很多开发者都有一个误区认为Lock 线程安全实际上Lock还有一个非常重要的作用Memory Ordering内存顺序保证例如下面这段代码obj-session session; obj-len len; ring-tail;很多人认为CPU一定按照第一句 ↓ 第二句 ↓ 第三句执行。实际上。现代 CPU根本不是这样工作的。为了提高执行效率。CPU会进行指令乱序执行Out-of-Order ExecutionStore Buffer 合并Load SpeculationCache Line 合并也就是说程序员看到的代码顺序。并不一定就是CPU真正执行的顺序。四、CPU 真正看到的世界假设Producer执行obj-data pkt; obj-length len; ring-tail next;程序员认为一定是data ↓ length ↓ tail但是CPU可能变成tail ↓ data ↓ length看到这里。很多人会疑惑tail 为什么会先写答案就是Store Buffer。五、Store Buffer现代 CPU 性能的秘密为了避免CPU每写一次内存都等待DDR。现代 CPU都会增加Store Buffer。工作方式大致如下CPU Core │ Store │ ▼ Store Buffer │ 后台刷新 ▼ L1 Cache │ ▼ L2 │ ▼ L3 │ ▼ MemoryCPU执行obj-data pkt;并不是立即写到Cache。而是先进入Store Buffer。CPU继续执行下一条。如果后面的ring-tail next;满足提交条件。完全可能比前面的数据更早对其它 CPU可见。六、一个最危险的时刻出现了假设Producer正在发送一个对象。正常情况下应该先写Object ↓ 更新TailConsumer流程看到Tail ↓ 取Object ↓ 处理Object如果发生Memory Reordering。CPU可能变成更新Tail ↓ Object 还在Store Buffer此时Consumer已经看到Tail于是开始读取Object。结果拿到未初始化对象表现出来就是随机NULL Pointer 随机Crash 偶发数据错误 Magic Cookie错误而且极难复现。因为只有Store Buffer刷新时机刚好满足竞争窗口。才会出现。七、为什么 x86 很少出现而 ARM 更容易踩坑很多团队都有一个现象程序在 Intel运行几年没有问题。迁移ARM不到一天Crash。为什么原因就在于不同 CPU 架构对于Memory Ordering采取了完全不同的设计策略。x86 使用的是TSOTotal Store Order模型对普通内存访问提供了较强的顺序保证因此很多隐藏的问题在 x86 上“不容易暴露”。而 ARM、RISC-V 等架构采用的是更宽松的内存模型CPU 和编译器拥有更大的重排序空间。如果程序缺少必要的 Memory Barrier同样的代码在这些平台上更容易出现随机错误。也正因为如此DPDK 在无锁数据结构中大量使用了架构相关的内存屏障而不是依赖某一种 CPU 的默认行为。未完待续