一、一次"性能优化"引发的现网投诉
某运营商5G核心网UPF已经稳定运行近一年。
整个系统采用典型的DPDK Run-to-Completion模型。
每个Worker线程固定绑定一个CPU核心,负责:
- RX收包
- GTP-U解析
- Session查找
- PDR/FAR处理
- NAT
- 封装
- TX发送
业务长期保持稳定。
为了进一步提升吞吐,研发团队对数据面进行了一个看似非常合理的优化。
原来的代码:
#define MAX_PKT_BURST 32 nb_rx = rte_eth_rx_burst(port_id, queue_id, rx_pkts, MAX_PKT_BURST);修改为:
#define MAX_PKT_BURST 128 nb_rx = rte_eth_rx_burst(port_id, queue_id, rx_pkts, MAX_PKT_BURST);优化理由十分充分:
- 一次收更多包;
- 减少
rte_eth_rx_burst()调用次数; - 减少循环判断;
- 提高CPU执行效率;
- 增加Cache命中率。
实验室测试结果也符合预期。
| 指标 | 修改前 | 修改后 |
|---|---|---|
| PPS | 64.8 Mpps | 66.3 Mpps |
| 吞吐 | 91.8 Gbps | 93.6 Gbps |
| CPU | 100% | 100% |
大家一致认为:
这是一次成功的优化。
版本顺利上线。
二、吞吐提升了,投诉却来了
上线后的第二天。
运营商网络维护中心开始收到业务告警。
奇怪的是。
投诉内容并不是:
- 网络中断;
- 丢包;
- 吞吐不足。
而是:
VoNR偶尔出现卡顿。
与此同时:
HTTPS页面偶尔需要等待一两秒。
数据库短连接偶尔超时。
TCP重传率没有明显变化。
UDP没有丢包。
系统所有传统指标几乎全部正常。
例如:
| 指标 | 状态 |
|---|---|
| CPU | 100% |
| RX Drop | 0 |
| TX Drop | 0 |
| NIC Error | 0 |
| PMD线程 | 正常 |
| HugePage | 正常 |
| NUMA | 正常 |
甚至:
整体吞吐相比以前还提高了约2%。
所有人都开始困惑。
为什么性能更高了,用户体验反而更差了?
三、真正异常的是P99.99
团队开始查看更细粒度的监控。
首先:平均时延。
修改前: 18 us 修改后: 19 us几乎没有变化。
继续:
P95:
修改前: 24 us 修改后: 26 us依旧正常。
然后:
P99:
修改前: 38 us 修改后: 165 us开始明显增加。
继续:
P99.9:
修改前: 62 us 修改后: 740 us所有人开始紧张。
最后:
P99.99:
修改前: 118 us 修改后: 7.6 ms整个会议室安静了。
原来真正的问题并不是:平均时延。
而是:Tail Latency(长尾时延)。
绝大多数包仍然处理得很快。
只有极少部分包等待了几毫秒。
对于网页影响不明显。
但是对于VoNR一两个包延迟几毫秒已经足够产生卡顿。
四、是不是CPU已经处理不过来了?
看到时延增加。
第一反应自然还是CPU。
于是继续Perf。
热点统计:
| 函数 | CPU占比 |
|---|---|
| Parser | 17% |
| Session Lookup | 13% |
| PDR/FAR | 26% |
| NAT | 11% |
| TX | 18% |
| 其它 | 15% |
和升级之前。
几乎完全一致。
继续:
Cache Miss:
4.1%正常。
Backend Stall:
18%正常。
NUMA:正常。
Hugepage:正常。
PCIe:正常。
网卡:正常。
整个系统没有任何性能瓶颈。
于是问题再次陷入僵局。
五、直到有人提出一个问题
会议持续了几个小时。
大家不停排查:
- NIC;
- CPU;
- ACL;
- Session;
- NAT。
最后。
一位老工程师问了一句:
我们最近是不是改过Burst大小?
所有人愣了一下。
因为:Burst一直被认为只是:"一次多收几个包。"
怎么可能影响时延?
事实上,真正的问题。
正是:Burst。
六、为什么DPDK一定要Burst?
很多刚学习DPDK的人都会问:
为什么Linux:
recv()一次一个包。
DPDK却一定要:
rte_eth_rx_burst()一次几十个包。
这是因为:现代CPU真正昂贵的不是计算。
而是:进入一次数据处理流程。
例如:
CPU一次执行:
读取RX Tail ↓ 读取Descriptor ↓ DMA同步 ↓ 更新Head ↓ 返回如果每个包都执行一次。
那么CPU的大量时间都会消耗在循环、函数调用、分支预测、Doorbell访问、Descriptor更新。
而不是真正处理数据包。
因此,DPDK选择批处理。
例如:
一次:
32个包整个流程只执行一次。
CPU效率立即提升。
这就是Burst存在的意义。
七、Burst真正优化了什么?
很多人认为:
Burst只是减少函数调用。
实际上远不止如此。
一个RX Burst实际上完成了很多工作:
NIC Descriptor Ring ↓ 检查DD Bit ↓ 批量读取Descriptor ↓ DMA同步 ↓ 更新RX Tail ↓ 返回mbuf数组注意:
这里:Descriptor本身就是顺序存放。
CPU读取32个。几乎全部命中L1/L2 Cache。
PCIe DMA也是连续访问。
因此:
Burst不仅减少函数调用。
更重要的是:提升:
- Descriptor访问效率;
- Cache局部性;
- DMA连续性;
- 指令流水线利用率。
这也是为什么:几乎所有Intel官方Sample。
默认:
#define MAX_PKT_BURST 32而不是:1。
八、那为什么128反而会增加时延?
到这里。
看起来:Burst越大应该越快。
为什么:128却导致P99.99增加几十倍?
答案藏在一个很多DPDK开发者容易忽略的现象中:
第一个包,必须等待最后一个包。
假设一次收到128个包。
Worker不会收到一个处理一个。
而是:
RX Burst(128) ↓ for(i=0;i<128;i++) ↓ 全部处理完成 ↓ TX Burst于是。
第1个包虽然最早到达,却必须等待后面127个包全部完成才能一起发送。
这种等待就是后面要分析的:Head-of-Line Blocking(队首阻塞)。
它也是本次事故真正的根源。
(未完待续)