一、一次"架构升级"带来的性能灾难
某运营商核心交换机项目已经稳定运行两年。
系统采用的是典型的流水线架构。
RX Thread
│
▼
Parser Thread
│
▼
ACL Thread
│
▼
Forward Thread
│
▼
TX Thread
每一级之间,都通过DPDK提供的rte_ring进行通信。
项目上线初期效果很好。
随着业务增加,团队不断向流水线中增加新的处理模块:
- VLAN处理
- ACL
- QoS
- NAT
- Telemetry
- Flow Statistics
- 镜像采集
整个系统逐渐演变成:
RX
│
▼
Parser
│
▼
Session Lookup
│
▼
ACL
│
▼
QoS
│
▼
Statistics
│
▼
Forward
│
▼
TX
每两个模块之间,都新增一个Ring。
上线半年后。
运营商反馈:
新版本吞吐能力下降约35%。
更加奇怪的是:
所有传统性能指标都没有任何异常。
| 指标 | 状态 |
|---|---|
| CPU | 100% |
| RX Drop | 0 |
| TX Drop | 0 |
| NIC Error | 0 |
| ACL耗时 | 正常 |
| Hash耗时 | 正常 |
| NUMA | 正常 |
| Hugepage | 正常 |
整个系统没有任何异常。
唯一的问题:
就是越来越慢。
二、第一轮排查:是不是算法变复杂了?
首先怀疑ACL。
Perf热点统计:
| 函数 | CPU占比 |
|---|---|
| rte_acl_classify | 28% |
| rte_hash_lookup | 11% |
| Parser | 16% |
| QoS | 9% |
| NAT | 13% |
| 其它 | 23% |
和上一版本相比。
几乎没有变化。
说明:
算法没有退化。
继续查看:
Cache Miss。
Cache Miss
4.8%
正常。
NUMA:正常。
Hugepage:正常。
NIC:正常。
RX Queue:没有积压。
所有能想到的问题全部排除了。
三、真正奇怪的是:CPU一直很忙,却没有干多少活
工程师继续使用:
perf stat
统计CPU事件。
结果发现:
Instructions
下降18%
但是:
CPU Utilization
100%
继续观察:
Cycles
几乎没有变化
也就是说:CPU仍然一直在运行。
但是:真正执行的指令却变少了。
CPU到底在忙什么?
继续分析:
perf record
热点开始出现变化。
除了业务函数。
开始出现大量:
rte_ring_enqueue_bulk()
rte_ring_dequeue_bulk()
所有人都觉得奇怪。
Ring不是:
Lock-Free。
为什么还能成为热点?
四、Lock-Free,并不代表没有代价
很多刚接触DPDK的人都会认为:
既然官方提供:
rte_ring
那么:它一定是最快的。
事实上。Lock-Free只是没有互斥锁(Mutex),并不意味着没有同步成本。
这是很多性能问题产生的根源。
为了理解这一点。
先看看Ring到底是什么。
五、rte_ring到底是什么?



rte_ring本质上就是一个环形队列。
内部维护四个核心变量:
prod_head
prod_tail
cons_head
cons_tail
假设:
Ring大小:
1024
Producer不断:
prod_head++
↓
写入对象
↓
prod_tail++
Consumer不断:
cons_head++
↓
读取对象
↓
cons_tail++
整个过程没有任何Mutex。
因此。
很多人认为:
没有锁。
性能一定很好。
实际上。
真正耗时的地方。
恰恰就在:
head
tail
这几个变量。
六、为什么Head/Tail会成为性能瓶颈?



继续看Producer。
多个Producer同时写Ring。
例如:
CPU0准备:
prod_head
100
↓
101
与此同时。
CPU1也准备:
prod_head
100
↓
101
如果:没有保护。Ring立即损坏。
因此。DPDK必须保证:只有一个CPU能够成功修改Head。
于是。源码中大量使用:
rte_atomic_compare_exchange()
或者
CAS
也就是:Compare-And-Swap。
很多人看到:CAS。
第一反应:
很快。
实际上。CAS并不是普通CPU指令。
CAS意味着:整个Cache Line。
需要获得:Exclusive Ownership。
也就是说:CPU准备修改:
prod_head
之前。
必须先让其它CPU全部失效。
如果:Producer很多。
那么:同一条Cache Line。
就在:
CPU0
↓
CPU1
↓
CPU2
↓
CPU3
之间不断迁移。
CPU真正等待的不是CAS。
而是:Cache一致性协议。
七、为什么Ring越多,性能反而越差?
很多项目会认为:模块越细越容易扩展。
于是:
RX
↓
Ring
↓
Parser
↓
Ring
↓
Session
↓
Ring
↓
ACL
↓
Ring
↓
QoS
↓
Ring
↓
TX
看起来每个模块都很独立。
实际上,每增加一级。都会新增一次:
Enqueue
+
Dequeue
意味着:每个数据包至少增加两次:
- CAS
- Memory Barrier
- Cache同步
如果流水线六级。
那么每个包可能经历:
12次
Head/Tail修改
注意。
这里只是:数据包传递。
真正业务处理还没有开始。
很多团队发现:业务模块越拆越细,CPU越来越高,吞吐却越来越低。
原因就在这里。
Ring的成本不是固定的,而是随着模块数量呈线性增长;当数据包处理本身已经很轻量时,线程之间传递数据的成本反而会超过真正的业务逻辑。
八、一个容易被忽略的问题:Memory Barrier
除了CAS之外。
rte_ring还有另一个隐藏成本。
就是:Memory Barrier(内存屏障)。
为了保证:Producer写入对象以后,Consumer一定能够看到。
CPU不能:随意重排序。
因此。每一次:
enqueue
dequeue
都会伴随Acquire、Release、Fence。
这些操作不会增加CPU利用率。
但是会降低流水线并行能力。
很多Perf里面:Backend Stall实际上就来自这里。
(未完待续)