为什么Burst越大,时延反而越高?——一次P99.99时延飙升事故,彻底理解DPDK Burst处理模型(上)
2026/7/9 3:51:40 网站建设 项目流程

一、一次"性能优化"引发的现网投诉

某运营商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命中率。

实验室测试结果也符合预期。

指标修改前修改后
PPS64.8 Mpps66.3 Mpps
吞吐91.8 Gbps93.6 Gbps
CPU100%100%

大家一致认为:

这是一次成功的优化。

版本顺利上线。


二、吞吐提升了,投诉却来了

上线后的第二天。

运营商网络维护中心开始收到业务告警。

奇怪的是。

投诉内容并不是:

  • 网络中断;
  • 丢包;
  • 吞吐不足。

而是:

VoNR偶尔出现卡顿。

与此同时:

HTTPS页面偶尔需要等待一两秒。

数据库短连接偶尔超时。

TCP重传率没有明显变化。

UDP没有丢包。

系统所有传统指标几乎全部正常。

例如:

指标状态
CPU100%
RX Drop0
TX Drop0
NIC Error0
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占比
Parser17%
Session Lookup13%
PDR/FAR26%
NAT11%
TX18%
其它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(队首阻塞)

它也是本次事故真正的根源。


(未完待续)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询