在上期,我们提到,子虚将SPDK的轮询机制,与JFZ女士的日本游记中的画面进行了联系,虽然觉得自己在隐秘地开车,但还是留下了证据。
为了让方老师不把自己开车的证据说出去,子虚主动提出,捐献一篇SPDK的学习心得,这就是SPDK在虚拟化中的应用——vhost加速。
我们知道,Linux下,KVM虚拟机的IO设备,实际上有三种工作方式:
- 纯模拟方式,通过软件模拟现实中不存在的硬件;
- 半模拟方式,在GuestOS内核中安装虚拟化硬件的前端(Frontend)驱动,同时在宿主机的Hypervisor上提供后端(Backend)驱动接口。常见的Ceph RBD和腾讯云的CBS驱动就是这种工作方式,在虚拟机中可以看到存储卷/dev/vd*,就是Frondend驱动识别出来的虚拟卷设备;
- 硬件虚拟化,让PCIe设备支持SR-IOV,虚拟化为多个设备,这种工作方式常见于网卡(NIC),对于存储设备很少见。
那么,我们有没有办法通过SPDK来加速虚拟机对NVMe SSD的访问呢?
这就涉及到今天的主题——SPDK vhost guest.
我们刚才提到,在KVM虚拟出的虚拟机中,挂载的云存储卷的设备名一般为vd*, 如vda, vdb, vdc... 而挂载在本地的硬盘设备名一般为sda, sdb... 这就是因为,QEMU为虚拟机提供了backend的虚拟化存储设备驱动接口,而在GuestOS操作系统中内置了这些虚拟化设备的Frontend驱动,这种方案叫做VirtIO。
VirtIO对于VM的GuestOS,虚拟出了一个PCI设备,当然也支持PCI-SIG的规范,如配置空间(PCI寄存器)和中断等功能。VM的GuestOS操作VirtIO的块设备时,实际上其驱动会将SCSI命令字写入一个队列,并向这个虚拟的PCI设备写入队列ID。由于写入PCI设备的配置空间属于特权指令,每次这种写入行为都会触发VM_EXIT。可想而知地,这样的实现成为了性能提升的卡点。
以读写NVMe为例:
- 操作系统将写命令写到写队列;
- 操作系统向NVMe控制器的PCIe配置空间(PCIe寄存器)写入命令字(也就是Doorbell Singaling);
- NVMe控制器在步骤2的触发下,去队列取命令;
- NVMe控制器执行写入命令;
- NVMe控制器向写完成队列反馈写成功消息,同时消耗一个队列令牌;
- NVMe控制器发起PCIe的MSI-X中断;
- 操作系统处理MSI-X中断,在中断处理例程或Linux的中断下半部中,处理写成功消息;
- 操作系统通过写NVMe控制器的PCIe配置空间,将写成功消息队列令牌返回给NVMe控制器
显然,在虚拟化系统中,步骤2/6/8都会产生VM_EXIT,也就是造成了性能提升的卡点。
SPDK vhost,就是通过基于SPDK实现virtio。SPDK在后端通过轮询机制,读取Frontend驱动向队列中写入的SCSi命令字,并且利用零拷贝技术消除VirtIO的性能卡点。
对于读写NVMe控制器的PCIe的空间造成的VM_Exit,SPDK运用了一个巧妙的技巧:利用NVMe 1.3中的shadow doorbell机制,也就是让虚拟机并不真的向NVMe的PCIe配置空间寄存器中写入doorbell触发NVMe控制器操作,而是模拟一个软件控制器,向软件控制器写入doorbell并不是特权指令,也不会产生VM_EXIT。
在上图中可见,Virtio-SCSI驱动不产生PCIe的MSI-X中断,改为虚拟化侧的SPDK vhost对队列进行轮询。进一步地,Virtio-SCSI驱动还可以用unix domain socket机制,来代替doorbell来通知QEMU。
这样,KVM/QEMU就巧妙地消除了VM_EXIT和内存拷贝的开销,让NVMe的高IO尽量少消耗宝贵的CPU指令周期,让单VM的IO突破百万的上限。
但是,我们发现,这样的机制只能让虚拟机访问本地的NVMe盘。对于分布式块存储的场景,有没有好的办法让虚拟机访问远端云盘也取得同样的加速效果呢?
请看下回分解。