100G交换机吞吐却下降了15%——一次DPDK Cache Line False Sharing故障深度分析

一、令人困惑的性能下降

我们负责维护一套数据中心核心交换机。

交换机采用纯DPDK用户态转发架构,承担园区汇聚和数据中心东西向流量交换。

硬件配置如下:

  • CPU:双路 Intel Xeon Gold
  • 网卡:双100GbE Intel Ethernet Controller
  • DPDK:23.11 LTS
  • 每端口8个RX Queue
  • 每个Queue绑定一个PMD Worker
  • 单机转发能力约160 Mpps

系统已经稳定运行近一年。

一次版本升级后,测试团队反馈:

相同硬件、相同业务模型,交换机吞吐下降约15%,P99时延明显上升。

更奇怪的是:

所有监控指标几乎全部正常。

指标状态
PMD CPU100%
RSS均衡
RX Queue正常
TX Queue正常
RX Drop0
RX Missed0
LinkUp

这意味着:

既不是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:

WorkerPPS
Worker019.8 Mpps
Worker119.6 Mpps
Worker219.7 Mpps
Worker319.9 Mpps
Worker419.7 Mpps
Worker519.8 Mpps
Worker619.6 Mpps
Worker719.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%略增
IPC1.891.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;

重新压测:

结果如下:

指标优化前优化后
PPS136 Mpps158 Mpps
P99延迟12.8 μs6.3 μs
IPC1.531.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微架构的运行方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.HeBoYan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值