了解 I/O 的工作原理以及理解算法和存储系统的用例和权衡可以让开发人员生活变得更好:他们将能够提前做出更好的选择(基于他们正在评估的数据库的底层内容),当数据库行为异常时排除性能问题(通过将他们的工作负载与他们选择的数据库的工作负载进行比较)并调整他们的堆栈(通过平衡负载、切换到不同的介质、文件系统、操作系统或选择不同的索引类型)。
该系列由 5 件组成:
- 详解磁盘 IO: 第 1 部分 IO 风格:页面缓存、标准 IO、O_DIRECT
- 详解磁盘 IO: 第 2 部分 mmap、fadvise、AIO
- 详解磁盘 IO: 第 3 部分 LSM 树
- 详解磁盘 IO: 第 4 部分 B 树和 RUM 猜想
- 详解磁盘 IO: 第 5 部分 LSM 树中的访问模式
根据 Wikipedia 介绍的 IO 类型
尽管人们经常讨论和谈论网络 IO,但文件系统 IO 却很少受到关注。部分原因是网络 IO 具有更多功能和实现细节,因操作系统而异,而文件系统 IO 的工具集则少得多。此外,在现代系统中,人们主要使用_数据库_作为存储手段,因此应用程序通过网络上的驱动程序与它们通信,而文件系统 IO 则留给数据库开发人员去理解和处理。我仍然认为,了解如何在磁盘上写入数据以及如何从磁盘读取数据非常重要。
IO 有几种“风格”(为简洁起见省略了一些功能):
- 系统调用:open,write,read,fsync,sync,close
- 标准 I/O:fopen,fwrite,fread,fflush,fclose
- 矢量 IO:writev、readv
- 内存映射 IO:open,mmap,msync,munmap
让我们首先讨论标准 IO 组合和一些“用户空间”优化,因为这是应用程序开发人员最终使用最多的。
缓冲 I/O
在谈论stdio.h函数时,关于“缓冲”的问题有些令人困惑。使用标准 IO 时,可以选择全缓冲和行缓冲,或者选择退出任何缓冲。此“用户空间”缓冲与内核缓冲(页面缓存)无关,我们将在本文后面讨论内核缓冲。您也可以将其视为“缓冲”和“缓存”之间的区别,这可能会清楚地区分这些概念。
扇区/块/页
块设备是一种特殊的文件类型,提供对 HDD 或 SSD 等硬件设备的缓冲访问。块设备基于扇区(相邻字节组)工作。大多数磁盘设备的扇区大小为 512 字节。扇区是块设备数据传输的最小单位,不可能传输少于一个扇区的数据。但是,通常可以一次获取多个相邻段。文件系统的最小可寻址单位是块。块是设备驱动程序请求的一组多个相邻扇区。典型的块大小为 512、1024、2048 和 4096 字节。通常 IO 通过虚拟内存完成,虚拟内存将请求的文件系统块缓存在内存中并作为中间操作的缓冲区。虚拟内存与页面一起工作,页面映射到文件系统块。典型的页面大小为 4096 字节。
总之,虚拟内存页面映射到文件系统块,映射到块设备。
标准 IO
标准 IO 使用read()和write()系统调用来执行 IO 操作。读取数据时,首先处理页面缓存。如果数据不存在,则触发_页面错误_并调入内容。这意味着对当前未映射区域执行的读取将花费更长时间,因为缓存层对用户是透明的。
在写入期间,缓冲区内容首先写入页面缓存。这意味着数据不会立即到达磁盘。实际的硬件写入是在内核决定执行脏页写回时完成的。
标准 IO 占用用户空间缓冲区,然后将其内容复制到页面缓存。当使用 O_DIRECT 标志时,缓冲区将直接写入块设备。
页面缓存
页面缓存 存储最近访问的文件片段,这些片段很可能在最近被访问。处理磁盘文件时, read() 和write() 调用不会直接发起磁盘访问,而是通过页面缓存。
缓冲 IO 的工作原理:应用程序通过内核页面缓存执行读写操作,这允许共享页面进程、从缓存提供读取服务并限制写入以减少 IO。
执行读取操作时,首先会查询页面缓存。如果数据已加载到页面缓存中,则只需将其复制出来供用户使用:不执行磁盘访问,读取完全从内存中进行。否则,文件内容将加载到页面缓存中,然后返回给用户。如果页面缓存已满,则最近最少使用的页面将刷新到磁盘上并从缓存中移出,以释放空间来存放新页面。
write() 调用只是将用户空间缓冲区复制到内核页面缓存,并将写入的页面标记为dirty 。稍后,内核在称为**flush或writeback 的过程中将修改写入磁盘。实际 IO 通常不会立即发生。同时,read() 将从页面缓存提供数据,而不是读取(现已过时的)磁盘内容。如您所见,页面缓存在读取和写入时都会加载。
标记为脏的页面将被刷新到磁盘,因为它们的缓存表示现在与磁盘上的表示不同。此过程称为写回。写回可能有潜在的缺点,例如排队 IO 请求,因此有必要了解使用写回时的阈值和比率,并检查队列深度以确保您可以避免节流和高延迟。您可以在Linux 内核文档中找到有关调整虚拟内存的更多信息。
页面缓存背后的逻辑可以通过时间局部性
原理来解释,该原理指出最近访问的页面将在不久的将来的某个时间点再次被访问。
另一个原则是空间局部性
,它意味着物理位置相近的元素很有可能彼此靠近。此原则用于称为“预取”的过程,该过程会提前加载文件内容,预测它们的访问并分摊部分 IO 成本。
页面缓存还通过延迟写入和合并相邻的读取来提高 IO 性能。
消歧义:缓冲区缓存和页面缓存:以前是完全独立的概念,在 2.4 Linux 内核中统一了。现在它主要被称为页面缓存,但有些人仍然使用缓冲区缓存这个术语,它已经成为同义词。
根据访问模式,页面缓存会保存最近访问过或可能很快访问的文件块(预取或用fadvise标记)。由于所有 IO 操作都通过页面缓存进行,因此诸如读-写-读
之类的操作序列可以从内存中提供,而无需后续的磁盘访问。
延迟错误
当执行由内核和/或库缓冲区支持的写入时,确保数据确实到达磁盘非常重要,因为它可能被缓冲或缓存在某处。当数据刷新到磁盘时会出现错误,这可能是在fsync
或关闭文件
时。如果您想了解更多信息,请查看 LWN 文章确保数据到达磁盘。
直接输入输出
在某些情况下,使用内核页面缓存执行 IO 是不可取的。在这种情况下,可以在打开文件时使用O_DIRECT标志。它指示操作系统绕过页面缓存
,避免存储额外的数据副本并直接对块设备执行 IO 操作。这意味着缓冲区直接刷新到磁盘上,而无需先将其内容复制到相应的缓存页面并等待内核触发写回
。
对于“传统”应用程序,使用直接 IO 很可能会导致性能下降而不是加速,但在正确使用的情况下,它可以帮助获得对 IO 操作的细粒度控制并提高性能。通常,使用此类 IO 的应用程序会实现自己的特定于应用程序的缓存层
。
直接 IO 的工作原理:应用程序绕过页面缓存,因此写入会立即发送到硬件存储
。这可能会导致性能下降,因为内核会缓冲和缓存写入,并在应用程序之间共享缓存内容。如果使用得当,可以大大提高性能并改善内存使用率。
内核开发人员通常不赞成使用 Direct IO。Linux 手册页甚至引用了 Linus Torwalds 的话:“O_DIRECT 一直让我感到困扰的是,整个接口都很蠢”。
但是, PostgreSQL和MySQL等数据库使用 Direct IO 是有原因的。开发人员可以确保对数据访问进行细粒度的控制,可能使用自定义 IO 调度程序和特定于应用程序的缓冲区缓存
。例如,PostgreSQL 使用 Direct IO 进行WAL(预写日志),因为他们必须尽可能快地执行写入,同时确保其持久性,并且可以使用这种优化,因为他们确信数据不会被立即重用,因此绕过页面缓存写入不会导致性能下降。
不鼓励同时使用直接 IO 和页面缓存打开同一个文件,因为即使数据在页面缓存中,也会对磁盘设备执行直接操作,这可能会导致不良结果。
块对齐
由于直接 IO 涉及直接访问备用存储,绕过页面缓存中的中间缓冲区,因此要求所有操作都与扇区边界对齐。
未对齐写入的示例(突出显示)。从左到右:写入既不在块边界上开始,也不在块边界上结束;写入从块边界开始,但写入大小不是块大小的倍数;写入不是从块边界开始。
换句话说,每个操作的起始偏移量必须是 512 的倍数,缓冲区大小也必须是 512 的倍数。使用页面缓存时,由于写入首先进入内存,因此对齐并不重要:在执行实际的块设备写入时,内核将确保将页面拆分为正确大小的部分,并向硬件执行对齐写入。
对齐写入的示例(突出显示)。从左到右:写入在块边界上开始和结束,大小恰好是块的大小;写入在块边界上开始和结束,大小是块大小的倍数。
例如,RocksDB通过预先检查来确保操作是块对齐的(旧版本通过在后台对齐来允许未对齐的访问)。
无论是否使用 O_DIRECT 标志,确保您的读取和写入是块对齐的始终是一个好主意
。跨越段边界将导致从磁盘加载(或写回到磁盘)多个扇区,如上图所示。使用块大小或适合块内的值可保证块对齐的 I/O 请求,并防止内核内部的无关工作。
非阻塞文件系统 IO
我在这里添加这部分内容,因为我经常在文件系统 IO 的上下文中听到“非阻塞”一词。这很正常,因为网络和文件系统 IO 的大多数编程接口都是相同的。但值得一提的是,没有真正的“非阻塞”文件系统 IO,可以以相同的意义来理解。
对于常规文件,O_NONBLOCK 通常会被忽略,因为块设备操作被视为非阻塞操作(例如,与套接字操作不同)。系统不考虑文件系统 IO 延迟。做出这一决定可能是因为操作完成或多或少存在硬性时间限制。
出于同样的原因,您通常在网络环境中使用的东西,比如select和epoll,不允许监视和/或检查常规文件的状态。
结束语
今天我们讨论了页面缓存、标准 IO 和 O_DIRECT 标志的使用,数据库开发人员经常使用这种优化来控制标准 IO 委托给内核的缓冲区缓存,并讨论了它的使用地点、工作原理、用途以及缺点。
下一篇文章将介绍内存映射、矢量 IO 和页面缓存优化。