一、令人困惑的性能下降
我们负责维护一套数据中心核心交换机。
交换机采用纯DPDK用户态转发架构,承担园区汇聚和数据中心东西向流量交换。
硬件配置如下:
- CPU:双路 Intel Xeon Gold
- 网卡:双100GbE Intel Ethernet Controller
- DPDK:23.11 LTS
- 每端口8个RX Queue
- 每个Queue绑定一个PMD Worker
- 单机转发能力约160 Mpps
系统已经稳定运行近一年。
一次版本升级后,测试团队反馈:
相同硬件、相同业务模型,交换机吞吐下降约15%,P99时延明显上升。
更奇怪的是:
所有监控指标几乎全部正常。
| 指标 | 状态 |
|---|---|
| PMD CPU | 100% |
| RSS | 均衡 |
| RX Queue | 正常 |
| TX Queue | 正常 |
| RX Drop | 0 |
| RX Missed | 0 |
| Link | Up |
这意味着:
既不是CPU算力不足,也不是网卡丢包。
真正的问题,一定隐藏在更深层。
二、第一轮排查:怀疑网卡
性能下降,第一反应自然是检查网卡。
首先读取DPDK统计:
struct rte_eth_stats stats;
rte_eth_stats_get(port_id, &stats);
printf("imissed=%" PRIu64 "\n", stats.imissed);
printf("ierrors=%" PRIu64 "\n", stats.ierrors);
printf("rx_nombuf=%" PRIu64 "\n", stats.rx_nombuf);
结果如下:
imissed = 0
ierrors = 0
rx_nombuf = 0
随后查看驱动统计:
ethtool -S eth0
重点关注:
- CRC Error
- RX FIFO Error
- DMA Error
- RX Miss
所有统计均为0。
说明:
网卡工作完全正常。
核心知识点一:DPDK中的"没有错误"并不代表"没有性能问题"
很多开发者习惯把性能问题与错误计数关联。
事实上,在DPDK中,大量性能退化都不会表现为:
- CRC错误
- DMA错误
- RX Miss
- Link Flap
因为真正限制系统性能的,很可能是CPU内部的数据访问效率,而不是网卡收发能力。
换句话说:
性能问题不一定伴随错误日志。
三、第二轮排查:RSS是否失衡?
高性能交换机最常见的问题之一,就是RSS分布不均。
如果某一个PMD线程承担了绝大多数流量,很容易形成热点。
于是统计每个Worker:
| Worker | PPS |
|---|---|
| Worker0 | 19.8 Mpps |
| Worker1 | 19.6 Mpps |
| Worker2 | 19.7 Mpps |
| Worker3 | 19.9 Mpps |
| Worker4 | 19.7 Mpps |
| Worker5 | 19.8 Mpps |
| Worker6 | 19.6 Mpps |
| Worker7 | 19.8 Mpps |
可以看到:
负载非常均衡。
因此可以排除:
- RSS Hash异常
- Queue倾斜
- Worker热点
核心知识点二:CPU 100%只是Polling模型的正常状态
很多刚接触DPDK的人都会问:
CPU已经100%了,是不是CPU已经跑满?
答案是否定的。
PMD线程采用Busy Poll模型:
while (1) {
rte_eth_rx_burst(...);
process_packets(...);
rte_eth_tx_burst(...);
}
无论有没有数据包:
CPU都会持续轮询。
因此:
CPU长期100%,只是说明线程没有休眠。
真正需要关注的是:
- 每Packet消耗多少Cycle
- Cache命中率
- IPC(Instructions Per Cycle)
- Stall Cycle比例
四、第三轮排查:Perf开始暴露异常
既然网卡没有问题。
开始使用Linux perf观察CPU行为:
perf stat \
-e cycles,instructions,cache-references,cache-misses \
-p <PMD_PID>
压测一分钟后:
得到如下统计:
| 指标 | 正常版本 | 异常版本 |
|---|---|---|
| Instructions | 基本一致 | 基本一致 |
| Cycles | +18% | ↑ |
| Cache Miss | +4% | 略增 |
| IPC | 1.89 | 1.53 |
看到这里。
团队第一反应:
是不是Cache Miss导致?
继续深入。
五、真正奇怪的数据
继续增加PMU事件:
perf stat \
-e mem_load_retired.l3_miss,\
mem_load_retired.l2_miss,\
cache-misses,\
cycles
结果却出乎意料:
L2 Miss:
变化很小。
L3 Miss:
变化很小。
Cache Miss总体:
并没有明显增加。
但是:
Cycles却持续增加。
也就是说:
CPU花费了更多时间。
却不是因为缓存没有命中。
那么:
CPU到底在等待什么?
核心知识点三:Cache Miss并不是唯一导致性能下降的原因
很多人认为:
CPU慢,
一定是Cache Miss。
实际上。
还有另一类更加隐蔽的问题:
Cache Line虽然命中了,却因为Cache一致性协议不断失效。
CPU不是在等待数据从内存回来。
而是在等待:
另一个CPU核心,把Cache Line的所有权交出来。
这种等待。
普通Cache Miss统计很难直接体现。
也是很多DPDK性能问题最容易被忽略的地方。
六、问题开始指向源码
既然:
- 网卡正常;
- RSS正常;
- Queue正常;
- Cache Miss变化不明显;
那么:
问题只能来自软件自身。
开始比较两个版本源码。
经过git diff。
终于发现:
新版本新增了一段统计代码:
struct worker_statistics {
uint64_t rx_packets;
uint64_t tx_packets;
uint64_t rx_bytes;
uint64_t tx_bytes;
};
static struct worker_statistics stats[MAX_WORKERS];
每个Worker处理完一个Burst后:
都会执行:
stats[worker_id].rx_packets += nb_rx;
stats[worker_id].tx_packets += nb_tx;
这段代码只有几行。
没有锁。
没有原子操作。
看起来几乎不可能影响性能。
然而。
真正的答案,
恰恰就隐藏在这里……
七、真正的元凶:Cache Line False Sharing
先来看这段代码:
struct worker_statistics {
uint64_t rx_packets;
uint64_t tx_packets;
uint64_t rx_bytes;
uint64_t tx_bytes;
};
static struct worker_statistics stats[MAX_WORKERS];
假设:
worker0 → stats[0]
worker1 → stats[1]
worker2 → stats[2]
……
很多开发者认为:
每个Worker只修改自己的统计信息,互不干扰。
实际上,这个结论并不成立。
问题出在Cache Line。

现代Intel Xeon处理器的Cache Line大小通常为64 Bytes。
而上面的结构体大小:
4 × uint64_t = 32 Bytes
意味着:
Cache Line 0
+-------------------------+
| stats[0] (32 Bytes) |
+-------------------------+
| stats[1] (32 Bytes) |
+-------------------------+
共64 Bytes
于是:
Worker0修改:
stats[0].rx_packets++
Worker1修改:
stats[1].tx_packets++
虽然:
访问的是两个完全不同的变量。
但是:
它们位于同一个Cache Line。
这就是:
False Sharing(伪共享)。
八、为什么伪共享如此致命?
现代CPU不是直接访问内存。
而是:
Memory
↓
L3 Cache
↓
L2 Cache
↓
L1 Cache
↓
CPU Core
每个核心都有:
- 私有L1
- 私有L2
L3一般共享。
当Worker0修改:
stats[0]
CPU必须:
获得整个Cache Line的写权限。
MESI协议:
立即使:
其它CPU核心中的:
同一Cache Line:
失效。
随后:
Worker1又修改:
stats[1]
于是:
整个Cache Line:
再次迁移。
形成:
Core0
Modified
↓
Core1
Modified
↓
Core2
Modified
↓
Core0
不断循环。
这就是:
Cache Line Ping-Pong。

核心知识点四:False Sharing并不是共享变量
很多人第一次听到:
False Sharing。
都会误认为:
多个线程访问同一个变量。
事实上:
真正的问题是:
多个线程访问不同变量,但变量位于同一个Cache Line。
因此:
程序逻辑完全正确。
没有竞争。
没有锁。
性能却急剧下降。
九、为什么DPDK特别容易出现?
DPDK最大的特点:
就是:
多核并行。
例如:
RX Queue0 → Core0
RX Queue1 → Core1
RX Queue2 → Core2
所有PMD:
同时处理Packet。
如果:
统计信息。
状态机。
计数器。
Session Cache。
位于:
同一个Cache Line。
那么:
每秒:
几千万次:
Cache失效。
CPU绝大部分时间:
不是处理Packet。
而是在:
等待Cache Ownership。
十、Perf为什么没有明显Cache Miss?
Cache Miss并没有增加。
为什么性能还是下降?
原因在于:
False Sharing。
属于:
Cache Coherence Traffic。
不是:
Memory Miss。
数据:
已经:
在Cache里面。
但是:
Cache Line:
不断失效。
因此:
普通:
cache-misses
事件:
增加很少。
真正增加的是:
HITM
(Hit Modified)
也就是:
其它CPU:
拥有:
Modified Cache Line。
CPU必须:
等待:
远端CPU:
写回。
十一、如何真正定位?
Linux从5.x开始:
提供:
perf c2c
(Cache To Cache)
例如:
perf c2c record \
-p <pid>
perf c2c report
报告中:
可以看到:
Remote HITM
Local HITM
如果:
某个地址:
Remote HITM:
非常高。
基本可以判断:
存在:
False Sharing。
这也是Intel官方推荐定位Cache一致性问题的重要工具。
核心知识点五:perf c2c比cache-misses更重要
排查DPDK多核性能问题:
很多人:
第一时间:
看:
cache-misses
实际上:
更应该关注:
Remote HITM
Store Latency
Cache-to-Cache
因为:
真正限制:
多核性能的。
往往不是:
Cache Miss。
而是:
Cache一致性。
十二、DPDK为什么大量使用__rte_cache_aligned?
翻阅DPDK源码。
会发现:
大量核心结构:
都有:
struct worker {
...
} __rte_cache_aligned;
很多开发者认为:
只是:
为了:
"对齐更快。"
其实:
真正目的:
是:
避免False Sharing。
例如:
修改后:
struct worker_statistics {
uint64_t rx_packets;
uint64_t tx_packets;
uint64_t rx_bytes;
uint64_t tx_bytes;
} __rte_cache_aligned;
此时:
每个Worker:
独占:
一个Cache Line。
即使:
结构体:
只有:
32Bytes。
编译器:
仍然:
填充:
到64Bytes。
于是:
布局变成:
Cache Line0
stats0
----------------
Cache Line1
stats1
----------------
Cache Line2
stats2
再也不会:
互相干扰。
十三、优化后的结果
修改:
仅仅一行:
} __rte_cache_aligned;
重新压测:
结果如下:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| PPS | 136 Mpps | 158 Mpps |
| P99延迟 | 12.8 μs | 6.3 μs |
| IPC | 1.53 | 1.88 |
| Remote HITM | 很高 | 接近0 |
整个优化:
没有修改:
算法。
没有修改:
网卡。
没有修改:
DPDK配置。
仅仅:
避免:
False Sharing。
吞吐:
恢复。
十四、工程实践中的几个经验
除了统计变量之外,下列对象也容易发生False Sharing:
1. Session统计
session->packets++;
多个核心:
更新:
相邻Session。
容易共享:
Cache Line。
2. Ring状态
Producer。
Consumer。
Head。
Tail。
如果:
布局:
不合理。
容易:
发生:
一致性竞争。
3. Timer
多个CPU:
更新时间轮。
同样:
容易:
形成:
Cache Ping-Pong。
4. 全局统计
例如:
g_total_packets++;
这是:
DPDK性能优化中:
最常见:
也是:
最严重:
的问题之一。
通常应该:
采用:
Per-lcore Counter。
最后:
统一汇总。
核心知识点六:减少共享,比减少锁更重要
很多开发者:
第一时间:
想到:
去锁。
实际上:
在DPDK中:
真正重要的是:
尽量不要共享。
共享越少。
Cache一致性流量越低。
系统扩展性越好。
十五、全文总结
这次故障最大的迷惑性在于:
- CPU始终100%
- 网卡没有错误
- RSS完全均衡
- Queue没有拥塞
- Cache Miss几乎正常
如果只关注传统性能指标,很难找到真正原因。
最终问题并非出在DPDK收发流程,而是新增统计代码引入了典型的Cache Line False Sharing。多个PMD线程虽然没有访问同一个变量,却不断竞争同一个Cache Line的所有权,导致MESI协议频繁触发Cache一致性同步,CPU花费大量周期等待缓存所有权迁移,而不是处理数据包。
对于DPDK这类高度并行的软件,性能瓶颈早已不仅仅来自算法复杂度,更来自底层硬件微架构。理解Cache Line、MESI协议、Cache Coherence以及__rte_cache_aligned背后的设计思想,才能真正理解为什么DPDK源码中大量数据结构都进行了Cache Line对齐。
很多时候,一个看似无害的uint64_t++,就足以让一套100G交换机损失10%以上的吞吐能力。这也说明,高性能网络软件优化,不仅要懂协议和DPDK,更要理解现代CPU微架构的运行方式。
62

被折叠的 条评论
为什么被折叠?



