背景介绍
NFV,即网络功能虚拟化,旨在利用标准的标准的硬件,在商业服务器COTS(ommercial off-the-shelf)上,尤其是x86平台,对网络功能(network function)如路由器、防火墙、CPE等进行虚拟化,减少上市时间(time-to-market),运营和维护成本等。
由ETSI推出的一系列规范旨在建立一个NFV体系结构(详细参考见:http://www.etsi.org/technologies-clusters/technologies/nfv)。
通俗地,我们可以认为NFV是一种软件定义电信基础设施的手段,即电信云。到这里,相信大家已经很可能想到:OpenStack作为开源IaaS最热门的解决方案,与NFV结合应该是非常好的选择。
作为开源领域的专家,Red Hat对NFV做出了尝试(参见:Red_Hat_OpenStack_Platform-10-Network_Functions_Virtualization_Product_Guide-en-US.pdf),Red Hat认为:NFV有意义的前提是虚拟化的网络功能(VNF)在性能上至少不比物理设备实现低。这显然是对数据面性能的一个挑战。
那么第一个问题显然是商业X86服务器能线速地承载一个或多个10GE、25GE、40GE甚至100GE速率地以太网网络接口卡(NIC)?Intel DPDK的出现使得这个成为可能,关于DPDK这里不再介绍,但是根据UnitedStack有云网络团队的经验,一个开启超线程的核对intel 82599 10G 以太网卡数据包接收核发送/接收能力在14Mpps(帧长64字节),因此对于面向DPDK的应用,数据面业务很容以在端口密度核cpu核上可扩展。
假设数据帧可以以线速的方式进入/离开宿主机,那么数据帧是否有能力线速的在虚拟机(在NFV体系结构中,网络业务运行在虚拟机[广义地,包括容器]中)快速转发呢?由于数据进入宿主机Host内存中,数据的下一步流向基于软件实现,这个过程是一个CPU费时的,DPDK现有的解决方案可以表明,宿主机与虚拟机(容器)之间的虚拟链路相对于硬件来说确实是一个瓶颈,下图(来自DPDK2016summit)列出了不同的虚拟链路的性能测试结果:
可以看出基于virtio-pci这种IPC规范的数据传输确实是一种高性能的解决方案,且比传统的Linux kernel、pcap甚至af_packet虚拟链路要高出不少。但这是宿主机到虚拟机(容器)的虚拟链路的极限性能了吗?
UnitedStack有云网络团队的工程师相信可以通过可以摒弃这种基于virtio-pci的数据传输方式,进而使用一种平台友好(x86 向量指令处理,更好的cache布局)的IPC机制来构建性能更好的虚拟链路。
高性能虚拟链路
作为实践,我们使用了一种向量化的ring buffer来作为宿主机和LXC容器之间通信的机制,并封装在DPDK PMD驱动中,下文称之为eth_vecring,从而原生地支持任何基于dpdk的应用。下面的图给出了eth_vecring总体结构视图:
在容器启动前,应该配置mount hugetlbfs,并且将之映射到lxc容器中,接下来eth_vecring(master,通常处于host中的pmd称为master,容器中的pmd称为slave,用于区分对该pmd来说,共享内存的方向)pmd会自动探测大页并分配一定的内存和记录虚拟链路的元数据。接下来,虚拟链路两端的PMD可以以类似全双工的工作方式往这个buffer中以一定的数据格式读写数据帧。下面我们先看这个虚拟链路的测试性能。
测试拓扑如下:
即宿主机和容器中均使用pktgen-DPDK来对虚拟链路进行发/收包测试,使用4个开启超线程的core,且四个core均在同一个socket上。
DPDK虚拟链路的eal参数如下所示:
其中,domain为一个容器标识,Link为虚拟链路名称,mac为虚拟链路pmd端的mac,master在host中的pmd指定,容器端可以不用指定,socket优先分配大页的numa 节点。
对于64字节最小数据帧来说:
其单核收包或者发包的吞吐率在18Mpps以上。
对于1514字节数据包,单核收发包如下:
即2Mpps(25Gbps),如果两端同时发送和接收:
同时发送和接收后1514字节数据包,单核处理能力在1.3Mpps(16Gbps),原因是eth_vecring是基于内存在为IPC通道的,很可能受限于同一个socket的前端总线传输能力。
那么eth_vecring在跨numa的接收能力会有什么影响呢,经测试,总体吞吐率在64字节大小帧长下由最初的18Mpps下降到13Mpps,这是一个非常大的性能损耗,因此,numa在内存访问的时候,占据至关重要的因素。
向量化的Ring Buffer
ring buffer本质上是一个静态数组(连续内存空间)数据结构存储的队列,在eth_vecring,所有的队列元素大小一个cache line size ,在X86_64 上,64字节。且队列起始地址也按照64字节对齐(从而保证所有的队列元素都是对齐的,这一点尤为重要,因为很多SIMD指令对内存的访问要求地址按照一定的粒度对齐)。
eth_vecring传输的数据帧均会拷贝到队列中,这样所带来的好处是减少虚拟链路两端的依赖性。因此内存拷贝是性能所在的关键,在eth_vecring的是现在,我们以sse/sse2/sse4.1/sse4.2(128bit)指令和avx/avx2(256bit)来进行内存的操作。一个很明显的事实是:无论是拷贝进入队列的数据或者是从队列中拷贝出来的数据,在很长一段时间内很可能不被使用,因此这个过程是non-temporal。
所以,在数据拷贝过程中,我们没有必要将队列相关的数据让cpu load到cache中,即我们要bypass cache,这么做的好处就是PMD的行为(即大量的内存IO行为)不会干预并且污染真正数据面业务已经load到cache中的数据。
很可能有人会问,这种拷贝真的高效吗,因为数据离cpu越远,所需要的内存访问时钟周期越多,比如L1d cache的访问时钟周期为1-2 cycles,L2 cache 访问为12 cycles,memory 访问为~140 cycles,那么绕过cache 所带来的内存访问开销会大很多,但是由于writing-combine以及stream-loading-buffer的存在,这种non-temporal的行为在真正和memory交易的时候是以cache line size为单位的,并且如果虚拟链路的IO如果污染cache,那么给数据面业务带来的性能损失将会是巨大的,另一方面,cache bypassing很可能会在总线空闲的时候将内存写回write back到memory,因此并不见得cache bypassing会非常低效。
对队列的操作是以cache line size为单位的,接下来简述cache line copy的实现avx-256实现:
对于对齐的地址,我们可以使用单条stream load SIMD指令将地址中的32字节的数据load进ymm寄存器(编译器自动编排寄存器序号)中,紧接着stream store到要拷贝的地址中。对于没有对齐的地址拷贝,只能将数据load到cache中来。eth_vecring会自动判断地址的对齐与否。
数据包传输是以16字节的头部为标识的,eth_vecring在传输的时候采取了类似virtio-pci中的emergable特性将最多4(一个cache line最多容纳4个16字节头部)个数据包头部合并在一起传输,报文传输头部以及数据块定义在下面的代码框中:
除了对内存的布局和访问做出优化外,队列传输的代码均用SIMD指令进行优化完成,别且使用inline以及branch prediction来提高L1i指令cache 的效率,这里将不再详述。
总结
回到题目的问题,一条指令能传输一个以太网帧?当然不可能,但是这并不妨碍我们从另外一个角度看待platform friendly IPC对虚拟链路的提高有着巨大的潜力。
随着AVX-512指令集对内存访问的支持,引入512-bit的zmm寄存器,那么对于一个512bit的数据,一个vmovntdqa指令即可完成从寄存器到memory的数据传输,恰好cache line size是64字节,恰好以太网最小帧长是64字节。(作者:陈杰,UnitedStack有云)