一、问题背景一次“看起来很正确”的优化几年前我在做一个基于 DPDK 的用户态网关。系统核心路径并不复杂RX ↓ Parser ↓ ACL ↓ NAT ↓ TX最开始系统采用的是memcpy()方式处理部分报文。例如VLAN 修改GTP-U decapHeader rewriteNAT 改写由于涉及packet rebuild因此部分场景需要重新组织报文。这时候团队里有人提出为什么不做零拷贝理由看起来非常合理memcpy 很耗 CPU零拷贝更先进Linux Kernel 也在零拷贝DPDK 本来就是高性能框架于是系统开始大量引入rte_pktmbuf_clone()indirect mbufexternal bufferscatter/gatherchained mbuf大家觉得性能一定会提升结果灾难开始了。二、系统出现了“越零拷贝越慢”最开始只是CPU utilization 上升后来问题越来越严重PPS 下降latency 抖动LLC miss 暴涨TX queue recycle 变慢某些 core 时不时 100%更奇怪的是memcpy 已经明显减少按理说CPU 应该下降才对。但实际上系统反而更慢了。三、为什么“少 memcpy”不一定意味着更快很多人理解性能优化会本能认为memcpy 性能杀手但实际上现代 CPU真正怕的并不是计算而是随机内存访问也就是说一次 cache miss可能比几十字节 memcpy代价更高。这是现代 CPU 微架构一个非常重要的事实。四、现代 CPU真正昂贵的是 Memory Stall后来我们使用perf stat分析系统。发现CPU cycles 大量消耗在stalled-cycles-backend也就是说CPU 并不是在执行 memcpy。而是在等待内存这时候问题方向彻底变了。五、DPDK mbuf 真正的本质很多人把 mbuf 理解成一个 packet buffer其实不准确。真正的 mbuf 本质是packet metadata data pointer结构类似struct rte_mbuf { void *buf_addr; uint16_t data_off; uint16_t data_len; uint32_t pkt_len; uint16_t refcnt; };注意真正的数据通常并不在 mbuf 本身。而在buf_addr 指向的 data buffer因此。所谓零拷贝本质上是共享 data buffer六、clone 真正做了什么例如rte_pktmbuf_clone()很多人以为clone 很轻量。实际上它做的是多个 mbuf 共享同一块 data buffer于是原来的独占 ownership变成共享 ownership于是问题开始出现。七、第一个隐藏问题引用计数一旦 clone系统就必须维护refcnt例如rte_mbuf_refcnt_update()问题在于refcnt本质上是atomic operation例如__atomic_fetch_add()而 atomic 在高频场景代价非常大。因为它会导致cache line lockpipeline stallmemory ordering barrier尤其多核场景。问题更严重。八、为什么 atomic 在 DPDK 中特别贵因为DPDK 本质是千万 PPS 高频循环例如20Mpps意味着每秒两千万次 refcnt 更新于是CPU 会持续争夺 cache line ownership最终大量 cycles 消耗在MESI cache coherency而不是业务逻辑。九、第二个隐藏问题Cache Locality 崩溃后来我们发现使用 clone 后cache miss 明显增加。为什么因为多个 core 开始共享同一个 data buffer于是packet data 会在多个 core 之间来回迁移 cache line这会导致cache line bouncing最终CPU 大量时间消耗在cache coherency traffic十、为什么 memcpy 有时候反而更快这是很多人最难理解的地方。因为memcpy 虽然复制数据。但它带来了独占 ownership例如core A memcpy 后新的 packet data 完全属于core A于是后续处理 cache locality 极好。而 clone虽然不复制数据。但会导致共享 cache line最终cache coherency 开销可能远超 memcpy。十一、第三个问题Multi-Segment Packet后来为了进一步减少 copy。系统开始大量使用chained mbuf即一个 packet 由多个 segment 组成。例如mbuf1 - mbuf2 - mbuf3看起来这非常灵活。但实际上现代 CPU 极其讨厌pointer chasing因为每次next pointer都可能cache miss十二、为什么链式 mbuf 会严重影响 Prefetch现代 CPU 非常依赖顺序访问这样hardware prefetcher 才能工作。但 chained mbuf 本质是随机跳转于是prefetch 完全失效。最终pipeline stall 暴涨。十三、第四个问题TX Recycle 开始失控后来系统又出现TX descriptor recycle 变慢原因NIC 无法及时释放shared buffer因为某些 clone packet refcnt 仍然不为 0。于是mbuf 无法回收。进一步导致mempool cache miss最终系统进入mempool starvation十四、为什么零拷贝系统更容易出现 Tail Latency因为零拷贝通常意味着共享生命周期而共享生命周期天然容易导致refcnt stallrecycle delaycache bouncingatomic contention于是平均 PPS 可能很好。但P99 latency会明显恶化。十五、真正的问题现代 CPU 已经不是“算力瓶颈”很多人还停留在CPU 算不动时代。实际上现代 Xeon 真正瓶颈是Memory System包括cachememory orderingNUMALLCTLB因此。真正高性能的数据面的核心目标已经不是减少计算而是减少共享十六、后来我们怎么重构后来我们彻底推翻原方案。核心原则1. 默认允许 memcpy只要copy size 较小例如L2/L3 headermetadatatunnel header直接 copy。因为小 copy 远比 cache bouncing 便宜2. 避免 clone 跨核原则谁创建 谁释放避免shared ownership3. 尽量避免 chained mbuf优先contiguous packet因为CPU 喜欢顺序内存4. 优先优化 Cache Locality真正的优化目标不是zero-copy而是cache-hot5. 避免频繁 atomic尤其高 PPS 场景。atomic 往往比memcpy 更贵十七、为什么 Linux Kernel 还能大量使用零拷贝因为Linux Kernel 优化目标通常是通用性例如大文件传输TCP streamsendfilesplice这些场景packet lifetime 较长。并且更偏throughput-oriented而 DPDK 尤其小包场景更偏ultra-low latency两者优化目标完全不同。十八、真正的大规模 DPDK 优化本质已经是 Cache Engineering很多人以为DPDK 优化是在优化网络实际上。真正的大规模数据面优化最后都在优化cache localitymemory ownershipNUMAmemory orderingprefetch behaviorpipeline stall也就是说现代 DPDK 性能优化本质已经进入CPU 微架构领域十九、总结很多 DPDK 系统为了追求零拷贝开始大量使用cloneindirect mbufchained mbufshared buffer结果系统反而CPU 更高latency 更差cache miss 暴涨P99 抖动recycle 延迟根本原因并不是DPDK 不够快而是共享 ownership 破坏了 cache locality真正优秀的数据面系统最终优化的已经不是copy 次数而是CPU cache 与内存行为而这才是现代高性能 DPDK 系统最核心的本质。