我早期做DPDK多核开发时总有一个认知只要没有锁性能就一定很好。因为没有 mutex没有 spinlock没有 rwlock全部 lockless理论上应该扩展性极强。但我曾经遇到一个非常诡异的问题程序架构完全无锁每核独立 RX queueper-core statistics独立 mempool cache无共享 ring看起来已经非常“DPDK 化”。但性能却始终上不去。更奇怪的是随着 core 数增加性能不仅没提升。反而下降。例如2 核8 Mpps4 核11 Mpps8 核10 MppsCPU 全部打满。但吞吐增长极差。第一次遇到时我怀疑过NUMAmbuf cacheRX queue 配置descriptor 不够PCIe 带宽最后才发现真正的问题是false sharing而这个问题也是多核高性能程序里最隐蔽、最容易被忽略的问题之一。一、问题现场程序很简单每个 lcore独立收包独立统计独立处理统计结构struct worker_stats { uint64_t rx_pkts; uint64_t tx_pkts; uint64_t drops; };然后struct worker_stats stats[MAX_CORE];每个核更新自己的stats[lcore_id]看起来完全没问题。因为没有锁。二、现象却很奇怪perf 结果大量 cache miss同时snoop hit cache invalidation异常高。三、为什么没有锁还会 cache 冲突这是理解 false sharing 的关键。很多人以为只有多个线程访问同一个变量 才会冲突。其实CPU cache 的粒度不是变量。而是cache line通常64 bytes四、什么是 cache lineCPU cache 不会一次只加载8 字节而是整块加载。例如64-byte cache line五、问题就出在这里虽然stats[0] stats[1]是不同变量。但它们可能位于同一个 cache line。六、于是发生什么例如core0更新stats[0].rx_pktscore1同时更新stats[1].rx_pkts虽然逻辑上互不相关。但物理上同一个 cache line。七、MESI 协议开始工作CPU 为了保证缓存一致性使用cache coherence protocol例如MESI八、于是 cache line 被疯狂抢夺过程类似core0 修改 ↓ cache line exclusive ↓ core1 修改 ↓ invalidate core0 ↓ core0 再修改 ↓ invalidate core1不断抖动。九、这就是 false sharing即“逻辑上不共享”、“物理上共享”导致 cache line 竞争。十、为什么 DPDK 特别容易遇到因为DPDK 本身极高 PPS极高 cache 敏感度多核持续写入busy pollingcache line 抖动会被无限放大。十一、为什么 core 越多性能反而越差因为参与竞争的 core 增多。cache coherence traffic 激增。最终CPU 大量时间浪费在cache sync而不是真正处理包。十二、如何确认 false sharingperf 非常关键。例如perf stat关注cache-missesLLC-load-missessnoop hitsremote HITM如果HITM 很高通常意味着cache line 争用。十三、真正修复方法后来做了一个简单修改struct worker_stats { uint64_t rx_pkts; uint64_t tx_pkts; uint64_t drops; } __rte_cache_aligned;十四、__rte_cache_aligned 是什么这是 DPDK 中非常经典的宏。作用cache line 对齐。通常64 bytes aligned十五、这样会发生什么现在stats[0] stats[1]分别位于不同 cache line。十六、于是竞争消失core0只修改自己的 line。core1也只修改自己的。不再互相 invalidation。十七、优化效果非常明显优化前CorePPS28 Mpps411 Mpps810 Mpps优化后CorePPS28 Mpps415 Mpps828 Mpps扩展性完全恢复。十八、一个更隐蔽的问题ring head/tailfalse sharing 不仅发生在 stats。还经常发生在ring producer indexconsumer indexqueue stateflow counter这些高频写变量。十九、为什么 DPDK 到处都有 cache align你会发现DPDK 源码里大量__rte_cache_aligned以前很多人只是“照着写”。其实背后都是避免 false sharing。二十、进一步理解 cache friendly design高性能程序优化很多时候已经不是算法复杂度。而是cache topology。包括cache lineNUMAprefetchmemory locality这些。二十一、为什么 false sharing 特别难发现因为没有锁没有崩溃没有错误日志CPU 也很高程序“看起来正常”。只是性能奇差。二十二、一个经典误区很多人认为无锁 高性能其实真正昂贵的不一定是锁。而是cache coherence。二十三、进一步理解现代 CPU现代多核 CPU真正贵的操作往往不是add mul branch而是跨核 cache 同步因为涉及interconnectsnoopinvalidatememory ordering二十四、为什么 DPDK 如此强调 per-coreDPDK 的设计哲学之一per-core everything即per-core mempool cacheper-core RX queueper-core statisticsper-core flow本质都是避免共享。二十五、这次排查真正学到什么以前我以为多核优化就是避免锁。后来才意识到真正困难的是避免 cache line 共享。这也是为什么很多高性能程序代码看起来很“浪费内存”。因为它们在用空间换 cache efficiency。二十六、工程经验总结DPDK 中高频写变量必须cache alignedper-core避免共享尤其统计计数器。二十七、总结为什么 DPDK 程序明明没有锁却还是性能很差很多时候不是算法问题网卡问题NUMA 问题而是false sharing。通过这个问题我们真正理解了核心概念cache lineMESIcache coherencefalse sharingcache alignedper-core design这也是高性能网络开发真正进入“底层优化”的开始性能竞争的对象已经不是代码。而是CPU cache。