高性能磁盘 I/O 开发学习笔记 -- 软件手段篇

上一篇文章咱们讲了一下硬盘(磁盘 & SSD)在硬件上的一些限制,总结了两个优化硬盘 I/O 的方向。本篇咱们就从 Linux 软件开发的角度,讲一下在软件设计中咱们应该如何提升硬盘 I/O。html

本文地址:http://www.javashuo.com/article/p-xyjmvbnr-hp.htmlnode

这里,咱们会涉及一个新的 “缓存” 概念。注意,这里的 “缓存” 和前文所说起的存储架构中的 “cache” 虽然中英文用词都同样,但二者是不一样的。
本文所说的缓存,指的是在 Linux 操做系统层面,在应用程序对硬盘进行读写(read / write 系统调用)时,对硬盘资源所作的一个预加载 / 延写入的机制。linux

Linux 文件缓存简介

从一个面试题提及

多年之前有一次面试,我被问了一个问题:
—— “你说一说,咱们调用write()以后,Linux 是怎么调用到底层的?”面试

我一脸懵逼,第一反应是这个问题太泛了,若是把我所知道的全部东西说出来的话,从顶层软件过程到底层硬件驱动编写,我能够讲一下午(参见个人工做经历)。编程

我只好再问了一句:“这个范畴有点大,请问您能不能具体地问一下呢?”
估计面试官也没想到我会反问,他只是重复了一下:“你就……把这个过程说一下吧。”
因而乎,我就从系统调用的实现原理讲起,然而很快就被面试官打断:“好吧能够了,你回去等消息吧。”segmentfault

消息确定是没等到,而我至今也没把握面试官但愿听到的答案是什么。此次说到硬盘 I/O 的时候我突然想到:或许面试官要的是这个吧? ————缓存

从 read / write 到硬盘

在现代操做系统中,一个 “真正的” 文件,当调用 read / write 的时候,数据固然不会简单地就直达硬盘。对于 Linux 而言,这个过程的一部分是这样的:服务器

http://blog.csdn.net/hguisu/article/details/6122513

在操做系统内核空间内,read / write 到硬件设备之间,按顺序有这么几层:架构

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

工做机制

这里我以为 IBM 的资料讲的特别清楚。下面是重点摘抄:异步

当应用程序须要读取文件中的数据时,操做系统先分配一些内存,将数据从存储设备读入到这些内存中,而后再将数据分发给应用程序;当须要往文件中写数据时,操做系统先分配内存接收用户数据,而后再将数据从内存写到磁盘上。

对于每一个文件的第一个读请求,系统读入所请求的页面并读入紧随其后的少数几个页面(很多于一个页面,一般是三个页面),这时的预读称为同步预读

若是应用程序接下来是顺序读取的话,那么文件 cache 命中,OS 会加大同步预读的范围,加强缓存效率,此时的预读被称为异步预读

若是接下来 cache 没命中,那么 OS 会继续使用同步预读。

知道了原理以后,接下来就是怎么作的问题了——

高性能硬盘 I/O 优化方案

基本思路

从缓存的工做机制来看,很简单,若是要充分利用 Linux 的文件缓存机制,那么最好的方法就是:每个文件都尽量地采用顺序读写,避免大量的 seek 调用。

其实这一条和我前一篇文章中从硬盘工做原理的角度出发提出的两个思路是一致的。好了,如今咱们能够搬出具体的 coding 思路了。这总结起来比讲原理短多了。如下就把几个设计思路讲一下吧:

尽量顺序地读写一个文件

从文件缓存角度,若是频繁地随机读取一个文件不一样的位置,极可能致使缓存命中率降低。那么 OS 就不得不频繁地往硬盘上预读,进一步致使硬盘利用率低下。因此在读写文件的时候,尽量的只是简单写入或者简单读取文件,而不要使用 seek

这条原则很是适用于 log 文件的写入:当写入 log 的时候,写就行了,不要常常翻回去查看之前的内容。

单进程读写硬盘

整个系统,最好只有一个进程进行磁盘的读写。而不是多个进程进行文件存取。这个思路,一方面和上一条 “顺序写” 原则的理由实际上是一致的。当多个进程进行磁盘读写的时候,随机度瞬间飙升。特别是多个进程操做多个文件的时候,磁盘的磁头极可能须要频繁大范围地移动。

若是确实有必要多个进程分别读取多个不一样文件的话,能够考虑下面的替代方案:

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

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

ProcessA   ProcessB   ProcessC
   |          |          |
   |          V          |
   *---->  The File  <---*

改成

ProcessA   ProcessB   ProcessC
   |          |          |
   |          V          |
   *---->  ProcessD  <---*
              |
              V
           The File

顺便还能够在这个服务进程中实现一些本身的缓存机制,配合 Linux 自身的文件缓存进一步优化磁盘 I/O 效率。

以 4kB 为单位写文件

这里能够看看下面这个伪代码:

const int WRITE_BLOCK_SIZE = 4096

for (int i = 0 to 999) {
    write(fd, buff, WRITE_BLOCK_SIZE)
}

其实这个问题,就是我在上一篇文章的 "硬盘文件存取速度的考量" 小节中所说的内容了。
这里有一个常量 WRITE_BLOCK_SIZE, 这并非能够随意取的值,比较合适的是 4096 或者其倍数,理由是文件系统每每以 4kB 为页,若是没有写够 4kB 的话,将致使文件须要多余的读出动做。虽然文件缓存在必定程度上可以帮你缓解,但总会有一部分操做会最落地到底层 I/O 的。因此实际操做中,要尽可能以 4kB 为边界操做大文件。

大目录的寻址效率

有一个问题被提了出来:咱们都知道,当咱们面对一个大目录(目录中有不少不少文件)的时候,这个目录刷出来须要很长的时间。那么咱们在开发的时候是否是要避免常常在这个大目录中读写文件呢?

实际上,当你第一次操做这个大目录的时候,可能延时确实会比较大。可是实测只要进入了这个目录以后,再后续操做的时候,却一点都不慢,和其余的普通目录至关。

这个问题的缘由,我我的猜想(求权威人士指正)是这样的:
  目录在文件系统中,是以一个 inode 的方式存在的,那么载入目录,实际上就是载入这个 inode。从存储的角度,inode 也只是一个普通的文件,那么载入 inode 的动做和载入其余文件同样,也会通过文件缓存策略。载入了一次以后,只要你持续地访问它,那么操做系统就会将这个 inode 保持在缓存中。所以后续的操做,就是直接读写 RAM 了,并不会受到硬盘 I/O 瓶颈的影响。

参考资料

Linux 内核的文件 Cache 管理机制介绍
磁盘I/O那些事
Linux系统结构 详解
【Linux编程基础】文件与目录--知识总结

相关文章
相关标签/搜索