如何晋升存储系统的性能是一个对存储工程师们来说是永恒的大命题,解决这个问题并没有一击即中的银弹,IO性能的优化都在细节里。明天咱们来讲一讲性能和IO模型之间的关系。
咱们先从本地磁盘的IO模型说起。一方面,对本地磁盘来说,传统机械磁盘HDD介质的IO性能比CPU指令和应用程序差了好几个数量级;另一方面,新型的SATA SSD和NVMe SSD硬盘的性能大幅晋升,在硬盘外部,磁盘控制器芯片具备多个队列来解决并发的IO申请,磁盘自身具备了更高水平并发的能力。那么如何解决磁盘交互慢,以及利用新型磁盘外部个性,晋升数据拜访性能,升高零碎开销呢?由此零碎工程师们引入了多种IO模型来应答这些问题。
01 IO模型
简略来说,咱们能够在上面这张二维的表中,别离从同步和异步、阻塞和非阻塞两个维度,演绎一下当初Linux操作系统中不同的IO模型。
同步阻塞 IO
这是应用程序编写时最罕用的IO模型。在该模型中,应用程序执行零碎调用时,会导致应用程序阻塞。例如,利用收回一个读的零碎调用,程序后续的逻辑会被阻塞,直到零碎调用实现(数据传输实现或失败)为止。当然,这个应用程序的阻塞,并不代表其它的利用不能继续执行,在这个利用被阻塞期间,会让出CPU,CPU能够执行其它的应用程序,只是这个程序自身被拜访磁盘IO操作阻塞住了。从处理器角度来看,还是挺高效的,而且即便传统HDD响应较慢,这种读写模式所波及的用户态、内核态上下文切换也不多,能满足大部分利用的性能需求。
同步非阻塞 IO
同步非阻塞模型和第一中模型的最大区别,是应用程序以非阻塞形式发送IO零碎调用之后,零碎会间接返回一个返回码(EAGAIN或者EWOULDBLOCK),这个返回码是提醒应用程序期待或稍后再次被动询问IO是否实现。在IO实现后的那次零碎调用,零碎会返回数据,这意味着IO可能曾经实现了,但仍需利用再次被动申请,能力取得数据,所以会带了一些额定的延时,存储整体的延时性能差,且产生了屡次内核和用户态之间的上下文切换,对延时要求高的利用个别不会采纳该模型。
异步阻塞IO
第三个IO模型,也称之为零碎事件驱动模型或IO multiplexing,也是十分罕用的IO模型。其机制能够简略了解为应用程序在发送零碎调用时,利用操作系统的epoll机制,被动申明去监听某个IO描述符fd状态的变动(或事件的类型),epoll机制会保障这个fd在产生指定变动后告诉利用,数据曾经筹备好,再由应用程序发动IO操作。在理论从磁盘进行IO过程中,由epoll机制自身去监听事件,应用程序并不关注epoll外部的执行,应用程序能够执行其它操作。
异步非阻塞IO
话题终于来到明天的重点,异步非阻塞IO,也称为AIO。这种模型的特点,是应用程序收回IO申请之后,零碎会间接返回,告知这个申请曾经胜利发动并被零碎接管了。零碎后盾在执行具体IO操作过程中,应用程序能够执行其它业务逻辑。当IO的响应达到时,会产生一个信号或由零碎间接执行一个回调函数来实现这次的IO操作。通过形容和下图能够看到,这种模型带来几个益处,一是利用并不会被某次IO申请阻塞,后续应用逻辑能够持续进行,且不须要轮询或再次发动相干零碎调用;二是这种模式的上下文切换很少,它能够在一个上下文实现多个IO的提交,因而零碎开销也很小。
AIO是Linux2.6内核提出的一个规范个性,提出来的目标,就是反对异步非阻塞模型。目前,AIO有两种实现形式,别离是应用libaio和io_uring。2.6以上版本的内核曾经实现了内核级别的AIO反对,配合用户态libaio库,即可反对异步非阻塞模式拜访,到当初曾经非常成熟和稳固。在5.x内核中引入的io_uring,则将作为对立框架,用于反对磁盘和网络等数据拜访的异步非阻塞操作,尽管io_uring利用场景更广,但成熟稳定性还欠缺一些,目前还在一直迭代中。因而业界通常说AIO的时候,默认指的就是libaio这种实现。
libaio的呈现,的确对SSD等新型介质是一个很好的反对和解放。如果不借助libaio,要充分发挥硬件性能的话,须要在应用程序级别引入多线程或多机多任务。这种形式存在两个有余,一是多线程之间须要上下文切换,而且也不能为了并发而无限量地引入大量的线程,这样对系统和CPU开销都很大;二是有的应用程序自身并没有实现多线程,也没有做多机并发,因而也不可能通过多线程形式来晋升对底层的利用。而通过libaio,就能够在一个线程的状况下,充分利用SSD等新型硬件外部多队列来实现并发(即SSD的控制器保护了多个工作队列,应用程序通过应用libaio,就能够在单线程下,释怀地往硬件下发大量IO申请,由硬件自身来解决多并发的问题),从而晋升单线程应用程序的性能,也可能缩小零碎因为多线程切换带来的开销。AIO是以后高性能零碎(不论是存储或是其余零碎)晋升解决能力的一个重要形式。
02 AIO(libaio)的限度
文件在关上时有两种形式,dio和buffer io。dio不写pagecache,间接和盘交互,buffer io会有内存pagecache染指,某些场景下会对性能有晋升,但有些特定IO场景中性能反而可能会降落。例如程序大IO,性能可能反而不如dio,这是因为buffer io要先写内存,再刷盘,而HDD或其它磁盘间接进行程序IO性能可能更高;另外某些对数据可靠性要求比拟高的场景中,写pagecache可能会有数据失落的危险,例如MySQL等数据库,这些利用在写数据时通常都会应用dio,读的时候会引入应用程序本身的一些缓存机制来晋升性能。
之所以介绍了一下dio和buffer io的背景,是因为libaio的一个限度是只反对dio。这是因为buffer io会遇到bounce buffer调配阻塞的问题,此外,在遇到非对齐的IO时,还会触发写惩办,这些对效率影响都较大,与libaio心愿晋升性能南辕北辙了,因而libaio在实现的时候默认就是dio了。
而新的io_uring则反对buffer io(对于io_uring,咱们就在当前再介绍了)。
03 分布式文件系统对AIO的反对及意义
对网络存储或者内部存储来说,客户端次要性能就是IO转发,所以客户端不波及间接拜访磁盘(IO拜访模型,尤其是AIO的初衷,就是解决本地拜访的问题),所以通常来说(尤其是对网络文件系统),相似GlusterFS等开源的分布式文件存储个别不会反对AIO。然而,但对于一些利用,例如MySQL,它不晓得本身的数据起源是本地文件系统还是网络文件系统,所以应用程序默认应用的是libaio,如果客户端不反对AIO,只是进行AIO转发的话,性能就会受到制约。在这种场景下,客户端就要模仿后端AIO的实现,进而充分发挥客户端的性能了。
04 YRCloudFile客户端对AIO的反对
YRCloudFile新版本的客户端对AIO的读写模式进行了反对。对于YRCloudFile客户端AIO的实现方面,须要了解接口io_setup、io_cancel、 io_destroy、io_getevents、 io_submit,内核中对应的接口为aio_read/write和aio_complete。在客户端中,首先要断定该申请是否是AIO申请,而后在执行aio_read/write的时候,决定是否异步,aio_read/write是实现的重点。
对于AIO读而言:首先要查看data buff和offset是否对齐,对于非PAGE_SIZE对应的申请,须要计算出其对应的物理pages,而后顺次 pin user pages,提早被换出,再封装申请并异步下发。映射page到内核线性地址空间后,从存储后端读取到数据进行填充,数据填充完后,回调aio_complete,并开释pages的援用计数。
期间要思考pagecache的影响,须要将重叠区间的pagecache进行回刷和期待,能够参考filemap_write_and_wait_range的解决。
此外,还要思考如下三类对齐的场景:
场景1:date_len <= PAGE_SIZE,写入数据在同一个page的场景。
场景2:date_len <= PAGE_SIZE,数据逾越两个page的场景。
场景3:date_len > PAGE_SIZE,数据在首个page内有偏移。
对于写而言:能够参考读的逻辑,大体上也是封装申请异步下发。并发解决后,回调aio_complete,在这个过程中,同样须要思考pagecache的影响。
性能数据
在实现libaio的反对后,客户端在应用fio+libaio场景的测试中,性能随着iodepth根本出现线性增长状态,直到达到客户端的性能下限,单客户端性能如下:
05 总结
在分布式文件系统中,客户不仅关注整个集群的性能,同时也会关注单个客户端的性能以及单线程下利用拜访的性能。对于很多业务而言,并发度不高,单线程的提早间接影响了零碎的性能;而局部业务逻辑(如Nginx,MySQL,seastar)都应用到了AIO模型,如果客户端不反对AIO,那么后端数据拜访的性能将会受到制约。
YRCloudFile在新版本中实现客户端的AIO反对后,进一步补救了这一短板,将可能更好地适配这些利用场景。