从一篇来自国人的优秀论文说起
USENIX ATC(USENIX Annual Technical Conference)是USENIX组织的技术年会,偏重系统设计与实现,是计算机系统领域具有最高学术地位的国际性会议之一, USENIX ATC每年录用70篇左右的论文,录用率在20%左右。在今年的USENIX ATC'20会议中,有一篇来自阿里云团队和上海交通大学新兴并行计算研究中心合作提交的论文 “Spool: Reliable Virtualized NVMe Storage Pool in Public Cloud Infrastructure”引起了笔者的兴趣。关于这篇论文,英文版地址在附录1,中文版地址在附录2, 读者可先行查阅。
仔细研读此文,笔者感同身受,对阿里云团队为解决这些复杂场景的问题而采取的种种努力也表示敬意。无独有偶,几年前,笔者所在团队也遇到过类似的问题,不同的是阿里云的应用场景是在公有云环境上,笔者遇到的是私有云环境上的问题,虽然大的场景有差距,解决方案也有区别,但总体解决思路还是比较类似的,并且都付出了艰苦的努力,才逐渐换来了现在产品的高性能,高可靠性和高可用性。
对于企业级生产系统,24(小时)×365(天)不间断运行是一个基本要求。而对于阿里公有云来说,尤其如此,现在阿里云上面已经运行了无数的关键企业应用,如文中所说 “我们目前在210个集群中部署Spool,大约20000台物理机,配备超过200000个NVMe SSD,提供低延迟、高IOPS和高吞吐量I/O支持的平台即服务云(IaaS)。生产中的云托管包括Cassandra、MongoDB、Cloudera和Redis等应用,是大数据、SQL、NoSQL数据库、数据仓库和大型事务数据库的理想选择”,对于这么多的应用系统,如果说升级软件,要不断中断客户业务的话,简直是不可想象的。
开源组件的局限性
我们先从技术的视角来探讨下论文中阿里云的Spool的架构,如下图所示
这是一个典型的linux下QEMU虚拟化框架。对于物理机上的若干NVMe盘,经过虚拟化后,再提供给QEMU下的Guest OS。在这个架构中,会用到比较多的开源组件,如SPDK用来管理NVMe盘,虚拟机和宿主机通过virtio和host来进行数据通信。如论文中所述,阿里云是4年前开始落地这个功能的,相信那时SPDK还并不是很稳定,那么经常性的更新,修正SPDK的bug就是一个常规需求了。
对于使用virtio框架的程序,必然要遵守virtio的一些规范,如该框架规定了前后端,virtqueue, 通信机制等。详细规范请参考附录3 。如下图,是guest和host数据交互的一般流程图。
GuestOS内的driver通过这个virtqueue与外边的Spool进行交互。这是一个很好的框架,但麻烦的是很多组件都用到它,不好在上面做定制化,比如更改它的一些核心数据结构。
正如论文中所述,guest和host之间是生产者-消费者模型,guest在有数据发送给host时,通过将描述符写入到descriptor table和available ring中,然后通知到host,host侧再从这个共享内存中取出信息,完成数据读写。
在没有论文中所述的Spool journal时,从guest发往host的IO请求在程序异常时,比如host侧的程序崩溃,或者SPDK,NVMe盘出现故障时,都有可能丢失那些“处理中的(Inflight)IO”。
比如上图中2处,IO1,IO2都被发送到NVMe设备,但NVMe设备尚未返回,这时SPDK软件出现故障了,则IO1,IO2的信息将可能完全丢失,guest可能不知道这些IO是否成功处理而造成潜在的数据不一致。
阿里云的解决方案
Spool journal的目的就在于能够保存这些Inflight IO的一些必要信息,以便于在后端故障恢复后,能够重新接着处理这些Inflight IO,保证IO运行的连续性,使Guest无感知。
原理上看很简单,但在实现的时候,却是布满陷阱。首先,程序运行时可能在代码的任意一行崩溃掉。因此,在设计上,这个journal应该是能够抗进程崩溃的,即虽然进程因为异常崩溃掉,但是journal不能丢失。其次,在新的进程启动后,要能认识这个journal,能恢复到崩溃前的一个一致性状态。最后,还要重试那些未完成的IO。
为了保存对应的journal信息,还会涉及到原子性保存,即记录journal时,不能在记录到一半的时候进程崩溃掉,而麻烦的地方在于journal信息可能会比较多,无法在一个指令中完成,所以论文中采用了一种特殊的方法,类似于RCU(Read,Copy and Update参见附录4)实现中的,先对一个副本变量进行修改,等到最后一步,才使用一个64位对齐的一次性原子操作,将journal信息记录下来。
可以看出,Spool journal是在线升级能够成功的关键因素,有了在线升级,则对于软件故障,基本上对于前端Guest业务是无感知的,软件会被快速重新拉起,继续处理IO。另外,文中介绍的Spool另外一个重要的作用是用于亚健康检测,比如检测到“坏盘”,则一般可以通过重新复位IO控制器解决,因为据统计,大部分IO读写错误其实不是真的磁盘损坏,而是IO通路上比如硬盘driver,IO控制器这样的一些中间组件出现故障,这时快速复位IO控制器会比“踢盘”来的更为聪明一些。
XSKY SDS遇到的问题
XSKY SDS是XSKY公司提供的私有云存储解决方案,在最初开发块存储的时候,也面临着同样的开源软件局限性问题,比如openstack在对接社区ceph版本时,至少在以下几方面存在着严重的问题:
虚拟机在打开ceph的卷时,每打开1个卷就会产生20多个线程,如果每个虚拟机打开10个卷,系统上有50个虚拟机,那么整个机器上就会有10000个线程,这对系统资源是一个极大的挑战。
虚拟机在打开ceph的卷时,实质上是要打开一个到ceph集群的网络通道,如果只打开1个网络通道,在某些情况下,如该卷是一个高性能卷时,单个网络通道又显得不够用,会成为瓶颈,需要打开多个网络通道才能完全把这个高性能卷驱动起来。
虚拟机在打开ceph的卷时,建立的和ceph集群的网络通道,并不具备应用层面的容错处理,即如果ceph集群进行升级,则那些Inflight IO可能会失去响应或者丢失,这样业务层就会出错,如果虚拟机的系统卷使用的也是ceph的卷时,则可能会引起虚拟机的挂死,无法恢复。即,开源ceph并不能很好的支持ceph软件的在线升级。
关于更多的细节内容,请参考附录5由XSKY在2017年发表的这篇文章的相关描述。
XSKY SDS的解决之道
笔者团队在2016年即开始解决上述问题,为了规约资源和在单卷下打开更多的网络通道,引入了XDC(XSKY DATA CLIENT)模块,其接管从开源librbd模块过来的所有命令,在XDC经过逻辑处理后,再发往XSKY SDS集群。并且在单卷的情况下,XDC还会按需打开多个通道,以提高单卷的读写性能。
而对于在线升级功能,巧合的是,和前面阿里云论文中所述的Spool Journal功能在原理上竟非常相似,都是采用进程间共享内存来管理,包括Inflight IO的状态保存,后端软件升级重启后的重发机制等。只不过实现机制不同,后文在描述实现原理时再详述。
如上图所示,对比开源Ceph实现,我们发现在性能上,XSKY SDS实现了:
单客户端4K随机写IOPS提升20%;
客户端IO延迟降低15%;
每节点CPU利用率降低40%;
每节点存储部分内存使用减少2/3;
完全满足最初的开发需求。
XSKY SDS 代理转发实现原理
与阿里云环境不同的是,我们的场景是使用QEMU下的RBD协议,将XDC提供的卷映射给Guest OS,这个卷实质上是一个逻辑上的网络卷,由XSKY SDS集群通过网络提供。
rbd driver通过librbd.so访问XSKY SDS提供的rbd设备,在开源社区版本中,librbd.so是直接访问ceph集群的,而我们则在librbd里面提供了代理转发逻辑,将多个卷打开通道的命令全部转发到XDC模块,由XDC按需打开一定的通道,从而减少CPU和内存资源,提高性能。
由于我们提供的是增强版的librbd.so,而这个库是链接到qemu进程的,我们并不需要更改qemu的核心代码,所有的逻辑都可以在librbd.so内部实现。为此,我们实现了qemu进程和XDC进程间的共享内存,在qemu进程启动的时候,我们会在librbd.so内部申请一定的内存,比如申请32M的空间,然后将这32M内存分配成mailbox头部,命令区,数据区。命令区按照规格预先分配好,比如命令队列是1024个,那么就最多能同时处理1024个命令,数据区则设计为可以动态增长。这段共享内存由librbd.so侧负责申请,然后映射到XDC侧。
使用这样的方法,所有的Inflight IO都是天然存在于共享内存中的,只要Guest不死机,所有的命令都存在,而如果Guest死机了,那命令自然也就没有存在的必要了。所以我们这个共享环已经兼具了前面Spool journal的属性了。
Guest在需要写入数据时,会首先使用librbd.so 将待写入的命令描述符填入CMD entry区,数据部分会写入到DATA区,最后再更新mailbox的 head和tail索引,再通过事件通知机制激活XDC侧的处理。
XDC侧则会根据mailbox的信息,获取对应的CMD entry,然后进行处理,处理完后再更新CMD entry的状态信息,再通知到librbd侧,librbd再进行后续IO完成处理流程。
下面给出一个简单的交互流程图,当然具体的产品化代码比这个要复杂的多。
对于最重要的在线升级功能,比如XDC升级,后端XSKY SDS升级等,我们是让XDC重启后,能够认识到之前的共享内存的,这里的技巧在于我们使用了PIPE FD,而这些FD是存在于虚拟内存文件系统之上的,当XDC重启后,通过这些FD来重建事件通知通道,进而在后续的事件通知中再交换之前的如共享内存地址等相关信息。等所有通道,共享内存都准备就绪后,librbd侧会重试之前未完成的IO。
有了这样的分离设计后,我们把librbd.so 称作head部分,而把XDC及其后边的整个XSKY SDS称作body部分,除了head不能在线升级外,整个body部分都是可以在线升级的。当然在线升级作为一个系统化工程,head除了设计稳定的接口不需要经常性的升级外,还是可以在需要的时候通过虚拟机迁移完成不中断业务的在线升级的。目前使用librbd proxy功能的XSKY SDS软件在生产系统上已经部署上万节点,运行4年多来,经历过多次如安全补丁修复,高级功能增强等,都是不需要停止业务系统直接动态升级,用户无感知的。
结语
在现代企业级存储系统中,关键业务系统对于在线升级要求是极高的,必须提供24*365的服务时间。软件系统在设计开发中,必须优先考虑能够无中断的提供高可靠,高可用性服务。同为企业级软件,XSKY SDS和阿里云系统都能提供这样的特性,其主要异同点在于:
阿里云将本地的NVMe设备通过Spool虚拟化后提供给虚拟机,而XSKY SDS则是通过RBD协议将集群内的逻辑虚拟卷使用XDC/librbd proxy技术提供给虚拟机。
Spool的开发在QEMU的virtio/virtio-blk/virtqueue框架下进行,受制于框架的约束,不得不在Spool侧实现较为复杂的journal,并且需要实现原子事务更新等机制。而librbd proxy则是基于librbd库进行,其共享内存机制可以完全独立实现,包括共享内存的分配,格式组织,通信协议等,可以实现的更为轻量级和高效。相较而言,Spool更为通用一些,而librbd proxy则更为轻量一些,两者殊途同归,有异曲同工之妙,却并无优劣之分。
两者都能提供毫秒级别的在线升级功能,Spool需要处理的是SPDK的耗时升级逻辑,而librbd proxy更多的是考虑网络层面的升级逻辑,如后端XSKY SDS整体软件的升级。两者都能处理如后端软件崩溃即时拉起功能,并能自动恢复,重发IO请求,做到业务无感知.
两者都经过长时间大规模的核心业务生产考验,阿里云在公有云系统上部署超过200多套集群20000多台的物理节点,而XSKY SDS也在私有云存储上经过长达4年部署上万物理节点的考验,证明这种方法确实是稳定可靠的。当然笔者也希望看到更多的优秀解决方案产生,如果读者有类似的应用场景和更好的解决方案,也欢迎留言讨论,以期望提高行业整体技术水平,更好的为客户服务。
附录
附录1:https://www.usenix.org/conference/atc20/presentation/xue
附录2:https://kernel.taobao.org/2020/07/solves-large-scale-high-performance-storage-reliability-problems
附录3:https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html
附录4:https://www.kernel.org/doc/html/latest/RCU/whatisRCU.html
附录5:https://www.xsky.com/news/5112/
作者:XSKY融合存储
原文链接:https://mp.weixin.qq.com/s/lCj0zgi6RILu_wufz68Oeg