写点什么

如何提升存储性能之 IO 模型和 AIO 大揭秘

用户头像
焱融科技
关注
发布于: 2020 年 11 月 13 日
如何提升存储性能之IO模型和AIO大揭秘

如何提升存储系统的性能是一个对存储工程师们来说是永恒的大命题,解决这个问题并没有一击即中的银弹,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 的提交,因此系统开销也很小。



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 支持后,进一步弥补了这一短板,将能够更好地适配这些应用场景。


发布于: 2020 年 11 月 13 日阅读数: 55
用户头像

焱融科技

关注

Drive Future Storage 2020.05.29 加入

面向未来的下一代云存储

评论

发布
暂无评论
如何提升存储性能之IO模型和AIO大揭秘