我早期做DPDK多核开发时,总有一个认知:只要没有锁,性能就一定很好。
因为:
- 没有 mutex
- 没有 spinlock
- 没有 rwlock
- 全部 lockless
理论上:应该扩展性极强。
但我曾经遇到一个非常诡异的问题:
程序架构完全无锁:
- 每核独立 RX queue
- per-core statistics
- 独立 mempool cache
- 无共享 ring
看起来已经非常“DPDK 化”。但性能却始终上不去。
更奇怪的是:随着 core 数增加:性能不仅没提升。反而下降。
例如:
2 核
8 Mpps4 核
11 Mpps8 核
10 MppsCPU 全部打满。但吞吐增长极差。
第一次遇到时,我怀疑过:
- NUMA
- mbuf cache
- RX 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 line
CPU cache 不会:
一次只加载:8 字节
而是:整块加载。
例如:
64-byte cache line五、问题就出在这里
虽然:
stats[0] stats[1]是不同变量。
但它们:可能位于:同一个 cache line。
六、于是发生什么
例如:
core0
更新:
stats[0].rx_pkts++core1
同时更新:
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 polling
cache line 抖动会被无限放大。
十一、为什么 core 越多性能反而越差
因为:参与竞争的 core 增多。cache coherence traffic 激增。
最终:CPU 大量时间浪费在:cache sync
而不是:真正处理包。
十二、如何确认 false sharing
perf 非常关键。
例如:
perf stat关注:
cache-misses
LLC-load-misses
snoop hits
remote 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。
十七、优化效果非常明显
优化前:
| Core | PPS |
|---|---|
| 2 | 8 Mpps |
| 4 | 11 Mpps |
| 8 | 10 Mpps |
优化后:
| Core | PPS |
|---|---|
| 2 | 8 Mpps |
| 4 | 15 Mpps |
| 8 | 28 Mpps |
扩展性完全恢复。
十八、一个更隐蔽的问题:ring head/tail
false sharing 不仅发生在 stats。
还经常发生在:
ring producer index
consumer index
queue state
flow counter
这些高频写变量。
十九、为什么 DPDK 到处都有 cache align
你会发现:DPDK 源码里大量:
__rte_cache_aligned以前很多人只是:“照着写”。
其实背后都是:避免 false sharing。
二十、进一步理解 cache friendly design
高性能程序优化:
很多时候已经不是:算法复杂度。
而是:cache topology。
包括:
cache line
NUMA
prefetch
memory locality
这些。
二十一、为什么 false sharing 特别难发现
因为:
没有锁
没有崩溃
没有错误日志
CPU 也很高
程序“看起来正常”。
只是:性能奇差。
二十二、一个经典误区
很多人认为:
无锁 = 高性能其实:真正昂贵的:不一定是锁。
而是:cache coherence。
二十三、进一步理解现代 CPU
现代多核 CPU:真正贵的操作:往往不是:
add mul branch而是:
跨核 cache 同步因为:
涉及:
- interconnect
- snoop
- invalidate
- memory ordering
二十四、为什么 DPDK 如此强调 per-core
DPDK 的设计哲学之一:per-core everything
即:
per-core mempool cache
per-core RX queue
per-core statistics
per-core flow
本质都是:避免共享。
二十五、这次排查真正学到什么
以前我以为:多核优化就是:避免锁。
后来才意识到:真正困难的是:避免 cache line 共享。
这也是为什么:
很多高性能程序:代码看起来很“浪费内存”。
因为:它们在用空间换 cache efficiency。
二十六、工程经验总结
DPDK 中:高频写变量:必须:
cache aligned
per-core
避免共享
尤其:统计计数器。
二十七、总结
为什么 DPDK 程序明明没有锁,却还是性能很差?
很多时候不是:
- 算法问题
- 网卡问题
- NUMA 问题
而是:false sharing。
通过这个问题,我们真正理解了:
核心概念
- cache line
- MESI
- cache coherence
- false sharing
- cache aligned
- per-core design
这也是高性能网络开发真正进入“底层优化”的开始:
性能竞争的对象,已经不是代码。
而是:CPU cache。