磁盘IO性能优化

从read / write 到硬盘

当我们调用read/write时,在linux系统中这个过程是下图这样子的:
文件系统

  • VFS:虚拟文件系统,可以大致理解为 read / write / ioctl 之类的系统调用就在这一层。当调用 open 之后,内核会为每一个 file descriptor 创建一个 file_operations 结构体实例。这个结构体里包含了 open、write、seek 等的实例(回调函数)。这一层其实是 Linux 文件和设备体系的精华之一,很多东西都隐藏或暴露在这一层。不过本文不研究这一块
  • 文件系统: 这一层是实际的文件系统实现层,向上隐藏了实现细节。当然,实际上除了文件系统之外,还包含其他的虚拟文件,包括设备节点、/proc 文件等等
  • buffer cache:这就是本文所说的 “缓存”。后文再讲。
  • 设备驱动:这是具体硬件设备的设备驱动了,比如 SSD 的读写驱动、磁盘的读写驱动、字符设备的读写驱动等等。
  • 硬件设备:这没什么好讲的了,就是实际的硬件设备接口。

工作机制

当应用程序需要读取文件中的数据时,操作系统先分配一些内存,将数据从存储设备读入到这些内存中,然后再将数据分发给应用程序;当需要往文件中写数据时,操作系统先分配内存接收用户数据,然后再将数据从内存写到磁盘上。
对于每个文件的第一个读请求,系统读入所请求的页面并读入紧随其后的少数几个页面(不少于一个页面,通常是三个页面),这时的预读称为同步预读。
如果应用程序接下来是顺序读取的话,那么文件 cache 命中,OS 会加大同步预读的范围,增强缓存效率,此时的预读被称为异步预读
如果接下来 cache 没命中,那么 OS 会继续使用同步预读。
那么如何优化磁盘IO的性能呢?
高性能硬盘I/O优化方案

基本思路

从缓存的工作机制来看,很简单,如果要充分利用 Linux 的文件缓存机制,那么最好的方法就是:每一个文件都尽可能地采用顺序读写,避免大量的 seek 调用。那么设计方法主要有:

尽可能地顺序读写一个文件:

从文件缓存角度,如果频繁地随机读取一个文件不同的位置,很可能导致缓存命中率下降。那么 OS 就不得不频繁地往硬盘上预读,进一步导致硬盘利用率低下。所以在读写文件的时候,尽可能的只是简单写入或者简单读取文件,而不要使用 seek。
这条原则非常适用于 log 文件的写入:当写入 log 的时候,写就好了,不要经常翻回去查看以前的内容。例如kafka、rocketMQ都是这样的设计。

单进程读写硬盘:

整个系统,最好只有一个进程进行磁盘的读写。而不是多个进程进行文件存取。这个思路,一方面和上一条 “顺序写” 原则的理由其实是一致的。当多个进程进行磁盘读写的时候,随机度瞬间飙升。特别是多个进程操作多个文件的时候,磁盘的磁头很可能需要频繁大范围地移动。
如果确实有必要多个进程分别读取多个不同文件的话,可以考虑下面的替代方案:

  • 这多个进程是否功能上是独立的?能不能分开放在几个不同的服务器之中?
  • 如果这几个进程确实需要放在同一台服务器上,那么能不能考虑为每个频繁读写的文件,单独分配一个磁盘?
  • 如果成本允许,并且文件大小不大的话,能否将磁盘更换为 SSD ?因为 SSD 没有磁头和磁盘的物理寻址动作,响应会快很多。

如果是多个进程同时写入一个文件(比如 log),那就更好办了。这种情况下,可以在这几个进程和文件中间加入一个内部文件服务器,将所有进程的存取文件需求汇总到该文件服务器中进行统一处理。

1
2
3
4
ProcessA   ProcessB   ProcessC
| | |
| V |
*----> The File <---*

改为

1
2
3
4
5
6
7
ProcessA   ProcessB   ProcessC
| | |
| V |
*----> ProcessD <---*
|
V
The File

顺便还可以在这个服务进程中实现一些自己的缓存机制,配合 Linux 自身的文件缓存进一步优化磁盘 I/O 效率。
尽量以4KB或者整数倍4KB读写文件
这是因为文件系统一般以4KB为一个页面

避免对大目录操作

目录在文件系统中,是以一个 inode 的方式存在的,那么载入目录,实际上就是载入这个 inode。从存储的角度,inode 也只是一个普通的文件,那么载入 inode 的动作和载入其他文件一样,也会经过文件缓存策略。载入了一次之后,只要你持续地访问它,那么操作系统就会将这个 inode 保持在缓存中。因此后续的操作,就是直接读写 RAM 了,并不会受到硬盘 I/O 瓶颈的影响。

把小文件的读写转换为大文件的写

随机磁盘IO很慢,如果是大量写入小文件,则读写性能都会较差;另外大量的inode也会占用大量的磁盘IO,寻址较慢,每次读写文件都需要两次转动磁头。那优化的思路就是把小文件转换成大文件。比如TFS。