第1章

认识DPDK

什么是DPDK?对于用户来说,它可能是一个性能出色的包数据处理加速软件库;对于开发者来说,它可能是一个实践包处理新想法的创新工场;对于性能调优者来说,它可能又是一个绝佳的成果分享平台。当下火热的网络功能虚拟化,则将DPDK放在一个重要的基石位置。虽然很难用短短几语就勾勒出DPDK的完整轮廓,但随着认识的深入,我们相信你一定能够认可它传播的那些最佳实践方法,从而将这些理念带到更广泛的多核数据包处理的生产实践中去。

DPDK最初的动机很简单,就是证明IA多核处理器能够支撑高性能数据包处理。随着早期目标的达成和更多通用处理器体系的加入,DPDK逐渐成为通用多核处理器高性能数据包处理的业界标杆。

1.1 主流包处理硬件平台

DPDK用软件的方式在通用多核处理器上演绎着数据包处理的新篇章,而对于数据包处理,多核处理器显然不是唯一的平台。支撑包处理的主流硬件平台大致可分为三个方向。

❑硬件加速器

❑网络处理器

❑多核处理器

根据处理内容、复杂度、成本、量产规模等因素的不同,这些平台在各自特定的领域都有一定的优势。硬件加速器对于本身规模化的固化功能具有高性能低成本的特点,网络处理器提供了包处理逻辑软件可编程的能力,在获得灵活性的同时兼顾了高性能的硬件包处理,多核处理器在更为复杂多变的高层包处理上拥有优势,随着包处理的开源生态系统逐渐丰富,以及近年来性能的不断提升,其为软件定义的包处理提供了快速迭代的平台。参见[Ref1-2]。

随着现代处理器的创新与发展(如异构化),开始集成新的加速处理与高速IO单元,它们互相之间不断地融合。在一些多核处理器中,已能看到硬件加速单元的身影。从软件包处理的角度,可以卸载部分功能到那些硬件加速单元进一步提升性能瓶颈;从硬件包处理的流水线来看,多核上运行的软件完成了难以固化的上层多变逻辑的任务;二者相得益彰。

1.1.1 硬件加速器

硬件加速器被广泛应用于包处理领域,ASIC和FPGA是其中最广为采用的器件。

ASIC(Application-Specific Integrated Circuit)是一种应特定用户要求和特定电子系统的需要而设计、制造的集成电路。ASIC的优点是面向特定用户的需求,在批量生产时与通用集成电路相比体积更小、功耗更低、可靠性提高、性能提高、保密性增强、成本降低等。但ASIC的缺点也很明显,它的灵活性和扩展性不够、开发费用高、开发周期长。

为了弥补本身的一些缺点,ASIC越来越多地按照加速引擎的思路来构建,结合通用处理器的特点,融合成片上系统(SoC)提供异构处理能力,使得ASIC带上了智能(Smart)的标签。

FPGA(Field-Programmable Gate Array)即现场可编程门阵列。它作为ASIC领域中的一种半定制电路而出现,与ASIC的区别是用户不需要介入芯片的布局布线和工艺问题,而且可以随时改变其逻辑功能,使用灵活。FPGA以并行运算为主,其开发相对于传统PC、单片机的开发有很大不同,以硬件描述语言(Verilog或VHDL)来实现。相比于PC或单片机(无论是冯·诺依曼结构还是哈佛结构)的顺序操作有很大区别。

全可编程FPGA概念的提出,使FPGA朝着进一步软化的方向持续发展,其并行化整数运算的能力将进一步在通用计算定制化领域得到挖掘,近年来在数据中心中取得了很大进展,比如应用于机器学习场合。我们预计FPGA在包处理的应用场景将会从通信领域(CT)越来越多地走向数据中心和云计算领域。

1.1.2 网络处理器

网络处理器(Network Processer Unit, NPU)是专门为处理数据包而设计的可编程通用处理器,采用多内核并行处理结构,其常被应用于通信领域的各种任务,比如包处理、协议分析、路由查找、声音/数据的汇聚、防火墙、QoS等。其通用性表现在执行逻辑由运行时加载的软件决定,用户使用专用指令集即微码(microcode)进行开发。其硬件体系结构大多采用高速的接口技术和总线规范,具有较高的I/O能力,使得包处理能力得到很大提升。除了这些特点外,NPU一般还包含多种不同性能的存储结构,对数据进行分类存储以适应不同的应用目的。NPU中也越来越多地集成进了一些专用硬件协处理器,可进一步提高片内系统性能。

图1-1是NP-5处理器架构框图,以EZCHIP公司的NP-5处理器架构为例,TOP部分为可编程部分,根据需要通过编写微码快速实现业务相关的包处理逻辑。NPU拥有高性能和高可编程性等诸多优点,但其成本和特定领域的特性限制了它的市场规模(一般应用于专用通信设备)。而不同厂商不同架构的NPU遵循的微码规范不尽相同,开发人员的成长以及生态系统的构建都比较困难。虽然一些NPU的微码也开始支持由高级语言(例如C)编译生成,但由于结构化语言本身原语并未面向包处理,使得转换后的效率并不理想。

图1-1 NP-5处理器架构框图

随着SDN对于可编程网络,特别是可编程数据面的要求,网络处理器也可能会迎来新的发展机遇,但依然需要解决好不同架构的底层抽象以及上层业务的语义抽象。

1.1.3 多核处理器

现代CPU性能的扩展主要通过多核的方式进行演进。这样利用通用处理器同样可以在一定程度上并行地处理网络负载。由于多核处理器在逻辑负载复杂的协议及应用层面上的处理优势,以及越来越强劲的数据面的支持能力,它在多种业务领域得到广泛的采用。再加上多年来围绕CPU已经建立起的大量成熟软件生态,多核处理器发展的活力和热度也是其他形态很难比拟的。图1-2是Intel双路服务器平台框图,描述了一个典型的双路服务器平台的多个模块,CPU、芯片组C612、内存和以太网控制器XL710构成了主要的数据处理通道。基于PCIe总线的I/O接口提供了大量的系统接口,为服务器平台引入了差异化的设计。

当前的多核处理器也正在走向SoC化,针对网络的SoC往往集成内存控制器、网络控制器,甚至是一些硬件加速处理引擎。

这里列出了一些主流厂商的多核处理器的SoC平台。

❑IA multi-core Xeon

❑Tilear-TILE-Gx

❑Cavium Network-OCTEON & OCTEON II

❑Freescale-QorIQ

❑NetLogic Microsystem-XLP

图1-2 Intel双路服务器平台框图

图1-3的Cavium OCTEON处理器框图以Cavium OCTEON多核处理器为例,它集成多个CPU核以及众多加速单元和网络接口,组成了一个片上系统(SoC)。在这些SoC上,对于可固化的处理(例如,流分类,QoS)交由加速单元完成,而对于灵活的业务逻辑则由众多的通用处理器完成,这种方式有效地融合了软硬件各自的优势。随着软件(例如,DPDK)在I/O性能提升上的不断创新,将多核处理器的竞争力提升到一个前所未有的高度,网络负载与虚拟化的融合又催生了NFV的潮流。

图1-3 Cavium OCTEON处理器框图

更多内容请参考相关Cavium和Ezchip的信息([Ref1-3]和[Ref1-4])。

1.2 初识DPDK

本书介绍DPDK,主要以IA(Intel Architecture)多核处理器为目标平台。在IA上,网络数据包处理远早于DPDK而存在。从商业版的Windows到开源的Linux操作系统,所有跨主机通信几乎都会涉及网络协议栈以及底层网卡驱动对于数据包的处理。然而,低速网络与高速网络处理对系统的要求完全不一样。

1.2.1 IA不适合进行数据包处理吗

以Linux为例,传统网络设备驱动包处理的动作可以概括如下:

❑数据包到达网卡设备。

❑网卡设备依据配置进行DMA操作。

❑网卡发送中断,唤醒处理器。

❑驱动软件填充读写缓冲区数据结构。

❑数据报文达到内核协议栈,进行高层处理。

❑如果最终应用在用户态,数据从内核搬移到用户态。

❑如果最终应用在内核态,在内核继续进行。

随着网络接口带宽从千兆向万兆迈进,原先每个报文就会触发一个中断,中断带来的开销变得突出,大量数据到来会触发频繁的中断开销,导致系统无法承受,因此有人在Linux内核中引入了NAPI机制,其策略是系统被中断唤醒后,尽量使用轮询的方式一次处理多个数据包,直到网络再次空闲重新转入中断等待。NAPI策略用于高吞吐的场景,效率提升明显。

一个二层以太网包经过网络设备驱动的处理后,最终大多要交给用户态的应用,图1-4的典型网络协议层次OSI与TCP/IP模型,是一个基础的网络模型与层次,左侧是OSI定义的7层模型,右侧是TCP/IP的具体实现。网络包进入计算机大多需要经过协议处理,在Linux系统中TCP/IP由Linux内核处理。即使在不需要协议处理的场景下,大多数场景下也需要把包从内核的缓冲区复制到用户缓冲区,系统调用以及数据包复制的开销,会直接影响用户态应用从设备直接获得包的能力。而对于多样的网络功能节点来说,TCP/IP协议栈并不是数据转发节点所必需的。

图1-4 典型网络协议层次OSI与TCP/IP模型

以无线网为例,图1-5的无线4G/LTE数据面网络协议展示了从基站、基站控制器到无线核心网关的协议层次,可以看到大量处理是在网络二、三、四层进行的。如何让Linux这样的面向控制面原生设计的操作系统在包处理上减少不必要的开销一直是一大热点。有个著名的高性能网络I/O框架Netmap,它就是采用共享数据包池的方式,减少内核到用户空间的包复制。

图1-5 无线4G/LTE数据面网络协议

NAPI与Netmap两方面的努力其实已经明显改善了传统Linux系统上的包处理能力,那是否还有空间去做得更好呢?作为分时操作系统,Linux要将CPU的执行时间合理地调度给需要运行的任务。相对于公平分时,不可避免的就是适时调度。早些年CPU核数比较少,为了每个任务都得到响应处理,进行充分分时,用效率换响应,是一个理想的策略。现今CPU核数越来越多,性能越来越强,为了追求极端的高性能高效率,分时就不一定总是上佳的策略。以Netmap为例,即便其减少了内核到用户空间的内存复制,但内核驱动的收发包处理和用户态线程依旧由操作系统调度执行,除去任务切换本身的开销,由切换导致的后续cache替换(不同任务内存热点不同),对性能也会产生负面的影响。

如果再往实时性方面考虑,传统上,事件从中断发生到应用感知,也是要经过长长的软件处理路径。所以,在2010年前采用IA处理器的用户会得出这样一个结论,那就是IA不适合做包处理。

真的是这样么?在IA硬件基础上,包处理能力到底能做到多好,有没有更好的方法评估和优化包处理性能,怎样的软件设计方法能最充分地释放多核IA的包处理能力,这些问题都是在DPDK出现之前,实实在在地摆在Intel工程师面前的原始挑战。

1.2.2 DPDK最佳实践

如今,DPDK应该已经很好地回答了IA多核处理器是否可以应对高性能数据包处理这个问题。而解决好这样一个问题,也不是用了什么凭空产生的特殊技术,更多的是从工程优化角度的迭代和最佳实践的融合。如果要简单地盘点一下这些技术,大致可以归纳如下。

轮询,这一点很直接,可避免中断上下文切换的开销。之前提到Linux也采用该方法改进对大吞吐数据的处理,效果很好。在第7章,我们会详细讨论轮询与中断的权衡。

用户态驱动,在这种工作方式下,既规避了不必要的内存拷贝又避免了系统调用。一个间接的影响在于,用户态驱动不受限于内核现有的数据格式和行为定义。对mbuf头格式的重定义、对网卡DMA操作的重新优化可以获得更好的性能。而用户态驱动也便于快速地迭代优化,甚至对不同场景进行不同的优化组合。在第6章中,我们将探讨用户态网卡收发包优化。

亲和性与独占,DPDK工作在用户态,线程的调度仍然依赖内核。利用线程的CPU亲和绑定的方式,特定任务可以被指定只在某个核上工作。好处是可避免线程在不同核间频繁切换,核间线程切换容易导致因cache miss和cache write back造成的大量性能损失。如果更进一步地限定某些核不参与Linux系统调度,就可能使线程独占该核,保证更多cache hit的同时,也避免了同一个核内的多任务切换开销。在第3章,我们会再展开讨论。

降低访存开销,网络数据包处理是一种典型的I/O密集型(I/O bound)工作负载。无论是CPU指令还是DMA,对于内存子系统(Cache+DRAM)都会访问频繁。利用一些已知的高效方法来减少访存的开销能够有效地提升性能。比如利用内存大页能有效降低TLB miss,比如利用内存多通道的交错访问能有效提高内存访问的有效带宽,再比如利用对于内存非对称性的感知可以避免额外的访存延迟。而cache更是几乎所有优化的核心地带,这些有意思而且对性能有直接影响的部分,将在第2章进行更细致的介绍。

软件调优,调优本身并不能说是最佳实践。这里其实指代的是一系列调优实践,比如结构的cache line对齐,比如数据在多核间访问避免跨cache line共享,比如适时地预取数据,再如多元数据批量操作。这些具体的优化策略散布在DPDK各个角落。在第2章、第6章、第7章都会具体涉及。

利用IA新硬件技术,IA的最新指令集以及其他新功能一直是DPDK致力挖掘数据包处理性能的源泉。拿Intel®DDIO技术来讲,这个cache子系统对DMA访存的硬件创新直接助推了性能跨越式的增长。有效利用SIMD(Single Instruction Multiple Data)并结合超标量技术(Superscalar)对数据层面或者对指令层面进行深度并行化,在性能的进一步提升上也行之有效。另外一些指令(比如cmpxchg),本身就是lockless数据结构的基石,而crc32指令对与4 Byte Key的哈希计算也是改善明显。这些内容,在第2章、第4章、第5章、第6章都会有涉及。

充分挖掘网卡的潜能,经过DPDK I/O加速的数据包通过PCIe网卡进入系统内存,PCIe外设到系统内存之间的带宽利用效率、数据传送方式(coalesce操作)等都是直接影响I/O性能的因素。在现代网卡中,往往还支持一些分流(如RSS, FDIR等)和卸载(如Chksum, TSO等)功能。DPDK充分利用这些硬件加速特性,帮助应用更好地获得直接的性能提升。这些内容将从第6章~第9章一一展开。

除了这些基础的最佳实践,本书还会用比较多的篇幅带领大家进入DPDK I/O虚拟化的世界。在那里,我们依然从I/O的视角,介绍业界广泛使用的两种主流方式,SR-IOV和Virtio,帮助大家理解I/O硬件虚拟化的支撑技术以及I/O软件半虚拟化的技术演进和革新。从第10章到第14章,我们会围绕着这一主题逐步展开。

随着DPDK不断丰满成熟,也将自身逐步拓展到更多的平台和场景。从Linux到FreeBSD,从物理机到虚拟机,从加速网络I/O到加速存储I/O, DPDK在不同纬度发芽生长。在NFV大潮下,无论是NFVI(例如,virtual switch)还是VNF, DPDK都用坚实有力的性能来提供基础设施保障。这些内容将在第10章~第15章一一介绍。

当然,在开始后续所有章节之前,让我们概览一下DPDK的软件整体框架。

1.2.3 DPDK框架简介

DPDK为IA上的高速包处理而设计。图1-6所示的DPDK主要模块分解展示了以基础软件库的形式,为上层应用的开发提供一个高性能的基础I/O开发包。它大量利用了有助于包处理的软硬件特性,如大页、缓存行对齐、线程绑定、预取、NUMA、IA最新指令的利用、Intel®DDIO、内存交叉访问等。

核心库Core Libs,提供系统抽象、大页内存、缓存池、定时器及无锁环等基础组件。

PMD库,提供全用户态的驱动,以便通过轮询和线程绑定得到极高的网络吞吐,支持各种本地和虚拟的网卡。

Classify库,支持精确匹配(Exact Match)、最长匹配(LPM)和通配符匹配(ACL),提供常用包处理的查表操作。

QoS库,提供网络服务质量相关组件,如限速(Meter)和调度(Sched)。

图1-6 DPDK主要模块分解

除了这些组件,DPDK还提供了几个平台特性,比如节能考虑的运行时频率调整(POWER),与Linux kernel stack建立快速通道的KNI(Kernel Network Interface)。而Packet Framework和DISTRIB为搭建更复杂的多核流水线处理模型提供了基础的组件。

1.2.4 寻找性能优化的天花板

性能优化不是无止境的,所谓天花板可以认为是理论极限,性能优化能做到的就是无限接近这个理论极限。而理论极限也不是单纬度的,当某个纬度接近极限时,可能在另一个纬度会有其他的发现。

我们讨论数据包处理,那首先就看看数据包转发速率是否有天花板。其实包转发的天花板就是理论物理线路上能够传送的最大速率,即线速。那数据包经过网络接口进入内存,会经过I/O总线(例如,PCIe bus), I/O总线也有天花板,实际事务传输不可能超过总线最大带宽。CPU从cache里加载/存储cache line有没有天花板呢,当然也有,比如Haswell处理器能在一个周期加载64字节和保存32字节。同样内存控制器也有内存读写带宽。这些不同纬度的边界把工作负载包裹起来,而优化就是在这个边界里吹皮球,不断地去接近甚至触碰这样的边界。

由于天花板是理论上的,因此对于前面介绍的一些可量化的天花板,总是能够指导并反映性能优化的优劣。而有些天花板可能很难量化,比如在某个特定频率的CPU下每个包所消耗的周期最小能做到多少。对于这样的天花板,可能只能用不断尝试实践的方式,当然不同的方法可能带来不同程度的突破,总的增益越来越少时,就可能是接近天花板的时候。

那DPDK在IA上提供网络处理能力有多优秀呢?它是否已经能触及一些系统的天花板?在这些天花板中,最难触碰的是哪一个呢?要真正理解这一点,首先要明白在IA上包处理终极挑战的问题是什么,在这之前我们需要先来回顾一下衡量包处理能力的一些常见能力指标。

1.3 解读数据包处理能力

不管什么样的硬件平台,对于包处理都有最基本的性能诉求。一般常被提到的有吞吐、延迟、丢包率、抖动等。对于转发,常会以包转发率(pps,每秒包转发率)而不是比特率(bit/s,每秒比特转发率)来衡量转发能力,这跟包在网络中传输的方式有关。不同大小的包对存储转发的能力要求不尽相同。让我们先来温习一下有效带宽和包转发率概念。

线速(Wire Speed)是线缆中流过的帧理论上支持的最大帧数。

我们用以太网(Ethernet)为例,一般所说的接口带宽,1Gbit/s、10Gbit/s、25Gbit/s、40Gbit/s、100Gbit/s,代表以太接口线路上所能承载的最高传输比特率,其单位是bit/s(bit per second,位/秒)。实际上,不可能每个比特都传输有效数据。以太网每个帧之间会有帧间距(Inter-Packet Gap, IPG),默认帧间距大小为12字节。每个帧还有7个字节的前导(Preamble),和1个字节的帧首定界符(Start Frame Delimiter, SFD)。具体帧格式如图1-7所示,有效内容主要是以太网的目的地址、源地址、以太网类型、负载。报文尾部是校验码。

图1-7 以太帧格式

所以,通常意义上的满速带宽能跑有效数据的吞吐可以由如下公式得到理论帧转发率:

而这个最大理论帧转发率的倒数表示了线速情况下先后两个包到达的时间间隔。

按照这个公式,将不同包长按照特定的速率计算可得到一个以太帧转发率,如表1-1所示。如果仔细观察,可以发现在相同带宽速率下,包长越小的包,转发率越高,帧间延迟也越小。

表1-1 帧转发率

满足什么条件才能达到无阻塞转发的理论上限呢?如果我们把处理一个数据包的整个生命周期看做是工厂的生产流水线,那么就要保证在这个流水线上,不能有任何一级流水处理的延迟超过此时间间隔。理解了这一点,对照表1-1,就很容易发现,对任何一个数据包处理流水线来说,越小的数据包,挑战总是越大。这样的红线对任何一个硬件平台,对任何一个在硬件平台上设计整体流水线的设计师来说都是无法逃避并需要积极面对的。

1.4 探索IA处理器上最艰巨的任务

在通用处理器上处理包的最大挑战是什么?为什么以往通用处理器很少在数据面中扮演重要的角色?如果我们带着这些问题来看数据面上的负载,就会有一个比较直观的理解。这里拿40Gbit/s的速率作为考察包转发能力的样本。如图1-8所示,曲线为不同大小的包的最大理论转发能力。

图1-8 线速情况下的报文的指令成本

分别截取64B和1024B数据包长,图1-8所示的线速情况下的报文的指令成本能明显地说明不同报文大小给系统带来的巨大差异。就如我们在包转发率那一节中理解的,对于越小的包,相邻包到达的时间间隔就越小,16.8ns vs 208.8ns。假设CPU的主频率是2GHz,要达到理论最大的转发能力,对于64B和1024B软件分别允许消耗33和417个时钟周期。在存储转发(store-forward)模型下,报文收发以及查表都需要访存。那就对比一下访存的时钟周期,一次LLC命中需要大约40个时钟周期,如果LLC未命中,一次内存的读就需要70ns。换句话说,对于64B大小的包,即使每次都能命中LLC,40个时钟周期依然离33有距离。显然,小包处理时延对于通用CPU系统架构的挑战是巨大的。

那是否说明IA就完全不适合高性能的网络负载呢?答案是否定的。证明这样的结论我们从两个方面入手,一个是IA平台实际能提供的最大能力,另一个是这个能力是否足以应对一定领域的高性能网络负载。

DPDK的出现充分释放了IA平台对包处理的吞吐能力。我们知道,随着吞吐率的上升,中断触发的开销是不能忍受的,DPDK通过一系列软件优化方法(大页利用,cache对齐,线程绑定,NUMA感知,内存通道交叉访问,无锁化数据结构,预取,SIMD指令利用等)利用IA平台硬件特性,提供完整的底层开发支持库。使得单核三层转发可以轻松地突破小包30Mpps,随着CPU封装的核数越来越多,支持的PCIe通道数越来越多,整系统的三层转发吞吐在2路CPU的Xeon E5-2658 v3上可以达到300Mpps。这已经是一个相当可观的转发吞吐能力了。

虽然这个能力不足以覆盖网络中所有端到端的设备场景,但无论在核心网接入侧,还是在数据中心网络中,都已经可以覆盖相当多的场景。

随着数据面可软化的发生,数据面的设计、开发、验证乃至部署会发生一系列的变化。首先,可以采用通用服务器平台,降低专门硬件设计成本;其次,基于C语言的开发,就程序员数量以及整个生态都要比专门硬件开发更丰富;另外,灵活可编程的数据面部署也给网络功能虚拟化(NFV)带来了可能,更会进一步推进软件定义网络(SDN)的全面展开。

1.5 软件包处理的潜力——再识DPDK

DPDK很好地将IA上包处理的性能提升到一个高度,这个高度已经达到很多网络应用场景的最低要求,使得满足要求的场景下对于网络业务软化产生积极的作用。

1.5.1 DPDK加速网络节点

在理解了IA上包处理面临的根本性挑战后,我们会对DPDK所取得的性能提升感到异常兴奋。更令人兴奋的是,按照DPDK所倡导的方法,随着处理器的每一代更新,在IA上的性能提升以很高的斜率不断发酵。当千兆、万兆接口全速转发已不再是问题时,DPDK已将目标伸向百万兆的接口。

DPDK软件包内有一个最基本的三层转发实例(l3fwd),可用于测试双路服务器整系统的吞吐能力,实验表明可以达到220Gbit/s的数据报文吞吐能力。值得注意的是,除了通过硬件或者软件提升性能之外,如今DPDK整系统报文吞吐能力上限已经不再受限于CPU的核数,当前瓶颈在于PCIe(IO总线)的LANE数。换句话说,系统性能的整体I/O天花板不再是CPU,而是系统所提供的所有PCIe LANE的带宽,能插入多少个高速以太网接口卡。

在这样的性能基础上,网络节点的软化就成为可能。对于网络节点上运转的不同形态的网络功能,一旦软化并适配到一个通用的硬件平台,随之一个自然的诉求可能就是软硬件解耦。解耦正是网络功能虚拟化(NFV)的一个核心思想,而硬件解耦的多个网络功能在单一通用节点上的隔离共生问题,是另一个核心思想虚拟化诠释的。当然这个虚拟化是广义的,在不同层面可以有不同的支撑技术。

NFV有很多诉求,业务面高性能,控制面高可用、高可靠、易运维、易管理等。但没有业务面的高性能,后续的便无从谈起。DPDK始终为高性能业务面提供坚实的支撑,除此以外,DPDK立足IA的CPU虚拟化技术和IO的虚拟化技术,对各种通道做持续优化改进的同时,也对虚拟交换(vswitch)的转发面进化做出积极贡献。应对绝对高吞吐能力的要求,DPDK支持各种I/O的SR-IOV接口;应对高性能虚拟主机网络的要求,DPDK支持标准virtio接口;对虚拟化平台的支撑,DPDK从KVM、VMWARE、XEN的hypervisor到容器技术,可谓全平台覆盖。

可以说,在如火如荼的网络变革的大背景下,DPDK以强劲的驱动力加速各种虚拟化的网络功能部署到现实的网络节点上。

1.5.2 DPDK加速计算节点

DPDK之于网络节点,主要集中在数据面转发方面,这个很容易理解;对于计算节点,DPDK也拥有很多潜在的机会。

C10K是IT界的一个著名命题,甚至后续衍生出了关于C1M和C10M的讨论。其阐述的一个核心问题就是,随着互联网发展,随着数据中心接口带宽不断提升,计算节点上各种互联网服务对于高并发下的高吞吐有着越来越高的要求。详见[Ref1-5]。

但是单一接口带宽的提高并不能直接导致高并发、高吞吐服务的发生,即使用到了一系列系统方法(异步非阻塞,线程等),但网络服务受限于内核协议栈多核水平扩展上的不足以及建立拆除连接的高开销,开始逐渐阻碍进一步高并发下高带宽的要求。另一方面,内核协议栈需要考虑更广泛的支持,并不能为特定的应用做特殊优化,一般只能使用系统参数进行调优。

当然,内核协议栈也在不断改进,而以应用为中心的趋势也会不断推动用户态协议栈的涌现。有基于BSD协议栈移植的,有基于多核模型重写的原型设计,也有将整个Linux内核包装成库的。它们大多支持以DPDK作为I/O引擎,有些也将DPDK的一些优化想法加入到协议栈的优化中,取得了比较好的效果。

可以说,由DPDK加速的用户态协议栈将会越来越多地支撑起计算节点上的网络服务。

1.5.3 DPDK加速存储节点

除了在网络、计算节点的应用机会之外,DPDK的足迹还渗透到存储领域。Intel®最近开源了SPDK(Storage Performance Development Kit),一款存储加速开发套件,其主要的应用场景是iSCSI性能加速。目前iSCSI系统包括前端和后端两个部分,在前端,DPDK提供网络I/O加速,加上一套用户态TCP/IP协议栈(目前还不包含在开源包中),以流水线的工作方式支撑起基于iSCSI的应用;在后端,将DPDK用户态轮询驱动的方式实践在NVMe上,PMD的NVMe驱动加速了后端存储访问。这样一个端到端的整体方案,用数据证明了卓有成效的IOPS性能提升。SPDK的详细介绍见:https://01.org/spdk

可以说,理解DPDK的核心方法,并加以恰当地实践,可以将I/O在IA多核的性能提升有效地拓展到更多的应用领域,并产生积极的意义。

1.5.4 DPDK的方法论

DPDK采用了很多具体优化方法来达到性能的提升,有一些是利用IA软件优化的最佳实践方法,还有一些是利用了IA的处理器特性。这里希望脱离这一个个技术细节,尝试着去还原一些核心的指导思想,试图从方法论的角度去探寻DPDK成功背后的原因,但愿这样的方法论总结,可以在开拓未知领域的过程中对大家有所助益。

1. 专用负载下的针对性软件优化

专用处理器通过硬件架构专用优化来达到高性能,DPDK则利用通用处理器,通过优化的专用化底层软件来达到期望的高性能。这要求DPDK尽可能利用一切平台(CPU,芯片组,PCIe以及网卡)特性,并针对网络负载的特点,做针对性的优化,以发掘通用平台在某一专用领域的最大能力。

2. 追求可水平扩展的性能

利用多核并行计算技术,提高性能和水平扩展能力。对于产生的并发干扰,遵循临界区越薄越好、临界区碰撞越少越好的指导原则。数据尽可能本地化和无锁化,追求吞吐率随核数增加而线性增长。

3. 向Cache索求极致的实现优化性能

相比于系统优化和算法优化,实现优化往往较少被提及。实现优化对开发者的要求体现在需要对处理器体系结构有所了解。DPDK可谓集大量的实现优化之大成,而这些方法多数围绕着Cache进行,可以说能娴熟地驾驭好Cache,在追求极致性能的路上就已经成功了一半。

4. 理论分析结合实践推导

性能的天花板在哪,调优是否还有空间,是否值得花更多的功夫继续深入,这些问题有时很难直接找到答案。分析、推测、做原型、跑数据、再分析,通过这样的螺旋式上升,慢慢逼近最优解,往往是实践道路上的导航明灯。条件允许下,有依据的理论量化计算,可以更可靠地明确优化目标。

1.6 从融合的角度看DPDK

这是一个最好的时代,也是一个最坏的时代。不可否认的是,这就是一个融合的时代。

随着云计算的推进,ICT这个词逐渐在各类技术研讨会上被提及。云计算的定义虽然有各种版本,但大体都包含了对网络基础设施以及对大数据处理的基本要求,这也是IT与CT技术融合的推动力。

那这和DPDK有关系吗?还真有!我们知道云计算的对象是数据,数据在云上加工,可还是要通过各种载体落到地上。在各种载体中最广泛使用的当属IP,它是整个互联网蓬勃发展的基石。高效的数据处理总是离不开高效的数据承载网络。

教科书说到网络总会讲到那经典的7层模型,最低层是物理层,最高层是应用层。名副其实的是,纵观各类能联网的设备,从终端设备到网络设备再到数据中心服务器,还真是越靠近物理层的处理以硬件为主,越靠近应用层的处理以软件为主。这当然不是巧合,其中深谙了一个原则,越是能标准化的,越要追求极简极速,所以硬件当仁不让,一旦进入多样性可变性强的领域,软件往往能发挥作用。但没有绝对和一成不变,因为很多中间地带更多的是权衡。

DPDK是一个软件优化库,目标是在通用处理器上发挥极致的包能力,以媲美硬件级的性能。当然软件是跑在硬件上的,如果看整个包处理的硬件平台,软硬件融合的趋势也相当明显。各类硬件加速引擎逐渐融入CPU构成异构SoC(System On-Chip),随着Intel®对Altera®收购的完成,CPU+FPGA这一对组合也给足了我们想象的空间,可以说包处理正处在一个快速变革的时代。

1.7 实例

在对DPDK的原理和代码展开进一步解析之前,先看一些小而简单的例子,建立一个形象上的认知。

1)helloworld,启动基础运行环境,DPDK构建了一个基于操作系统的,但适合包处理的软件运行环境,你可以认为这是个mini-OS。最早期DPDK,可以完全运行在没有操作系统的物理核(bare-metal)上,这部分代码现在不在主流的开源包中。

2)skeleton,最精简的单核报文收发骨架,也许这是当前世界上运行最快的报文进出测试程序。

3)l3fwd,三层转发是DPDK用于发布性能测试指标的主要应用。

1.7.1 HelloWorld

DPDK里的HelloWorld是最基础的入门程序,代码简短,功能也不复杂。它建立了一个多核(线程)运行的基础环境,每个线程会打印“hello from core #”, core #是由操作系统管理的。如无特别说明,本文里的DPDK线程与硬件线程是一一对应的关系。从代码角度,rte是指runtime environment, eal是指environment abstraction layer。DPDK的主要对外函数接口都以rte_作为前缀,抽象化函数接口是典型软件设计思路,可以帮助DPDK运行在多个操作系统上,DPDK官方支持Linux与FreeBSD。和多数并行处理系统类似,DPDK也有主线程、从线程的差异。

        int
        main(int argc, char **argv)
        {
            int ret;
            unsigned lcore_id;
            ret = rte_eal_init(argc, argv);
            if (ret < 0)
                rte_panic(“Cannot init EAL\n”);
            /* call lcore_hello() on every slave lcore */
                RTE_LCORE_FOREACH_SLAVE(lcore_id) {
                rte_eal_remote_launch(lcore_hello, NULL, lcore_id);
            }
            /* call it on master lcore too */
            lcore_hello(NULL);
            rte_eal_mp_wait_lcore();
            return 0;
        }

1. 初始化基础运行环境

主线程运行入口是main函数,调用了rte_eal_init入口函数,启动基础运行环境。

        int rte_eal_init(int argc, char **argv);

入口参数是启动DPDK的命令行,可以是长长的一串很复杂的设置,需要深入了解的读者可以查看DPDK相关的文档与源代码\lib\librte_eal\common\eal_common_options.c。对于HelloWorld这个实例,最需要的参数是“-c <core mask>”,线程掩码(core mask)指定了需要参与运行的线程(核)集合。rte_eal_init本身所完成的工作很复杂,它读取入口参数,解析并保存作为DPDK运行的系统信息,依赖这些信息,构建一个针对包处理设计的运行环境。主要动作分解如下:

❑配置初始化

❑内存初始化

❑内存池初始化

❑队列初始化

❑告警初始化

❑中断初始化

❑PCI初始化

❑定时器初始化

❑检测内存本地化(NUMA)

❑插件初始化

❑主线程初始化

❑轮询设备初始化

❑建立主从线程通道

❑将从线程设置在等待模式

❑PCI设备的探测与初始化

对于DPDK库的使用者,这些操作已经被EAL封装起来,接口清晰。如果需要对DPDK进行深度定制,二次开发,需要仔细研究内部操作,这里不做详解。

2. 多核运行初始化

DPDK面向多核设计,程序会试图独占运行在逻辑核(lcore)上。main函数里重要的是启动多核运行环境,RTE_LCORE_FOREACH_SLAVE(lcore_id)如名所示,遍历所有EAL指定可以使用的lcore,然后通过rte_eal_remote_launch在每个lcore上,启动被指定的线程。

        int rte_eal_remote_launch(int (*f)(void *),
            void *arg, unsigned slave_id);

第一个参数是从线程,是被征召的线程;

第二个参数是传给从线程的参数;

第三个参数是指定的逻辑核,从线程会执行在这个core上。

具体来说,int rte_eal_remote_launch(lcore_hello, NULL, lcore_id);

参数lcore_id指定了从线程ID,运行入口函数lcore_hello。

运行函数lcore_hello,它读取自己的逻辑核编号(lcore_id),打印出“hello from core #”

        static int
        lcore_hello(__attribute__((unused)) void *arg)
        {
            unsigned lcore_id;
            lcore_id = rte_lcore_id();
            printf("hello from core %u\n", lcore_id);
            return 0;
        }

这是个简单示例,从线程很快就完成了指定工作,在更真实的场景里,这个从线程会是一个循环运行的处理过程。

1.7.2 Skeleton

DPDK为多核设计,但这是单核实例,设计初衷是实现一个最简单的报文收发示例,对收入报文不做任何处理直接发送。整个代码非常精简,可以用于平台的单核报文出入性能测试。

主要处理函数main的处理逻辑如下(伪码),调用rte_eal_init初始化运行环境,检查网络接口数,据此分配内存池rte_pktmbuf_pool_create,入口参数是指定rte_socket_id(),考虑了本地内存使用的范例。调用port_init(portid, mbuf_pool)初始化网口的配置,最后调用lcore_main()进行主处理流程。

              int main(int argc, char *argv[])
              {
                  struct rte_mempool *mbuf_pool;
                  unsigned nb_ports;
                  uint8_t portid;
                  /* Initialize the Environment Abstraction Layer (EAL). */
                  int ret = rte_eal_init(argc, argv);
                  /* Check that there is an even number of ports t send/receive on. */
                  nb_ports = rte_eth_dev_count();
                  if (nb_ports < 2 —— (nb_ports & 1))
                      rte_exit(EXIT_FAILURE, "Error: number of ports must be even\n");
                  /* Creates a new mempool in memory to hold the mbufs. */
                  mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", NUM_MBUFS * nb_ports,
                      MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
                  /* Initialize all ports. */
                  for (portid = 0; portid < nb_ports; portid++)
                      if (port_init(portid, mbuf_pool) ! = 0)
                          rte_exit(EXIT_FAILURE, "Cannot init port %"PRIu8 "\n",
                                    portid);
                  /* Call lcore_main on the master core only. */
                  lcore_main();
                  return 0;
              }

网口初始化流程:

        port_init(uint8_t port, struct rte_mempool *mbuf_pool)

首先对指定端口设置队列数,基于简单原则,本例只指定单队列。在收发两个方向上,基于端口与队列进行配置设置,缓冲区进行关联设置。如不指定配置信息,则使用默认配置。

网口设置:对指定端口设置接收、发送方向的队列数目,依据配置信息来指定端口功能

        int rte_eth_dev_configure(uint8_t port_id, uint16_t nb_rx_q,
                    uint16_t nb_tx_q, const struct rte_eth_conf *dev_conf)

队列初始化:对指定端口的某个队列,指定内存、描述符数量、报文缓冲区,并且对队列进行配置

        int rte_eth_rx_queue_setup(uint8_t port_id, uint16_t rx_queue_id,
                      uint16_t nb_rx_desc, unsigned int socket_id,
                      const struct rte_eth_rxconf *rx_conf,
                      struct rte_mempool *mp)
        int rte_eth_tx_queue_setup(uint8_t port_id, uint16_t tx_queue_id,
                          uint16_t nb_tx_desc, unsigned int socket_id,
                          const struct rte_eth_txconf *tx_conf)

网口设置:初始化配置结束后,启动端口int rte_eth_dev_start(uint8_t port_id);

完成后,读取MAC地址,打开网卡的混杂模式设置,允许所有报文进入。

              static inline int
              port_init(uint8_t port, struct rte_mempool *mbuf_pool)
              {
                  struct rte_eth_conf port_conf = port_conf_default;
                  const uint16_t rx_rings = 1, tx_rings = 1;
                  /* Configure the Ethernet device. */
                  retval = rte_eth_dev_configure(port, rx_rings, tx_rings, &port_conf);
                  /* Allocate and set up 1 RX queue per Ethernet port. */
                  for (q = 0; q < rx_rings; q++) {
                      retval = rte_eth_rx_queue_setup(port, q, RX_RING_SIZE,
                              rte_eth_dev_socket_id(port), NULL, mbuf_pool);
                  }
                  /* Allocate and set up 1 TX queue per Ethernet port. */
                  for (q = 0; q < tx_rings; q++) {
                      retval = rte_eth_tx_queue_setup(port, q, TX_RING_SIZE,
                              rte_eth_dev_socket_id(port), NULL);
                  }
                  /* Start the Ethernet port. */
                  retval = rte_eth_dev_start(port);
                  /* Display the port MAC address. */
                  struct ether_addr addr;
                  rte_eth_macaddr_get(port, &addr);
                  /* Enable RX in promiscuous mode for the Ethernet device. */
                  rte_eth_promiscuous_enable(port);
                  return 0;
              }

网口收发报文循环收发在lcore_main中有个简单实现,因为是示例,为保证性能,首先检测CPU与网卡的Socket是否最优适配,建议使用本地CPU就近操作网卡,后续章节有详细说明。数据收发循环非常简单,为高速报文进出定义了burst的收发函数如下,4个参数意义非常直观:端口,队列,报文缓冲区以及收发包数。

基于端口队列的报文收发函数:

        static inline uint16_t rte_eth_rx_burst(uint8_t port_id, uint16_t queue_id,
        struct rte_mbuf **rx_pkts, const uint16_t nb_pkts)
        static inline uint16_t rte_eth_tx_burst(uint8_t port_id, uint16_t queue_id,
        struct rte_mbuf **tx_pkts, uint16_t nb_pkts)

这就构成了最基本的DPDK报文收发展示。可以看到,此处不涉及任何具体网卡形态,软件接口对硬件没有依赖。

        static __attribute__((noreturn)) void lcore_main(void)
        {
            const uint8_t nb_ports = rte_eth_dev_count();
            uint8_t port;
            for (port = 0; port < nb_ports; port++)
                if (rte_eth_dev_socket_id(port) > 0 &&
                        rte_eth_dev_socket_id(port) ! =
                                  (int)rte_socket_id())
                    printf("WARNING, port %u is on remote NUMA node to "
                              "polling thread.\n\tPerformance will "
                              "not be optimal.\n", port);
            /* Run until the application is quit or killed. */
            for (; ; ) {
                /*
                  * Receive packets on a port and forward them on the paired
                  * port. The mapping is 0-> 1, 1-> 0, 2-> 3, 3-> 2, etc.
                  */
                for (port = 0; port < nb_ports; port++) {
                    /* Get burst of RX packes, from first port of pair. */
                    struct rte_mbuf *bufs[BURST_SIZE];
                    const uint16_t nb_rx = rte_eth_rx_burst(port, 0,
                              bufs, BURST_SIZE);
                    if (unlikely(nb_rx == 0))
                        continue;
                    /* Send burst of TX packets, to second port of pair. */
                    const uint16_t nb_tx = rte_eth_tx_burst(port ^ 1, 0,
                              bufs, nb_rx);
                    /* Free any unsent packets. */
                    if (unlikely(nb_tx < nb_rx)) {
                        uint16_t buf;
                        for (buf = nb_tx; buf < nb_rx; buf++)
                              rte_pktmbuf_free(bufs[buf]);
                    }
                }
            }
        }
        }

1.7.3 L3fwd

这是DPDK中最流行的例子,也是发布DPDK性能测试的例子。如果将PCIE插槽上填满高速网卡,将网口与大流量测试仪表连接,它能展示在双路服务器平台具备200Gbit/s的转发能力。数据包被收入系统后,会查询IP报文头部,依据目标地址进行路由查找,发现目的端口,修改IP头部后,将报文从目的端口送出。路由查找有两种方式,一种方式是基于目标IP地址的完全匹配(exact match),另一种方式是基于路由表的最长掩码匹配(Longest Prefix Match, LPM)。三层转发的实例代码文件有2700多行(含空行与注释行),整体逻辑其实很简单,是前续HelloWorld与Skeleton的结合体。

启动这个例子,指定命令参数格式如下:

        ./build/l3fwd [EAL options] -- -p PORTMASK [-P]
        --config(port, queue, lcore)[, (port, queue, lcore)]

命令参数分为两个部分,以“--”为分界线,分界线右边的参数是三层转发的私有命令选项。左边则是DPDK的EAL Options。

❑[EAL Options]是DPDK运行环境的输入配置选项,输入命令会交给rte_eal_init处理;

❑PORTMASK依据掩码选择端口,DPDK启动时会搜索系统认识的PCIe设备,依据黑白名单原则来决定是否接管,早期版本可能会接管所有端口,断开网络连接。现在可以通过脚本绑定端口,具体可以参见http://www.dpdk.org/browse/dpdk/tree/tools/dpdk_nic_bind.py

❑config选项指定(port, queue, lcore),用指定线程处理对应的端口的队列。要实现200Gbit/s的转发,需要大量线程(核)参与,并行转发。

先来看主线程流程main的处理流程,因为和HelloWorld与Skeleton类似,不详细叙述。

        初始化运行环境: rte_eal_init(argc, argv);
        分析入参: parse_args(argc, argv)
        初始化lcore与port配置
        端口与队列初始化,类似Skeleton处理
        端口启动,使能混杂模式
        启动从线程,令其运行main_loop()

从线程执行main_loop()的主要步骤如下:

        读取自己的lcore信息完成配置;
        读取关联的接收与发送队列信息;
        进入循环处理:
        {
            向指定队列批量发送报文;
            从指定队列批量接收报文;
        批量转发接收到报文;
    }

向指定队列批量发送报文,从指定队列批量接收报文,此前已经介绍了DPDK的收发函数。批量转发接收到的报文是处理的主体,提供了基于Hash的完全匹配转发,也可以基于最长匹配原则(LPM)进行转发。转发路由查找方式可以由编译配置选择。除了路由转发算法的差异,下面的例子还包括基于multi buffer原理的代码实现。在#if (ENABLE_MULTI_BUFFER_OPTIMIZE == 1)的路径下,一次处理8个报文。和普通的软件编程不同,初次见到的程序员会觉得奇怪。它的实现有效利用了处理器内部的乱序执行和并行处理能力,能显著提高转发性能。

              for (j = 0; j < n; j += 8) {
                  uint32_t pkt_type =
                      pkts_burst[j]->packet_type &
                      pkts_burst[j+1]->packet_type &
                      pkts_burst[j+2]->packet_type &
                      pkts_burst[j+3]->packet_type &
                      pkts_burst[j+4]->packet_type &
                      pkts_burst[j+5]->packet_type &
                      pkts_burst[j+6]->packet_type &
                      pkts_burst[j+7]->packet_type;
                  if (pkt_type & RTE_PTYPE_L3_IPV4) {
                      simple_ipv4_fwd_8pkts(&pkts_burst[j], portid, qconf);
                  } else if (pkt_type & RTE_PTYPE_L3_IPV6) {
                      simple_ipv6_fwd_8pkts(&pkts_burst[j], portid, qconf);
                  } else {
                      l3fwd_simple_forward(pkts_burst[j], portid, qconf);
                      l3fwd_simple_forward(pkts_burst[j+1], portid, qconf);
                      l3fwd_simple_forward(pkts_burst[j+2], portid, qconf);
                      l3fwd_simple_forward(pkts_burst[j+3], portid, qconf);
                      l3fwd_simple_forward(pkts_burst[j+4], portid, qconf);
                      l3fwd_simple_forward(pkts_burst[j+5], portid, qconf);
                      l3fwd_simple_forward(pkts_burst[j+6], portid, qconf);
                      l3fwd_simple_forward(pkts_burst[j+7], portid, qconf);
                      }
                  }
                  for (; j < nb_rx ; j++) {
                      l3fwd_simple_forward(pkts_burst[j], portid, qconf);
                  }

依据IP头部的五元组信息,利用rte_hash_lookup来查询目标端口。

        mask0 = _mm_set_epi32(ALL_32_BITS, ALL_32_BITS, ALL_32_BITS, BIT_8_TO_15);
        ipv4_hdr = (uint8_t *)ipv4_hdr + offsetof(struct ipv4_hdr, time_to_live);
        __m128i data = _mm_loadu_si128((__m128i*)(ipv4_hdr));
        /* Get 5 tuple: dst port, src port, dst IP address, src IP address and protocol */
        key.xmm = _mm_and_si128(data, mask0);
        /* Find destination port */
        ret = rte_hash_lookup(ipv4_l3fwd_lookup_struct, (const void *)&key);
        return (uint8_t)((ret < 0)? portid : ipv4_l3fwd_out_if[ret]);

这段代码在读取报文头部信息时,将整个头部导入了基于SSE的矢量寄存器(128位宽),并对内部进行了掩码mask0运算,得到key,然后把key作为入口参数送入rte_hash_lookup运算。同样的操作运算还展示在对IPv6的处理上,可以在代码中参考。

我们并不计划在本节将读者带入代码陷阱中,实际上本书总体上也没有偏重代码讲解,而是在原理上进行解析。如果读者希望了解详细完整的编程指南,可以参考DPDK的网站。

1.8 小结

什么是DPDK?相信读完本章,读者应该对它有了一个整体的认识。DPDK立足通用多核处理器,经过软件优化的不断摸索,实践出一套行之有效的方法,在IA数据包处理上取得重大性能突破。随着软硬件解耦的趋势,DPDK已经成为NFV事实上的数据面基石。着眼未来,无论是网络节点,还是计算节点,或是存储节点,这些云服务的基础设施都有机会因DPDK而得到加速。在IT和CT不断融合的过程中,在运营商网络和数据中心网络持续SDN化的过程中,在云基础设施对数据网络性能孜孜不倦的追求中,DPDK将扮演越来越重要的作用。