高性能网关产品数据面开发原则

昨天有幸听了一场K总的一场关于数据面软件开发原则的课程,颇有受益,在此记录一下。遗憾的是由于记错时间了,错过了前面的精彩内容。

历史背景

从历史看,高性能网络产品数据面开发经历了从专用硬件到通用硬件的过程,大致经历了ASIC–>NP–>MIPS—>Intel x86 COTS。最早是基于ASIC实现的,但ASIC存在一个比较大的问题即灵活性和可扩展性。NP是专门为网络设备处理网络流量而设计的处理器,其转发等算法和操作都进行了优化。相对于ASIC,它相对提高了灵活性,但它也受限于指令集。今年来,随着Intel DPDK的成熟,性能好、灵活性强,网关产品数据面开发也逐渐转为使用商用x86服务器平台。

概念与术语

对于网管产品开发,有几个基本术语需要注意:

  1. IMIX, 互联网典型包长度
  2. PPS = Packet per Second
  3. 10G线速
    • 10G = 10 * 1000 * 1000 * 1000 bits
    • 最小包长 = 64 + 12 + 8 = 84Bytes = 672 bits
    • 10G / 672bis = 14880952.38 = 14.88 Mpps
  4. CPP: cycles per packet, IPP: Instructions per packet, CPI: Cycles per Instruction, BPS = packet_size * pps

2G主频CPU处理10G网卡达到限速意味着2GHz/14.8Mbps = 134 clock cycles

现代CPU体系结构

现代CPU使用了一系列的先进技术来提升性能:

  1. 超标量体系 (SuperScalar)
  2. 流水线技术 (Pipelining)
  3. 高速缓存 L1 cache(L1d、L1i), L2 cache, L3 cache
  4. 乱序执行,将多条指令不按程序规定的顺序分开发送给各个部件来提高指令级并行能力(程序需要显示调用Memory Barrier指令做内存屏障)
  5. 分支预测(BranchPrediction):对程序的流程进行预测,然后读取其中过一个分支的指令。如此来提升CPU的运算速度。
  6. 支持各种扩展指令集:如SIMD指令、AES,SHA指令加解密算法优化等

高性能数据面的设计原则

1. 使用用户空间轮询

  1. 中断和系统调用都是需要较大开销的
  2. Run_to_completion vs Pipeline 应优先选择Run to completion,因为Pipeline会带来cache miss
  3. 轮询应结合绑核
  4. 对于空转损耗问题,可通过插入nanosleep缓解,但生产环境应该慎用

2. 使用RSS HW Queue

first

RSS(Receive Side Scaling)特性允许网卡支持多个Rx/Tx队列,在网卡侧对报文进行解析,获取IP地址、协议和端口五元组信息,然后通过配置的HASH函数根据五元组信息计算出HASH值分发给对应的CPU。对于某些更高级的Intel NIC,还支持Flow Director,某种程度上是可编程的高级RSS。

3. 使用零拷贝和LockFree技术

内存拷贝对性能会带来相当大的影响,为此要实现高性能,就需要尽量减少数据拷贝。锁也是影响系统可扩展性的关键之一,要尽量避免资源争用。VPP上的数据面和控制面的同步基于CAS原子操作实现。有时候dpdk ring非常好非常高效,如ipsec vpn场景。

4. 尽量提高Cache命中率

CPU上cache层次结构如下图所示:

2019-12-03-12-46-31

同内存不同,cache介质为SRAM,相较于DRAM昂贵,但速度快很多。为此,减少cache miss是提升性能的关键。对于cache这块还容易忽略的是iCache Miss常常被忽略,我们也需要关心代码布局,使用更小的代码尺寸。CPU需保证cache一致性,即对于指定内存数据,所有的处理器核心在任意时刻都应该得到相同的内容。现代CPU一般是通过MESI协议实现cache一致性,mesi协议如下图所示。为维护cache一致性,远程写请求会导致RequestForOwner操作,通知其他CPU将cache状态置为invalid状态。当一个软件线程被调度到不同CPU上运行时,cacheline会产生大量的RFO,这也是需要绑定CPU的原因。另外,多个CPU写同意cacheline内存区域,也会产生大量的RFO操作,因此,代码中需要避免伪共享问题。

5. 批量处理

轮询机制运行一次接受或发送多个报文,批量处理可以分摊接收或者发送操作本身的开销。对于计算部分,绝大部分报文需要做相同或者相似的计算处理,这意味着相同的指令会被反复地执行。报文的批量计算分摊了函数调用的上下文切换、堆栈的初始化等开销,同时大大减少了l1i cache miss。另外,批量计算提供了更好的代码优化的可能性(数据预取,多重循环等)。

代码优化技术

总体来说,代码优化的关键在于提升指令执行的并行度IPC。一般来说,有如下优化技术:

  1. Cache line对齐,减少dCache miss。另外需要避免伪共享问题
  2. 数据预取,减少dCache miss (prefetch指令)
  3. 分支预测,优化代码布局
    • 或者避免代码分支,以空间换时间
    • 或者使用buitin_expect
  4. 函数内联,减少函数调用开销,不过需要注意的一点即这可能导致更大的代码段问题
  5. 使用SIMD
  6. 使用多重循环优化处理报文,更好地优化CPU流水线
  7. 编译器优化

为更好地指导性能优化,Linux perf是一个非常好用的工具。对于数据面开发,需要关注的性能事件有cycles,branches,branch-miesses,cache-misses,instructions,L1-icache-load-misses,L1-dcache-load-misses,LLC-loads,LLC-load-misses,dTLB-loads,dTLB-load-misses,cache-misses。这些事件中,主要需要关注的有:

  1. Cycles or cpu-cycles
    • Counter for cpu clock cycle
    • 可以得到CPP
  2. Cache-misses
    • Most likely LLC misses
  3. Instructions
    • 除以Cycles可以得到IPC

传统网络协议栈 vs VPP

传统网络协议栈工作模式如下图所示:逐个报文计算处理,每个报文路径很长,可能多次iCache Miss。另外,很难设计合理的数据预取策略。

vpp工作模式如下:包处理过程分解为一个有向图节点,以向量的方式通过图节点,每个图节点都优化为尽量适应指令cache,包会通过预取指令预取到数据cache。

对于VPP开发,要注意不要写太简单的node,太简单node没有什么优化空间,无法达到足够强的指令并行度,另外代码应该向t1-ip4-input, t1-ip4-lookup,t1-ip4-rewrite代码学习。对于prefetch,也要尽量小心使用(有时候包长问题可能导致预取性能掉很多)。

讨论的几个问题

  1. 不要使用超线程
  2. 不要使用clock_gettime(),要使用rdtsc指令
  3. 尽可能对称处理,减少cache,避免pipeline模式,减少cache miss。
  4. 避免伪共享
  5. 控制面和数据面同步问题,尽量使用mpsafe数据结构和算法,如bhash vs hash
  6. CPU Turbo打开性能会更好,但是可能导致性能指标不稳定。