VirtIO vHost VFIO vDPA vDUSE IO 虚拟化演进之路
主要参考文章:Linux网络虚拟化技术的演进之路 - 知乎
这个 RedHat 的系列博客讲的也非常好,有技术 overview 也有 deep dive,值得一看:Virtio-networking: first series finale and plans for 2020
Trap-and-emulate -> VirtIO (2008) -> Vhost (2010) -> VFIO (2012) -> Vhost-user (2014) -> VFIO-mdev (2016) -> vDPA (2020) -> VDUSE (2021).
VFIO 能够直接将硬件资源透传给虚机使用,性能最佳,VirtIO 性能稍逊,但胜在更加灵活。那有没有可能将两者的优势结合起来呢?vDPA 技术框架,就是为了实现这一目标。
演进的顺序貌似是:全虚拟化->virtio->vhost->vfio->vdpa->vduse。
总结:
- VirtIO
->VirtIO + irqfd/ioeventfd(VirtIO-fd):vCPU 线程只 exit 到 KVM,不必到 QEMU;事件通过 fd 路由到 QEMU mainloop thread; - VirtIO-fd
->VirtIO-fd + iothread(VirtIO-fd-IOT):事件通过 fd 路由到专门的 iothread,不 block mainloop thread 做重要的事情; - VirtIO-fd-IOT
->vHost:vHost 引入一个协议,不和 QEMU 强绑定,可以路由到任何一个进程;- vhost-kernel:路由到内核线程;
- vhost-user:路由到用户态驱动/DPDK。
- vHost
->vDPA:数据通路的这个线程,硬件来实现,进一步提升性能,同时保留了灵活性; - vDUSE:让 vDPA 从虚拟化架构中脱离出来,让 Host 上的其他进程也可以使用设备,适合 VM/ Container 混部场景。
为什么 VFIO/SRIOV 不支持热迁移而 VirtIO 可以?
什么是 VirtIO 里的数据链路(数据面 datapath)和控制链路(控制面 control path)?
因为 VirtIO 设备端以一个 PCI 设备的方式呈现给 guest。
控制面用于在 host/guest 之间协商设备能力。
- 控制链路:控制链路用于设备管理和配置,例如初始化设备、设置参数、查询状态或发送控制命令。控制建立或删除 Front-end 和 Back-end 之间的数据路径,提供可管理的灵活性。Ring buffer 和 Descriptor table 的内存地址,驱动如何告知设备比如
irqfd&ioeventfd,那些 ,驱动感知设备支持的特性等等。vHost 下,控制面基于 VirtIO Spec 在 QEMU 中实现,但数据平面并没有。 - 数据链路:在前端(驱动)和后端(设备)之间传输数据,性能优先。virtqueue 里面的各个 entry 所指向的内存地址所对应的内存区域的填写/读取。比如说当报文到达内核 TAP 设备时候,内核发出并送到 QEMU 的通知消息,然后 QEMU 需要把数据拷贝到 guest 内存中并通知 guest。
对应地,VirtIO 标准也可以分为两个部分:
- VirtIO Spec:定义了如何在 Front-end 和 Back-end 之间构建控制路径和数据路径的标准,例如:数据路径规定采用环形队列缓冲区布局。
- VHost Protocol:定义了如何降数据路径的高性能实现标准,例如:基于 Kernel、DPDK、SmartNIC Offloading 的实现方式。
从全虚拟化到 VirtIO
请看 VirtIO 相比于 full virtualization I/O 的优势是什么?^
从 VirtIO 到 vHost
牢记:无论是 VirtIO/vHost/vhost-user 解决的都是 KVM 的 irqfd/ioeventfd 和谁通信的问题,都在数据通路中:
- VirtIO:和 QEMU mainloop 或者 QEMU iothread 通信;
- vHost:和 vhost 的内核线程通信;
- vhost-user:和用户态驱动或者 DPDK 等等进程进行通信。
因此,vHost 本质上是一个协议(protocol),起初 VirtIO 数据面和控制面都是在 QEMU 中实现的,vHost 定义了如何把 VirtIO 数据面从 QEMU 中剥离出来 offload 到一个第三方组件,无论是一个 kernel module(vhost)还是一个用户态组件(vhost-user OVS DPDK)还是一个用户态驱动。
ioeventfd 的重点在于解决 vCPU 线程被 block 的 vCPU 性能问题,而 vHost 解决的是设备的性能问题(存储,网络收包等等)。
VirtIO 的控制面基于 Spec 在 QEMU 中实现,但数据平面并没有。为什么呢?答案是性能。如果我们简单地在 QEMU 中实现 VirtIO 数据面(人话来讲,就是在 QEMU 调用各种 send/recv/open 等等 syscall 来操作设备发送),我们将为每个从 host kernel 到 guest 的数据包进行上下文切换,反之亦然。这是一项代价高昂的操作,会增加延迟并需要更多处理时间(请记住,QEMU 是另一个 Linux 进程),因此我们希望尽可能避免它。
这就是 vHost protocol 发挥作用的地方,使我们能够实现一个 VirtIO 数据面,绕过 QEMU 直接从 host kernel 进入 guest。 vhost protocol 本身只描述了如何建立数据面。它的实现者还需要实现 buffers/rings 内存布局来识别 guest/host 的数据缓冲区,并进行实际的数据包收发。 vhost protocol 既可以在内核态实现 (vhost-net) 也可以在用户态实现 (vhost-user)。
尽管 VM 主动通知设备的这个通知机制通过 ioeventfd^ 不需要 exit 到 vCPU 线程的 userspace 而是让 iothread 通过 ioeventfd 来进行处理了;但是从另一个数据流方向来看,当报文到达内核 TAP 设备时候,host 内核发出并送到 QEMU 的通知消息(return to userspace),然后 QEMU 利用 irqfd 向 KVM 发送中断(enter to sys again),KVM 发送中断到 VM,这样先从内核态到用户态、再从用户态回到内核态的路径带来了不必要的开销。这会导致 IO 性能不佳。
VirtIO 通信机制从原本的 QEMU 用户态 I/O 线程和 guest virtio driver (non-root kernel mode) 通信直接变成了 vHost 内核 I/O 线程和 guest driver 通信。vHost 内核 I/O 线程拿到数据包之后,直接走内核协议栈和网卡驱动进行处理,从而优化掉了 QEMU 到内核态的额外开销。相当于数据通路没有 QEMU 什么事,直接 bypass 掉了。
仍然有两个通路:
- 数据通路是从 tap 设备接收数据报文,内核的 vhost-net 模块把该数据报文拷贝到 virtqueue,从而使得客户机接收报文。
- 消息通路是当报文从 tap 设备到达 vhost-net 时候,QEMU irqfd 进行通知。
原始的 VirtIO 控制面数据面都是在 QEMU 里实现的。irqfd 仅仅是把处理从 vCPU 线程 offload 到 IO 线程了,并没有避免 user-sys context switch。vHost 把数据面 offload 到了 kernel 中,这样数据的转发不需要 user-sys context switch。
QEMU 不参与通信,但也没有完全退出舞台,它还要负责一些控制层面的事情,比如和 KVM 之间的控制指令的下发等。
Deep dive into Virtio-networking and vhost-net
VirtIO 和 vHost 在实现上的区别:
- 基于 shared memory 的 vring/virtqueue 以前是 QEMU 这里的 device 端,申请在自己的用户态内存空间中的,现在直接由 vHost 内核线程申请,放在 kernel space 了;
-
irqfd和ioeventfd以前是 QEMU 注册,现在应该是 vhost 内核线程来打开并注册了。
VirtIO (有 irqfd/ioevenetfd 但是还没有使用 iothread 的情况),可以看到 ioeventfd/irqfd 都由 mainloop thread 来处理,也就是直接上报给了 mainloop thread,并不是 vCPU thread,不需要 vCPU thread 再往上上报 mainloop 这一层了:
vHost:
从 vhost-net 到 vhost-user
Move the dataplane from the kernel to userspace. vhost 是一个协议,定义了如何 offload VirtIO 数据面到第三方(不管是 kernel model 还是一个用户态的程序)。实际上目前主要在 DPDK 中实现了 vhost-user,也就是 DPDK 中有 vhost-user 的 library:3. Vhost Library — Data Plane Development Kit 25.07.0-rc2 documentation
其实从数据面本身的处理来说,从 kernel 移到 userspace 没有什么多余要更改的代码,但是在和 QEMU 中的控制面来交互上来说,vhost 和 vhost-user 的主要区别还是在这里的(一个基于 ioctl,一个基于 UDS 或者其他 socket):
https://k48xz7gzkw.feishu.cn/docx/QcsXdAc3PoF6VHxhHM5cPeuHnhe#share-EZOldO82SodkuqxR0s0cjzpInMg
vhost-user 相比于 vhost-net 并不是改进关系,而是各自有各自的使用场景。
- vHost 适用数据源是 TAP 设备,也就是直接从内核过来的数据包;
- vhost-user 适合的数据源是 OVS DPDK,工作于 userspace(DPDK),所以我们应该从 userspace 来接数据,那就没必要把数据面再放到 kernel space 了。guest 要发数据,直接 exit 到 KVM 后,通过各种 fd 和 userspace 的 DPDK 通信,然后 DPDK 直接发走就好了。
- 如果要进一步 OVS offload 到 CX 网卡上做 vRDMA 的话,数据通路直接用硬件实现,这就是 vDPA^ 了。
由于:
- QEMU 和 vHost 的线程模型对 I/O 性能的优化并不友好,而且
- 由每个虚机单独分出线程来处理 I/O 这种方式从系统全局角度来看可能也并不是最优的(一个 QEMU 虚拟机都有自己的 io kernel thread,有必要一个虚拟机一个吗?)
因此,vhost-user 提出了一种新的方式,即将 virtio 设备的数据面 offload 到另一个专用进程来处理。这样,
- 由于是专用进程,线程模型不再受传统 QEMU 和 vhost 线程模型制约,可以任意优化,
- 可以以 1: M 的方式同时处理多个虚机的 I/O 请求,不需要每一个虚拟机都有自己的 IO 线程,
- 而且相较于 vhost 这种内核线程方式,用户进程在运维方面更加具备灵活性。
DPDK 就是利用了 vhost-user。
纯软件的优化,基本上到了 vhost 和 vhost-user 已经是极致了,后面的一些技术比如 vDPA 都要用到硬件卸载设备直通了。
vhost(vhost-user)网络I/O半虚拟化详解:一种 virtio 高性能的后端驱动实现-CSDN博客
Deep dive into Virtio-networking and vhost-net
VFIO-mdev
mdev 本质上是在 VFIO 层面实现 VF 功能。算是对没有 SRIOV 情况的一个补充。
有些场景下,我们希望能把后端的硬件设备切分成更小的实例提供给更多的 VM 来使用,但是这个设备又没有 SRIOV 或者 SIOV 的能力 (比如 GPU,NVME) 那该怎么办呢?别着急,VFIO-mdev 框架就是为了解决这样的问题而生的。
如果一个设备需要给多个进程提供用户态驱动的访问能力,这个设备在 probe 的时候可以注册到 mdev 框架中,成为一个 mdev 框架的 pdev。之后,用户程序可以通过 sysfs 创建这个 pdev 的 mdev。
vDPA (VirtIO Data Path Acceleration)
RedHat 提出的(2019 ~ 2020?)。vDPA 的介绍网站:vDPA - virtio Data Path Acceleration - vDPA - virtio Data Path Acceleration
VirtIO 控制面软件模拟,数据面硬件实现,控制面复杂用硬件实现难,数据面简单用硬件实现容易。
是把 vHost 内核线程做的事情 offload 到硬件上,进一步提升性能
VFIO 能够直接将硬件资源透传给虚机使用,性能最佳,VirtIO 性能稍逊,但胜在更加灵活。那有没有可能将两者的优势结合起来呢?vDPA 技术框架,就是为了实现这一目标。
注意,vDPA 并没有设备直通,只是在性能上接近设备直通的性能。也就是设备还是在 host 上管理。
它表示一类设备:这类设备的数据面处理是严格遵循 VirtIO 协议规范的,即驱动和设备会按照第三节提到的 VirtIO 通信流程来进行通信,但控制路径,比如:通信流程里面提到的 ring buffer 和 descriptor table 的内存地址,驱动如何告知设备,设备支持的特性,驱动如何感知,这些都是厂商自定义的。这样做的好处是,可以降低厂商在实现这类设备时的复杂度。
vDPA 相比于直通普通设备的好处在于灵活性,那么灵活在哪里呢?
Guest 对于一类设备只需要一种驱动(net, blk),不需要每一个厂商都维护自己的驱动了。同时我们可以虚拟化出很多个设备给很多个 Guest 展示,后端的数据通路处理线程直接在硬件上跑,可以通过分时复用或者某种方式,相比于 VFIO,这种方式不受 VF 数量限制,因为想虚拟化出多少个设备就可以虚拟化出多少个给 guest,同时性能上也损失很少,因为 IO 都是在硬件上做的。
vDPA 设备能在 host 上运行吗?
理论上应该可以,毕竟 host 上也是有 VirtIO 驱动的。
MLX 的 ConnectX 支持 vDPA,那么我们 vRDMA 不是说要 OVS 卸载到物理网卡上处理而不是继续使用 DPDK 从而能够节省 CPU 核吗?是不是就是利用了 vDPA 来做 offload?不是,是直接基于 VF 的。详见 Switchdev^
vDPA 支持热迁移。
从 vDPA 到 vDUSE
vDUSE 是字节在 2020 年提出的。vDPA device in userspace. 看下面图的红色部分就一目了然了。
| ![[vDUSE.pdf#page=13&rect=456,292,1502,861&color=annotate | vDUSE, p.13]] |
vDPA 由硬件实现,Intel 有自己的硬件驱动,mellanox 有自己的硬件驱动,硬件驱动生成一个 vdpa_device,挂载到 vdpa_bus 上。vDPA 也可以由软件实现,Intel 在内核写了一个 vdpa_sim,用户态用软件模拟 vDPA 硬件就是 VDUSE,用户态和内核态建立通道,用户态通过这个通道给内核注册一个 vdpa_device,这个 device 收到 vDPA 控制信息转发给用户态,数据面通过用户态和内核态共享内存实现。
好处就是如果硬件支持 vDPA 那么用硬件的,硬件不支持则用软件实现的 vDPA,上层使用 vDPA API 可以不需要必须要有对应硬件设备,屏蔽底层差异。软件实现在用户态更简单,很多硬件实现了网络 IO vDPA 加速,但目前没有看到硬件实现存储 IO vDPA 加速,字节跳动提出的 VDUSE 借助内核 vDPA 框架统一了容器和虚拟机的存储,如果哪天硬件实现了存储 IO vDPA 加速,线上切换到硬件方案相对来说比较容易。
vDUSE 最大的用途在于支持了 host 上的容器使用软件实现的 VirtIO 设备。在容器不感知后端实现,使用标准内核接口的情况下,可以在用户态处理容器应用的网络/IO 操作。 所以这种架构也可以用来实现云盘(把云盘虚拟化成一个 vdpa_device 就可以了)。
(6 封私信 / 18 条消息) 虚拟化硬件加速-vdpa - 知乎
Introducing VDUSE: a software-defined datapath for virtio
都是到用户态,和 vhost-user 的区别是什么?
在大部分场景下,vDUSE 的功能都可以用 vhost-user 代替,例如 VM 场景。vhost-user 与 vDUSE 的数据面处理效率基本是相同的,都是通过 virtqueue 直接共享内存交互。但显然 vhost-user 的 kick 和 irq 路径要比 vDUSE 短一些,性能也会更好一些。vDUSE 的优势还是能通过一种尽可能透明、无感知的方式为 host 上的应用或容器提供虚拟设备。不过理论上说,完全可以通过提供一些更直接的内核接口,让 vhost-user 进程直接使用这个云盘设备。从这个角度看,vDUSE 的优点是复用了内核已有的 vDPA 框架,不需要再增加一套内核虚拟设备框架让 vhost-user 来操作。
vDUSE 另一个理论上的优点是可以与 vDPA 物理设备协同工作,在有 vDPA 设备的机器上作为 vDPA 的备选设备实现,一旦 vDPA 设备故障或不可用时可以将 virtio 设备的后端切换到 vDUSE 实现上,保证 VM 或容器可以继续运行。不过这个场景能遇到的可能性似乎更少,为了这么罕见的场景开发和部署 vDUSE 的可能性不大。
vHost
The vHost drivers in Linux provide in-kernel VirtIO device emulation. It allows offloading the data plane to kernel (control plane is still in QEMU). 没有 vHost 的时候,data plane 也是在 QEMU 的 iothread 的,这就导致有很多不必要的从 kernel 到 QEMU 的 context switch,从而影响了性能。
We can split virtio into two parts:
- virtio spec - defines how to create a control plane and the data plane between the guest and host. For example the data plane is composed of buffers and rings layouts which are detailed in the spec.
- vhost protocol - A protocol that allows the virtio dataplane implementation to be offloaded to another element (user process or kernel module) in order to enhance performance.
Introduction to virtio-networking and vhost-net
QEMU 代码里 qemu/hw/virtio/vhost* 仅仅是 vhost 的 control plane。
可以看这篇文章再深入了解一下:Virtio and Vhost Architecture - Part 2 · Better Tomorrow with Computer Science
It is usually called virtio when used as a front-end driver in a guest operating system or vhost when used as a back-end driver in a host.
So IMO they are basically the same thing in different context.
因为 frontend driver 和 backend driver 都实现在了 kernel 中,为了进行区分,我们把 host 部分的代码叫做 vhost。
有一个目录叫做:drivers/vhost/,里面包含了所有 vhost 相关的代码。而 guest 相关的代码大部分在 drivers/virtio/ 下面。
vhost kernel module is implemented in linux/drivers/vhost, and control plane is implemented in hw/virtio/vhost*.
Stefan Hajnoczi: QEMU Internals: vhost architecture
Virtio and Vhost Architecture - Part 1 · Better Tomorrow with Computer Science
vHost 实操
主要参考文章:
创建网络:
user@host $ virsh net-define /usr/share/libvirt/networks/default.xml
Network default defined from /usr/share/libvirt/networks/default.xml
user@host $ virsh net-start default
Network default started
user@host $ virsh net-list
Name State Autostart Persistent
--------------------------------------------
default active no yes
创建虚拟机(可以看到,网络这里使用了 default,也就是我们刚刚创建的):
user@host $ virt-install --import --name virtio-test1 --ram=4096 --vcpus=2 \
--nographics --accelerate \
--network network:default,model=virtio --mac 02:ca:fe:fa:ce:01 \
--debug --wait 0 --console pty \
--disk /var/lib/libvirt/images/virtio-test1.qcow2,bus=virtio --os-variant fedora30
检查一下这个虚拟机的配置:
user@host $ virsh dumpxml virtio-test1
<devices>
...
<interface type='network'>
<mac address='02:ca:fe:fa:ce:01'/>
<source network='default' bridge='virbr0'/>
<target dev='vnet0'/>
<model type='virtio'/>
<alias name='net0'/>
<address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> </interface>
...
</devices>
可以看到已经创建了一个 VirtIO 设备。