一 简介前端
用户对超高并发、超大规模计算等需求推进了存储硬件技术的不断发展,存储集群的性能愈来愈好,延时也愈来愈低,对总体IO路径的性能要求也愈来愈高。在云硬盘场景中,IO请求从生成到后端的存储集群再到返回之间的IO路径比较复杂,虚拟化IO路径尤为可能成为性能瓶颈,由于虚机内全部的IO都须要经过它下发给后端的存储系统。咱们使用了SPDK来优化虚拟化IO路径,提出了开源未解决的SDPK热升级和在线迁移方案,而且在高性能云盘场景中成功应用,取得了不错的效果,RSSD云硬盘最高可达120万IOPS。本文主要分享咱们在这方面的一些经验。后端
二 SPDK vhost的基本原理数组
SPDK(Storage Performance Development Kit )提供了一组用于编写高性能、可伸缩、用户态存储应用程序的工具和库,基本组成分为用户态、轮询、异步、无锁 NVMe 驱动,提供了从用户空间应用程序直接访问SSD的零拷贝、高度并行的访问。网络
在虚拟化IO路径中,virtio是比较经常使用的一种半虚拟化解决方案,而virtio底层是经过vring来通讯,下面先介绍下virtio vring的基本原理,每一个virtio vring 主要包含了如下几个部分:架构
desc table数组,该数组的大小等于设备的队列深度,通常为128。数组中每个元素表示一个IO请求,元素中会包含指针指向保存IO数据的内存地址、IO的长度等基本信息。通常一个IO请求对应一个desc数组元素,固然也有IO涉及到多个内存页的,那么就须要多个desc连成链表来使用,未使用的desc元素会经过自身的next指针链接到free_head中,造成一个链表,以供后续使用。并发
available数组,该数组是一个循环数组,每一项表示一个desc数组的索引,当处理IO请求时,从该数组里拿到一个索引就能够到desc数组里面找到对应的IO请求了。异步
used 数组,该数组与avail相似,只不过用来表示完成的IO请求。当一个IO请求处理完成时,该请求的desc数组索引就会保存在该数组中,而前端virtio驱动获得通知后就会扫描该数据判断是否有请求完成,若是完成就会回收该请求对应的desc数组项以便下个IO请求使用。async
SPDK vhost的原理比较简单,初始化时先由qemu的vhost驱动将以上virtio vring数组的信息发送给SPDK,而后SPDK经过不停的轮寻available数组来判断是否有IO请求,有请求就处理,处理完后将索引添加到used数组中,并经过相应的eventfd通知virtio前端。函数
当SPDK收到一个IO请求时,只是指向该请求的指针,在处理时须要能直接访问这部份内存,而指针指向的地址是qemu地址空间的,显然不能直接使用,所以这里须要作一些转化。高并发
在使用SPDK时虚机要使用大页内存,虚机在初始化时会将大页内存的信息发送给SPDK,SPDK会解析该信息并经过mmap映射一样的大页内存到本身的地址空间,这样就实现了内存的共享,因此当SPDK拿到qemu地址空间的指针时,经过计算偏移就能够很方便的将该指针转换到SPDK的地址空间。
由上述原理咱们能够知道SPDK vhost经过共享大页内存的方式使得IO请求能够在二者之间快速传递这个过程当中不须要作内存拷贝,彻底是指针的传递,所以极大提高了IO路径的性能。
咱们对比了原先使用的qemu云盘驱动的延时和使用了SPDK vhost以后的延时,为了单纯对比虚拟化IO路径的性能,咱们采用了收到IO后直接返回的方式:
1.单队列(1 iodepth, 1 numjob)
qemu 网盘驱动延时:
SPDK vhost延时:
可见在单队列状况下延时降低的很是明显,平均延时由原来的130us降低到了7.3us。
2.多队列(128 iodepth,1 numjob)
qemu 网盘驱动延时:
SPDK vhost延时:
多队列时IO延时通常会比单队列更大些,可见在多队列场景下平均延时也由3341us降低为1090us,降低为原来的三分之一。
三 SPDK热升级
在咱们刚开始使用SPDK时,发现SPDK缺乏一重要功能——热升级。咱们使用SPDK 并基于SPDK开发自定义的bdev设备确定会涉及到版本升级,而且也不能100%保证SPDK进程不会crash掉,所以一旦后端SPDK重启或者crash,前端qemu里IO就会卡住,即便SPDK重启后也没法恢复。
咱们仔细研究了SPDK的初始化过程发现,在SPDK vhost启动初期,qemu会下发一些配置信息,而SPDK重启后这些配置信息都丢失了,那么这是否意味着只要SPDK重启后从新下发这些配置信息就能使SPDK正常工做呢?咱们尝试在qemu中添加了自动重连的机制,而且一旦自动重连完成,就会按照初始化的顺序再次下发这些配置信息。开发完成后,初步测试发现确实可以自动恢复,但随着更严格的压测发现只有在SPDK正常退出时才能恢复,而SPDK crash退出后IO仍是会卡住没法恢复。从现象上看应该是部分IO没有被处理,因此qemu端虚机一直在等待这些IO返回致使的。
经过深刻研究virtio vring的机制咱们发如今SPDK正常退出时,会保证全部的IO都已经处理完成并返回了才退出,也就是所在的virtio vring中是干净的。而在乎外crash时是不能作这个保证的,意外crash时virtio vring中还有部分IO是没有被处理的,因此在SPDK恢复后须要扫描virtio vring将未处理的请求下发下去。这个问题的复杂之处在于,virtio vring中的请求是按顺序下发处理的,但实际完成的时候并非按照下发的顺序的。
假设在virtio vring的available ring中有6个IO,索引号为1,2,3,4,5,6,SPDK按顺序的依次获得这个几个IO,并同时下发给设备处理,但实际可能请求1和4已经完成,并返回了成功了,以下图所示,而2,3,5,6都尚未完成。这个时候若是crash,重启后须要将2,3,5,6这个四个IO从新下发处理,而1和4是不能再次处理的,由于已经处理完成返回了,对应的内存也可能已经被释放。也就是说咱们没法经过简单的扫描available ring来判断哪些IO须要从新下发,咱们须要有一块内存来记录virtio vring中各个请求的状态,当重启后可以按照该内存中记录的状态来决定哪些IO是须要从新下发处理的,并且这块内存不能因SPDK重启而丢失,那么显然使用qemu进程的内存是最合适的。因此咱们在qemu中针对每一个virtio vring申请一块共享内存,在初始化时发送给SPDK,SPDK在处理IO时会在该内存中记录每一个virtio vring请求的状态,并在乎外crash恢复后能利用该信息找出须要从新下发的请求。
四 SPDK在线迁移
SPDK vhost所提供的虚拟化IO路径性能很是好,那么咱们有没有可能使用该IO路径来代替原有的虚拟化IO路径呢?咱们作了一些调研,SPDK在部分功能上并无现有的qemu IO路径完善,其中尤其重要的是在线迁移功能,该功能的缺失是咱们使用SPDK vhost代替原有IO路径的最大障碍。
SPDK在设计时更可能是为网络存储准备的,因此支持设备状态的迁移,但并不支持设备上数据的在线迁移。而qemu自己是支持在线迁移的,包括设备状态和设备上的数据的在线迁移,但在使用vhost模式时是不支持在线迁移的。主要缘由是使用了vhost以后qemu只控制了设备的控制链路,而设备的数据链路已经托管给了后端的SPDK,也就是说qemu没有设备的数据流IO路径因此并不知道一个设备那些部分被写入了。
在考察了现有的qemu在线迁移功能后,咱们觉着这个技术难点并非不能解决的,所以咱们决定在qemu里开发一套针对vhost存储设备的在线迁移功能。
块设备的在线迁移的原理比较简单,能够分为两个步骤,第一个步骤将全盘数据从头至尾拷贝到目标虚机,由于拷贝过程时间较长,确定会发生已经拷贝的数据又被再次写入的状况,这个步骤中那些再次被写脏的数据块会在bitmap中被置位,留给第二个步骤来处理,步骤二中经过bitmap来找到那些剩余的脏数据块,将这些脏数据块发送到目标端,最后会block住全部的IO,而后将剩余的一点脏数据块同步到目标端迁移就完成了。
SPDK的在线迁移原理上于上面是相同的,复杂之处在于qemu没有数据的流IO路径,因此咱们在qemu中开发了一套驱动能够用来实现迁移专用的数据流IO路径,而且经过共享内存加进程间互斥的方式在qemu和SPDK之间建立了一块bitmap用来保存块设备的脏页数量。考虑到SPDK是独立的进程可能会出现意外crash的状况,所以咱们给使用的pthread mutex加上了PTHREAD_MUTEX_ROBUST特性来防止意外crash后死锁的状况发生,总体架构以下图所示:
五 SPDK IO uring体验
IO uring是内核中比较新的技术,在上游内核5.1以上才合入,该技术主要是经过用户态和内核态共享内存的方式来优化现有的aio系列系统调用,使得提交IO不须要每次都进行系统调用,这样减小了系统调用的开销,从而提供了更高的性能。
SPDK在最新发布的19.04版本已经包含了支持uring的bdev,但该功能只是添加了代码,并无开放出来,固然咱们能够经过修改SPDK代码来体验该功能。
首先新版本SPDK中只是包含了io uring的代码甚至默认都没有开放编译,咱们须要作些修改:
1.安装最新的liburing库,同时修改spdk的config文件打开io uring的编译;
2.参考其余bdev的实现,添加针对io uring设备的rpc调用,使得咱们能够像建立其余bdev设备那样建立出io uring的设备;
3.最新的liburing已经将io_uring_get_completion调用改为了io_uring_peek_cqe,并须要配合io_uring_cqe_seen使用,因此咱们也要调整下SPDK中io uring的代码实现,避免编译时出现找不到io_uring_get_completion函数的错误:
4.使用修改open调用,使用O_SYNC模式打开文件,确保咱们在数据写入返回时就落地了,而且比调用fdatasync效率更高,咱们对aio bdev也作了一样的修改,同时添加读写模式:
通过上述修改spdk io uring设备就能够成功建立出来了,咱们作下性能的对比:
使用aio bdev的时候:
使用io uring bdev的时候:
可见在最高性能和延时上 io uring都有不错的优点,IOPS提高了约20%,延迟下降约10%。这个结果其实受到了底层硬件设备最大性能的限制,还未达到io uring的上限。
六 总结
SPDK技术的应用使得虚拟化IO路径的性能提高再也不存在瓶颈,也促使UCloud高性能云盘产品能够更好的发挥出后端存储的性能。固然一项技术的应用并无那么顺利,咱们在使用SPDK的过程当中也遇到了许多问题,除了上述分享的还有一些bug修复等咱们也都已经提交给了SPDK社区,SPDK做为一个快速发展迭代的项目,每一个版本都会给咱们带来惊喜,里面也有不少有意思的功能等待咱们发掘并进一步运用到云盘及其它产品性能的提高上。