引言

随着计算机硬件资源整合的发展,虚拟化的研究与应用日新月异。Virtio/Vhost作为一种设备虚拟化的典型应用,在业界受到了广泛的关注。Virtio 最开始由Rusty Russell在其2008年发表的论文[1]中提出,其先进性不言而喻,除了各种在Virtio Spec规范内对其的优化外,Virtio Spec本身也在逐步演化,到现在Virtio已经有了1.1版本。

异步模式的Vhost/Virtio就是这些优化之一。异步Vhost这个概念在DPDK中已经存在了一年多了,从一开始只支持split ring到后来DPDK 21.05里对packed ring支持的引入,可见异步Vhost已经日渐成熟。本文将从packed ring的ring结构,工作方式,其优缺点,以及异步模式的特点,实现与性能等方面逐步对其展开介绍。

Virtio & Vhost

虚拟化技术背景

在介绍Virtio/Vhost之前,虚拟化相关的技术背景不可不提。虚拟化(Virtualization)技术的普及,是计算机产业发展迅猛,硬件性能急速提升带来的必然结果。从根本上说,虚拟化是一种技术,这项技术可以利用局限于硬件的资源来创建灵活多变的 IT 服务。它让人们能够将物理计算机的工作能力分配给多个用户或多个环境,从而充分利用计算机的运算能力。

虚拟化技术的应用,优化的是计算机计算资源的分配,使得计算资源能够在一个相对细粒度的范畴进行切割分配。这实质上得利于计算机硬件的高速发展,单台物理机器的计算资源十分丰富,丰富到若以传统单台物理机作为基本资源切割单位话,容易造成计算资源的极大浪费。于是,虚拟化技术的应用需求应运而生。

虚拟化技术,基本可以分为两类,一类是全虚拟化(Full virtualization),一类是半虚拟化 (Paravirtualization)。下面将从这两个分类说起,简单聊聊他们各自的特性。

全虚拟化(Full virtualization),顾名思义,指的是完全虚拟化。使用这种虚拟化技术能让客户系统(Guest OS)不知道自己运行在一个虚拟环境中。全虚拟化技术提供底层物理系统的全部抽象。客户系统与运行在裸机上的系统相比并没有什么区别,这样带来的好处显而易见,就是模拟环境对客户机操作系统没有任何特别的要求。但是这样带来的坏处也不可忽视,就是由软件提供的硬件模拟势必带来一定的性能损耗。由于全虚拟化的性能损耗不可忽视,半虚拟化(Paravirtualization)找到了它的用武之地,这是另一种虚拟化热门技术。它使用虚拟机管理程序(Hypervisor)分享底层的硬件,客户操作系统集成了虚拟化相关的代码,这种方法因为操作系统自身能够与虚拟进程进行很好的协作,所以性能损失很小,大大提升了虚拟化的性能利用率。缺点就是客户系统中需要运行特定的驱动程序,客户系统需要感知到自己其实是一个虚拟系统,兼容性不是那么强。

由于半虚拟化技术具有非常强劲的性能和相对容易克服的缺点,使得它成为了目前业界的主流应用方案之一。Virtio/Vhost就是一套流行开放的半虚拟化前后端通信标准,在实际生产环境中应用非常广泛。用Virtio/Vhost这套标准可以实现很多设备的模拟,例如网络,硬盘,串口设备等。详细的Virtio/Vhost介绍可以查阅Virtio Spec[2]。本文主要叙述异步路径下packed ring的原理和实现,并与split ring做一定的对比分析。

Virtio与Vhost的基本通信原理

Virtio/Vhost是一套半虚拟化前后端通信标准,我们一般称Virtio为前端Driver,后端Vhost为Device。典型的Virtio/Vhost需要先通过socket通信建立数据面通信连接,数据面通信主要通过共享内存的方式进行。前后端收发包都是基于前端作为生产者,后端作为消费者的一套通信机制。例如前端要将数据发到后端,那么前端提供写好数据的内存区域,后端读取并复制到相应的地方。如果是前端要接收后端发送的数据,也是由前端提供空闲的内存区域,后端将要发送的数据复制到这些内存区域,整个过程简单且高效。

在Virtio 1.1以后存在两种前后端通信的queue结构。一种是一开始就存在的split ring结构,一种是在Virtio 1.1新引入的packed ring结构。下面我将讲讲这两种ring结构的异同,以及他们之间的性能对比,以帮助大家更好的了解packed ring。

Split Ring和Packed Ring

从split ring说起,ring结构上,split ring把descriptor环形缓冲区(descriptor ring),可用环形缓冲区(available ring)和已用环形缓冲区(used ring) 分别使用3个数据结构来表示,这也是他被叫做split ring的原因。前端作为生产者将新生产可给后端使用的descriptor在available ring中更新,然后由后端从available ring中读取到自己可用的descriptor进行使用,使用完以后将用完的descriptor写到used ring中,供前端处理,循环使用这些内存。这里所谓的使用,可以从别的地方把数据拷贝到这些descriptor所描述的内存区域中,也可以是从这些descriptor所描述的内存区域中把数据拷贝到别的地方。

packed ring是Virtio Spec 1.1提出了一种新的ring结构,前后端通信的逻辑与split ring基本相似,不过ring结构发生了些改变,将split ring中原本分离的ring结构整合在一起全部用一个ring 表示。虽然在操作上确实复杂了一些,但是在前后端收发包操作时packed ring模式下只需访问一个ring结构,相比于split ring每次需要操作3个ring而言,packed ring减少了所需访问的内存空间,对cache来说更加友好。

下图是packed ring的ring结构,这个ring是前后端都可见的,通过descriptor中的标记位判断这个descriptor的归属权在前端还是在后端。后端使用两个指针来追踪生产和消费的进度,分别为last_avail_idx和last_used_idx。前端作为生产者初始化这些descriptor,后端作为消费者不断的取用这些descriptor并在使用结束后将使用完的descriptor写回这个ring中,前端观察到这个descriptor被写回则更新这个descriptor,使得后端在下次可以继续使用。值得一提的是前端按照ring的顺序操作这些descriptor,而后端则是按照完成顺序操作这些descriptor,且在in-order模式下descriptor的可用性一定是连续的,既若X号descriptor不可用,则默认X+1号descriptor同样不可用。这样一套前后端的协作机制构成了packed ring的基本信息传递方式。

image-20220224221542937

Figure 1. descriptor ring结构

由于ring的操作方式发生了改变,使得两种ring的性能产生了一定的差异,从这里可以看到,从下图(本文不提供绝对性能数据)可以看到。这是PVP场景下,使用Intel(R) Xeon(R) Platinum 8180 CPU @ 2.50GHZ,前后端各自使用两个Core,Vhost使用两个queue的性能对比。

image-20220224221603068

Figure 2. split ring 与 packed ring的性能对比

从图中可以发现,packed ring的性能相比split ring有一定的提升,这一部分来自于更加优化的ring结构,还有一部分来自于DPDK Vhost lib 中对packed ring的批量处理优化。

异步Vhost

在分别了解完split ring和packed ring之间的异同后,想必大家应该可以更好理解我们异步Vhost的设计。不过在正式讲我们的异步Vhost设计之前,可能需要简单聊一聊什么是异步,什么是同步。

同步与异步

在软件设计中,同步模式与异步模式的例子数不胜数。所谓同步模式,通俗讲就是我们假定有一个任务需要被完成,在这个任务结束前CPU不会做其他的事情,一直等待这个任务完成,然后才可以进入下一个阶段。而所谓异步模式,就是这个任务交给别的Core或者别的设备完成,这个线程的CPU并不需要在这等待这个任务的完成,而且去做别的事情,等待这个任务被完成了以后,通过轮询或者中断等方式告知这个CPU这个任务已完成。通过下面两张图举例我们可以清晰的认识到这两种编程模型的区别。

image-20220224221614405

Figure 3. 同步模式与异步模式的区别

以上图为例,左边表示的是同步模式下的软件工作方式,右边表示的是异步模式下的软件工作方式。从左边我们可以看到,这个工作在完成时,CPU必须要在原地等待,直到这个任务完成了,我们才可以接着做别的事情。接着看到右图的异步模式,相比于左图主要区别在于在这个任务被完成的期间,CPU是可以去做别的事情的。异步模式在一定程度上实现了多个任务的并行处理,节省了运行时间。这也是异步模型的主要优势之一。

异步Vhost设计与实现

传统模式下的Virtio/Vhost数据路径使用CPU拷贝数据报文,CPU拷贝是阻塞性的,其特点在于拷贝的过程需要被等待,要等拷贝完了之后 CPU 才能去做别的事情。在拷贝大批量数据时,Vhost端的数据拷贝成为了整个通信过程中的性能瓶颈,我们可以称这个为同步模式的Vhost。在这个基础上,我们提出了Vhost的异步模型,将descriptor的处理工作交给CPU完成,将数据拷贝交给Intel CPU上的DMA引擎CBDMA、DSA上进行处理。

➢ 异步模式下的Vhost Packed Ring

上文已经简单介绍过packed ring的结构,Vhost为了运行效率会在本地保存一个已用环形缓冲区的备份,以减小更新descriptor ring的频率,提升运行效率。详细的运行机制,如下图所示:

image-20220224221624700

Figure 4. descriptor 消费过程

Async shadow used ring就是Vhost后端在本地缓存的一个used ring的缓冲区,后端取用descriptor后,由于DMA拷贝的特性,拷贝任务并不会马上完成,所以在DMA拷贝完成之前,我们会将这个descriptor的信息暂存在Async shadow used ring中,在DMA拷贝完成后将 Async shadow used ring中的内容一起写回 descriptor ring中,并通知前端进行回收处理。

这是异步模式跟同步模式的主要区别。那我们怎么知道DMA会在什么时候完成数据拷贝呢?这个时候就要了解到Async Vhost enqueue的两个 API 了。

异步路径的enqueue操作有两个 API 组成:

rte_vhost_submit_enqueue_burst()  
rte_vhost_poll_enqueue_completed()

rte_vhost_submit_enqueue_burst()会将相应需要enqueue的包进行enqueue操作, rte_vhost_poll_enqueue_completed()会查询目前DMA数据拷贝的完成情况,只有查询到包的数据已经完成了相应的拷贝,才能将对应的暂存在Async shadow used descriptor中的元素写回 used ring。这是在 API 层面上异步路径和同步路径最主要的差别。

rte_Vhost_submit_enqueue_burst()函数的调用会将内存拷贝交由DMA处理后继续处理下一批数据包,在这里将descriptor处理和内存拷贝两件工作在时间上重叠起来,提升了数据传输的总体性能。

➢性能表现与分析

说了那么多原理和实现,那么接下来可以看看 Async Vhost packed ring的性能吧。测试场景为physical port -> virtual port -> physical port (PVP)场景,如下图所示:

image-20220224221637214

Figure 5. PVP测试拓扑

环境配置:

  • CPU: Intel(R) Xeon(R) Platinum 8180 CPU @ 2.50GHZ
  • NIC: Ethernet Controller E810-C for QSFP 1592
  • Forwarding Cores:2
  • Forwarding mode:mac forward
  • Queues: 2

固定包长的包从pktgen中发出,经过几次转发最后返回pktgen,统计不同包长时,pktgen收包的速率。对比使用packed ring的异步路径和同步路径的性能差异,异步路径使用CBDMA设备做内存拷贝,同步路径使用 CPU 做内存拷贝。

image-20220224221648101

Figure 6. PVP测试性能对比图

可以看到,在512B时异步路径的优势刚开始显现,这主要是由于大包在传输时,内存拷贝开销所占比逐渐提升。包越大,内存拷贝需要的资源则越多,CBDMA内存拷贝所带来的性能提升也就越明显。

总结

将 CPU 不擅长的工作卸载到专用的硬件加速器上在性能优化领域是一个不错的思路,特别是在这项工作占比很大的时候,在很多应用场景上也得到了充分的印证。本文介绍了使用CBDMA加速Vhost packed ring的原理和实例以及性能提升,由此可以看出,Async Vhost对Vhost性能的提升还是非常明显的,在相似场景的处理上具有不错的借鉴意义。


参考文献

  1. https://dl.acm.org/doi/pdf/10.1145/1400097.1400108
  2. https://docs.oasis-open.org/Virtio/Virtio/v1.1/cs01/Virtio-v1.1-cs01.html