Linux 下访问 I/O 的各种方法,我们为 Scylla 选择了哪个?为什么?

当大多数服务器应用程序开发人员想到 I/O 时,首先他们会考虑网络 I/O,因为大多数资源可以通过网络进行访问:数据库,对象存储以及其他微服务。 然而,数据库的开发人员也必须考虑文件 I/O。 本文介绍了可用的选择及其权衡,以及为什么 Scylla 选择异步直接 I/O(AIO/DIO)作为其访问方法。

选择访问文件

一般来说,访问 Linux 服务器上的文件有四种选择:读/写,mmap,直接 I/O(DIO)读/写和异步直接 I/O (AIO/DIO)。

传统读/写

从一开始就使用的传统方法,就是使用 read(2)和 write(2)系统调用。而在最新的实现中,读系统调用(或其许多变体之一 -  pread,readv,preadv 等)要求内核读取文件的一部分并将数据复制到调用进程地址空间中。如果所有请求的数据都在页面缓存中,则内核会将其复制并立即返回;否则,它将安排磁盘将请求的数据读入页面缓存,阻止调用线程,并且当数据可用时,它将恢复线程并复制数据。另一方面,写入通常 (注解 1 )会将数据复制到页面缓存中;内核会将页面缓存写回磁盘一段时间。

一种替代和更现代的方法是使用 mmap(2) ,系统调用将文件记录到应用程序地址空间中。 这会导致一部分地址空间直接引用到包含文件数据的页面缓存页面。 在此准备步骤之后,应用程序可以使用处理器的存储器读和写指令访问文件数据。 如果请求的数据恰好在缓存中,则内核将被完全忽略,并且以内存速度执行读取(或写入)。 如果发生高速缓存未命中,则会发生页错误,并且内核将活动线程置于休眠状态,以便读取该页面的数据。 当数据最终可用时,存储器管理单元被编程,使得新读取的数据可被线程访问,然后被唤醒。

Direct I/O (DIO)

传统的读/写和 mmap 都涉及内核页缓存,并将内核的 I/O 延迟调度。 当应用程序希望自己调度 I/O(至于原因我们稍后将解释),它可以使用 Direct I/O。 这涉及使用 O_DIRECT 标志打开文件; 进一步的活动将使用系统调用的正常读写序列,但是它们的行为已经改变:不是去访问缓存,而是直接访问磁盘,这意味着调用线程将无条件地进入休眠状态。 此外,磁盘控制器将直接将数据复制到用户空间,绕过内核。

异步 direct I/O (AIO/DIO)

Direct I/O 的一种重构,异步 Direct I/O 的行为与之非常类似,但不会使调用线程阻塞。相反,应用程序线程可使用 io_submit(2) 系统调用来调度 Direct I/O 操作,并且该线程不会阻塞;I/O 操作与正常线程并行运行。单独的系统调用 io_getevents(2)  用于等待并收集已完成的 I/O 操作的结果。和 DIO 一样,内核中的页面缓存会被绕过,磁盘控制器负责将数据直接复制到用户空间。

了解权衡

不同的访问方法都有各自的特点,在其他方面有所不同。 表1总结了这些特点,详细阐述如下。

Characteristic R/W mmap DIO AIO/DIO
Cache control kernel kernel user user
Copying yes no no no
MMU activity low high none none
I/O scheduling kernel kernel mixed user
Thread scheduling kernel kernel kernel user
I/O alignment automatic automatic manual manual
Application complexity low low moderate high

缓存控制

读/写和 mmap 的缓存都是由内核负责。系统的大部分内存都被提供给页面缓存。内核决定哪些页面在内存不足时被删除,以及页面何时需要写回磁盘,并且控制预读。应用程序可以使用 madvise(2) 和  fadvise(2)  系统调用为内核提供一些指导。

让内核控制缓存的巨大优点是内核开发人员在数十年来投入巨大的精力来调整缓存使用的算法。这些算法由数千种不同的应用程序使用,通常是有效的。然而,缺点是这些算法是通用的,并没有调整到对应的应用程序级别。内核必须判断应用程序将如何运行,即使知道应用程序不同,但也无法帮助内核进行判断。这会导致页面被误删, I/O 排列出错,或预先读取的数据无法消耗。

复制和 MMU 活动

mmap 方法的优点之一是如果数据在缓存中,则内核将被完全绕过。内核不需要将数据从内核复制到用户空间并返回,因此在该活动上花费的处理器周期较少。这有利于主要存在于缓存中的负载(例如,如果存储大小与 RAM 大小的比率接近 1:1)。

然而,当数据不在缓存中时,mmap 的缺点就会显现。当存储大小与 RAM 大小的比例明显高于 1:1 时,通常会发生这种情况。引入缓存的每个页面都会导致另一个页面被逐出。这些页面必须先插入页面表,之后又从表格中删除;内核必须扫描页面表以隔离停止活动的页面,然后将其删除。另外,mmap 需要页面表的内存。在 x8 6处理器上,这需要映射文件大小的 0.2%。这似乎很低,但是如果应用程序的存储比率为 100:1,则 20% 的内存(0.2%* 100)会被用于页面表。

I/O 调度

让内核控制缓存(使用 mmap 和 读/写访问方法)的一个问题是应用程序丢失了 I/O 调度的控制。内核选择其认为合适的数据块,调度它进行写入或读取。这可能会导致以下问题:

  • 写入风暴:当内核调度大量写入时,磁盘将被占用很长时间,并影响读取延迟。

  • 内核无法区分“重要” 和 “不重要” 的 I/O。属于后台任务的 I/O 可以压倒前台任务,这将影响其延迟(注释 2

通过绕过内核页面缓存,应用程序承担自己调度 I/O 的负荷。这并不意味着问题得到解决,而是意味着如果给予充分的关注和努力,问题可以得到解决。

当使用 Direct I/O 时,每个线程控制发出 I/O 的时间。而内核控制线程的运行时间,所以发出 I/O 的责任在内核和应用程序之间共享。使用 AIO/DIO,应用程序可以完全控制何时发出 I/O。

线程调度

使用 mmap 或读/写的 I/O 密集型应用程序无法猜测其缓存命中率是多少。因此,它必须运行大量的线程(明显大于它运行的机器的核心数)。使用太少的线程,它们可能都在等待磁盘而使处理器未被充分利用。由于每个线程通常最多有一个磁盘 I/O,所以运行的线程数必须大约是存储子系统的并发数乘以某个小的因子,以保持磁盘完全占用。然而,如果缓存命中率足够高,那么这些大量的线程将互相争夺有限数量的核心。

当使用直接 I/O 时,这个问题所有缓解,因为应用程序知道线程在 I/O 上何时被阻塞,何时可以运行,因此应用程序可以根据运行时条件调整运行的线程数。

使用 AIO/DIO,则应用程序可以完全控制正在运行的线程和等待 I/O(两者完全分离),所以它可以轻易地调整到内存或磁盘绑定的条件或介于两者之间的任意条件。

我来评几句
登录后评论

已发表评论数()