Linux的网络瓶颈
以Linux为例,传统网络设备驱动包处理的动作可以概括如下:
- 数据包到达网卡设备。
- 网卡设备依据配置进行DMA操作。
- 网卡发送中断,唤醒处理器。
- 驱动软件填充读写缓冲区数据结构。
- 数据报文达到内核协议栈,进行高层处理。
- 如果最终应用在用户态,数据从内核搬移到用户态。
- 如果最终应用在内核态,在内核继续进行。
随着网络接口带宽从千兆向万兆迈进,原先每个报文就会触发一个中断,中断带来的开销变得突出,大量数据到来会触发频繁的中断开销,导致系统无法承受,因此有人在Linux内核中引入了NAPI机制(New API)。
在Linux系统中,网络数据包进入计算机后,通常需要经过协议处理(如TCP/IP协议栈)。即使在某些场景下不需要进行协议处理,数据包仍然需要从内核缓冲区复制到用户缓冲区。
将数据包从内核缓冲区复制到用户缓冲区涉及到系统调用和数据拷贝操作。这些操作会消耗CPU资源和时间,影响用户态应用程序从网络设备直接获取数据包的能力。
同时,对于某些网络功能节点(如数据转发节点),并不一定需要完整的TCP/IP协议栈。
Linux中有著名的高性能网络I/O框架Netmap,其减少了内核到用户空间的包复制。
NAPI机制
NAPI机制是一种 “中断 + 轮训” 收包机制。
NAPI 允许网络适配器在短时间内接收多个数据包,并将它们放入一个环形缓冲区中,然后产生一次中断通知CPU。这样可以减少中断的频率,提高系统的处理效率。
当网络适配器进入 NAPI 模式时,它将暂时停止产生中断,直到有足够多的数据包积累在缓冲区中,或者经过一段时间后再产生中断。这样可以减少处理每个数据包所需的中断处理次数。
当CPU处理接收到的数据包时,它会进入软中断上下文,在这个上下文中,CPU可以高效地处理多个网络数据包,从而进一步减少了从用户空间到内核空间的切换开销。
Netmap
Netmap 使用一个共享的环形缓冲区(即数据包池),在内核空间和用户空间之间共享。网络设备的数据包可以直接在内核空间的数据包池中进行处理,而无需立即复制到用户空间。
当用户空间需要处理数据包时,它可以通过映射共享的数据包池来直接访问数据包,而无需进行额外的数据复制操作。这种零拷贝技术显著降低了处理每个数据包时的CPU和内存开销。
Netmap 允许用户空间应用程序通过轮询方式高效地获取和处理数据包,而不是依赖于传统的中断驱动方式。这种方式在高速网络环境中可以显著提高数据包的处理效率和吞吐量。
Netmap没广泛使用。其原因有几个:
-
Netmap需要驱动的支持,即需要网卡厂商认可这个方案。
-
Netmap仍然依赖中断通知机制,没完全解决瓶颈。
-
Netmap更像是几个系统调用,实现用户态直接收发包,功能太过原始,没形成依赖的网络开发框架,社区不完善。
DPDK的基本原理
左边是原来的方式数据从 网卡 -> 驱动 -> 协议栈 -> Socket接口 -> 业务
右边是DPDK的方式,基于UIO(Userspace I/O)旁路数据。数据从 网卡 -> DPDK轮询模式-> DPDK基础库 -> 业务
用户态的好处是易用开发和维护,灵活性好。并且Crash也不影响内核运行,鲁棒性强。
DPDK支持的CPU体系架构:x86、ARM、PowerPC(PPC)
UIO机制
为了让驱动运行在用户态,Linux提供UIO机制。使用UIO可以通过read感知中断,通过mmap实现和网卡的通讯。
UIO(Userspace I/O)是运行在用户空间的I/O技术。Linux系统中一般的驱动设备都是运行在内核空间,而在用户空间用应用程序调用即可,而UIO则是将驱动的很少一部分运行在内核空间,而在用户空间实现驱动的绝大多数功能
核心优化 PMD
DPDK的UIO驱动屏蔽了硬件发出中断,然后在用户态采用主动轮询的方式,这种模式被称为PMD(Poll Mode Driver)。
UIO旁路了内核,主动轮询去掉硬中断,DPDK从而可以在用户态做收发包处理。带来Zero Copy、无系统调用的好处,同步处理减少上下文切换带来的Cache Miss。
相对于linux系统传统中断方式,Intel DPDK避免了中断处理、上下文切换、系统调用、数据复制带来的性能上的消耗,大大提升了数据包的处理性能。
运行在PMD的Core会处于用户态CPU100%的状态,网络空闲时CPU长期空转,会带来能耗问题。所以,DPDK推出Interrupt DPDK模式。
Interrupt DPDK模式可以使用中断模式驱动程序,并且可以和其他进程共享同个CPU Core,但是DPDK进程会有更高调度优先级。
大页内存
Linux操作系统通过查找TLB来实现快速的虚拟地址到物理地址的转化。由于TLB是一块高速缓冲cache,容量比较小,容易发生没有命中。当没有命中的时候,会触发一个中断,然后会访问内存来刷新页表,这样会造成比较大的时延,降低性能。Linux操作系统的页大小只有4K,所以当应用程序占用的内存比较大的时候,会需要较多的页表,开销比较大,而且容易造成未命中。
相比于linux系统的4KB页,Intel DPDK缓冲区管理库提供了Hugepage大页内存,大小有2MB和1GB页面两种,可以得到明显性能的提升,,这样就减少了虚拟页地址到物理页地址的转换时间,也减少了TLB-Miss。
CPU亲和性
随着CPU的核心的数目的增长,Linux的核心间的调度和共享内存争用会严重影响性能。利用Intel DPDK的CPU affinity可以将各个线程绑定到不同的cpu,可以省去来回反复调度带来的性能上的消耗。
在一个多核处理器的机器上,每个CPU核心本身都存在自己的缓存,缓冲区里存放着线程使用的信息。如果线程没有绑定CPU核,那么线程可能被Linux系统调度到其他的CPU上,这样的话,CPU的cache命中率就降低了。
利用CPU的affinity技术,一旦线程绑定到某个CPU后,线程就会一直在指定的CPU上运行,操作系统不会将其调度到其他的CPU上,节省了调度的性能消耗,从而提升了程序执行的效率。
软件调优
比如说有以下几种代码实践:
- 结构的cache line对齐
- 数据在多核间访问避免跨cache line共享
- 适时地预取数据
- 多元数据批量操作
- 使用CPU指令直接操作
- 分支预测,预先做指令读取
DPDK 核心组件
环境抽象层(EAL):为DPDK其他组件和应用程序提供一个屏蔽具体平台特性的统一接口,环境抽象层提供的功能主要有:DPDK加载和启动;支持多核和多线程执行类型;CPU核亲和性处理;原子操作和锁操作接口;时钟参考;PCI总线访问接口;跟踪和调试接口;CPU特性采集接口;中断和告警接口等。
堆内存管理组件(Malloc lib):堆内存管理组件为应用程序提供从大页内存分配对内存的接口。当需要分配大量内存小块时,使用这些接口可以减少TLB缺页。
环缓冲区管理组件(Ring lib):环缓冲区管理组件为应用程序和其他组件提供一个无锁的多生产者多消费者FIFO队列API:Ring。Ring是借鉴了Linux内核kfifo无锁队列,可以无锁出入对,支持多消费/生产者同时出入队。
内存池管理组件(Mem pool lib):为应用程序和其他组件提供分配内存池的接口,内存池是一个由固定大小的多个内存块组成的内存容器,可用于存储相同对象实体,如报文缓存块等。内存池由内存池的名称来唯一标识,它由一个环缓冲区和一组核本地缓存队列组成,每个核从自己的缓存队列分配内存块,当本地缓存队列减少到一定程度时,从内存缓冲区中申请内存块来补充本地队列。
网络报文缓存块管理组件(Mbuf lib):提供应用程序创建和释放用于存储报文信息的缓存块的接口,这些MBUF存储在内存池中。提供两种类型的MBUF,一种用于存储一般信息,一种用于存储报文信息。
定时器组件(Timer lib):提供一些异步周期执行的接口(也可以只执行一次),可以指定某个函数在规定的时间异步的执行,就像LIBC中的timer定时器,但是这里的定时器需要应用程序在主循环中周期调用rte_timer_manage来使定时器得到执行。定时器组件的时间参考来自EAL层提供的时间接口。
DPDK的运行形式
大部分DPDK的代码是以lib的形式运行在用户应用的进程上下文,为了达到更高的性能。应用通常都会多进程或者多线程的形式运行在不同的lcore上
多进程的场景下,为了保证关键信息(比如内存资源)的一致性, 不同进程会把公共的数据mmap同一个文件,这样任何一个进程对数据的修改都可以影响到其他进程。