本文为[1]的翻译;
当大多数的后台开发者考虑IO的时候,他们考虑的是网络IO,因为现在大多数的资源都是建立在网络之上的:数据库,对象存储,以及其他微服务。然而,数据库的开发者也必须考虑文件IO。这篇文章描述了可用的选择及其利弊,以及为什么Scylla选择异步IO(AIO/DIO)作为它的IO调用方法。
操作文件的方法
通常来说,Linux 服务器重有四种方法来操作文件,read/write, mmap, Direct I/O (DIO) read/write, and asynchronous direct I/O (AIO/DIO).
read/write
传统的可用方法就是去使用系统调用函数read 和 write。在现代的实现方式,系统调用函数read(or pread,readv,preadv,etc) 请求内核读取文件中的一段数据,然后把这段数据复制到调用read 函数的进程的地址空间里取。如果请求的这段数据实在页缓存中(page cache)中,那么内核直接赋值,立刻返回。否则的话,内核将会调度磁盘读取这段数据并存入页缓存中(page cache),同时阻塞线程,当这个数据可以利用的时候,将会唤醒线程,拷贝数据。写函数(write) 将把数据拷贝到页缓存中(page cache),内核随后会把页缓存的数据写到磁盘中。
read/write
Mmap
另外一个可以选择的,更高级一点的方法是系统调用函数mmap,它把文件直接映射到应用的地址空间。这将导致应用的地址空间的一部分是直接引用包含文件数据的页缓存地址(page cache),在调用mmap之后,这个应用可以直接通过处理器的内存读写指令来读写文件中的数据。如果请求的数据在缓存中(hit),那么可以完全绕开内核,读写速度都是内存级别的读写。如果请求的数据不在缓存中(miss),那么将会产生页错误(page error)。内核将会让这个线程休眠(sleep)。然后去读取缺失的页的数据。当数据可用,线程wake,内存管理单元重新编程,让新读取的数据对线程可用。
mmap
Direct I/O (DIO)
传统的read/write 和 mmap 都涉及到内核的页缓存(page cache),同时都把IO的调度扔给内核来处理。如果应用想要自己调度IO(原因随后解释),它可以使用 direct IO。这涉及到打开文件的时候用到的flag--O_DIRECT; 接下来将会使用正常的read 和 write 等一系列的系统调用。但是他们的行为改变了: 并不读取缓存,直接访问磁盘,这意味着调用线程将无条件进入休眠状态。而且,磁盘控制器(disk controller)将直接将数据复制到用户空间,绕过内核。
Asynchronous direct I/O (AIO/DIO)
Direct IO 的改进,异步IO、AIO和DIO 类似,但是线程不阻塞。相反,应用线程通过io_submit 系统调用函数来调度IO操作,但是线程不阻塞。I/O操作与正常线程执行并行运行。一个独立的系统调用函数io_getevents ,用来等待和搜集完整的IO操作的结果。和DIO一样,内核的页缓存(page cache)被绕过了,磁盘控制器(disk controller)负责将直接将数据复制到用户空间,
Understanding the tradeoffs
不同的方法又不同的特点,Table 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 |
Cache control
对于 read/write 和mmap 函数来说,内存由内核负责,系统的大部分内存都用作页缓存。当内存跑的慢的时候,当内存页需要写回磁盘的时候,当需要预读的时候,内核来决定替换哪些页面。应用可以通过系统调用函数madvise 和 fadvise 来给内核一些指导。
让内核来控制缓存的一个比较大的优点是,十几年来,内核开发者已经付出了很多努力来对缓存算法进行调优。这些算法被用在成千上万的应用中,并且十分有效。缺点就是,这些算法是通用的,并不针对某一应用进行优化。内核需要猜测应用的行为。即使应用的行为是不同的,它也没有办法来帮助内核猜测正确。导致错误的页被替换,IO调度错误,预读的数据后续不会被使用到。
Copying and MMU activity
mmap的一个优点就是如果数据在缓存中,那么内核就会被完全绕过。内核不需要把数据从内核空间拷贝到用户空间,然后再拷贝回来,因此,在该操作上花费的处理器周期更少,有利于加载大部分缓存中的内容(例如 the ratio of storage size to RAM size is close to 1:1)
mmap的缺点就是当数据不在缓存中。缓存不命中通常是由于the ratio of storage size to RAM size is significantly higher than 1:1。新页进入缓存,另外的页就会被替换。这些页不得不从页表中删除和插入。内核必须扫描页表以隔离不活动的页,使它们成为替换页的候选者。而且mmap需要为页表申请内存。X86的处理器上,页表占映射文件大小的0.2%。这通常很低,但是如果这个应用的 ratio of storage to memory为100:1,那么页表的空间就占整个内存的20%(0.2% * 100)。
I/O scheduling
内核控制缓存(read/write 和 mmap)的一个问题是应用无法控制IO的调度。内核调选它认为合适的数据块,调度它进行读写。这可能导致以下问题:
- 写风暴: 当内核调度大量的写,磁盘一段时间将会很忙,这将会导致读延迟
- 内核无法区分"重要的"和"不重要的"IO。属于后台任务的I/O会压倒前台任务,这将影响它们的延迟。
通过绕过内核页缓存,应用将自己调度IO,这并不意味着问题已经解决了,但是这意味着,通过开发人员的思考和努力,问题可以被解决。
当使用DIO,每一个线程控制发布IO的时间。但是,内核控制什么时候线程运行。因此,在内核和应用程序之间共享发布I/O的权限。通过AIO/DIO。应用可以完全控制IO的时间。
Thread scheduling
一个IO密集型的应用使用mmap 和 read/write 的时候,并不会知道缓存命令率的大小,因此必须使用大量的线程(通常比机器的核心数多)。如果使用的线程过少,它们可能都在等待磁盘,使处理器无法充分利用。由于每个线程通常最多只有一个磁盘I / O未完成,因此运行线程的数量必须围绕存储子系统的并发性乘以一些小因子才能保持磁盘完全占用,但是,如果高速缓存命中率足够高,则这些大量线程将针对有限数量的核相互竞争。
当使用DIO的时候,这个问题有所减轻,因为应用程序确切地知道线程何时在I / O上被阻塞以及何时可以运行,因此应用程序可以根据运行时条件来调整运行线程的数量
通过DIO/AIO,应用程序可以完全控制正在运行的线程和等待IO(两者完全分离);因此可以轻松调整内存或磁盘限制条件或他们两者之间的任何内容。
I/O alignment
存储设备有一个块大小;所有的IO必须以此块大小的倍数执行,这个块大小通常是512 或者 4196 bytes,如果使用 read/write 或者 mmap,这个内核自动进行块的对齐;一小块读或者写将会自动被扩展正确的块边界,否则将会出问题。
如果使用DIO,通常由应用来进行块对齐。这增加了复杂性,同时也提供了一个优势: 即使512字节边界足够,内核也会过度对齐到4096字节边界,但使用DIO的应用程序可以进行512字节对齐读取,这样可以节省项目的带宽。
Application complexity
通过前面的讨论,对于IO密集型的应用,我们更偏爱使用AIO/DIO。但是这些方法会带来一个巨大的问题:复杂度。将缓存管理交由应用程序处理意味着它可以以更少的开销做出比内核更好的选择,但是,这些缓存算法需要被重写以及测试。如果使用AIO,通常要求应用来使用回调,协程,或者其他相似的技术,这通常会导致很多高可用库的不可用。
Scylla and AIO/DIO
对于Scylla来说,我们选择表现最好的AIO/DIO。为了降低以上讨论的复杂度的问题,我们写了一个框架: Seastar,一个高性能的针对IO密集型应用的框架。Seastar抽象执行AIO的细节,并为网络,磁盘和多核通信提供通用API。 它还提供适用于不同用例的状态管理的回调和协程样式。
Scylla的不同部分使用不同的IO:
- 压缩使用用户级别的预读和写回技术来确保高性能。但是绕过应用层的缓存是由于可以预测的低命中率。
- 查询(读取)使用用户层的预读和用户层的缓存。因为我们提前知道磁盘上数据的边界,用户层的预读可防止预读溢出,用户层缓存不仅可以缓存从磁盘读取的数据,还可以缓存来自多个文件的数据合并到单个缓存项中的内容。
- 小字节读取,通常与512字节边界对齐,以减少数据传输和延迟。
- 单独的I / O调度类确保commitlog写入获得所需的带宽,并且不受读取或dominate read的支配
- Seastar I / O调度程序允许我们动态控制压缩和查询的I / O速率,以满足用户服务水平协议(SLAs)
AIO / DIO是从应用程序直接驱动NVMe drivers以进一步绕过内核的良好开端。 这或许成为未来的Seastar功能。
[1] https://www.scylladb.com/2017/10/05/io-access-methods-scylla/