当前位置:嗨网首页>书籍在线阅读

28-改进读请求

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

4.6.3 改进读请求

每次读请求必须返回最新的数据。因此,当请求的数据不在页缓存中时,读请求在数据从磁盘读出前一直会阻塞——这可能是一个相当漫长的操作。我们将这种性能损失称为读延迟(read latency)。

一个典型的应用可能在短时期发起好几个I/O读请求。由于每个请求都是同步的,后面的请求会依赖于前面请求的完成。举个例子,假设我们要读取一个目录下所有的文件。应用会打开第一个文件,读取一块数据,等待,然后再读下一段数据,如此往复,直到读完整个文件。然后,该应用开始读取下一个文件。所有的请求都是串行进行的:只有当前请求结束后,后续请求才可以执行。

这和写请求(缺省是非同步的)形成了鲜明的对比,写请求在短时间内不需要发起任何I/O操作。从用户空间应用角度看,写操作请求的是数据流,不受硬盘性能的影响。这种数据流行为只会影响读操作:由于写数据流会占用内核和磁盘资源。该现象被称为“写饿死读(writes-starving-reads)”问题。

如果I/O调度器总是以插入方式对请求进行排序,可能会“饿死”(无限期延迟)块号值较大的访问请求。下面,我们再来看一下之前的例子。如果新的请求不断加入,比如都是50~60间的,第109 块的访问请求将一直不会被调度到。读延迟的问题很严重,可能会极大影响系统性能。因此,I/O 调度器采用了一种机制,可以避免“饿死”现象。

最简单的方法就是像Linux内核2.4那样,采用Linus电梯调度法(Linus Elevator)[8],在该方法中,如果队列中有一定数量的旧的请求,则停止插入新的请求。这样整体上可以做到平等对待每个请求,但在读的时候,却增加了读延迟(read latency)。问题在于这种检测方法太简单。因此,2.6内核丢弃了Linus电梯调度算法,转而使用了几种新的调度器算法。

Deadline I/O调度器

Deadline I/O调度器(截止时间I/O调度器)是为了解决2.4调度程序及传统的电梯调度算法的问题。Linus电梯算法维护了一个经过排序的I/O等待列表。队首的I/O请求是下一个将被调度的。Deadline I/O 调度器保留了这个队列,为了进一步改进原来的调度器,增加了两个新的队列:读FIFO队列和写FIFO 队列。队列中的项是按请求提交时间来排序。读 FIFO 队列,如它名字所述,只包含读请求,同样写FIFO队列只包含写请求。FIFO队列中的每个请求都设置了一个过期时间。读FIFO队列的过期时间设置为500毫秒,写队列则为5秒。

当提交一个新的I/O请求后,它会按序被插入到标准队列,然后加入到相应队列(读队里或写队列)的队尾。通常情况下,硬盘总是先发送标准队列中队首的I/O请求。因为普通队列是按块号排列的(linus电梯调度法也如此),这样可以通过减小查找次数来增大全局吞吐量。

当某个FIFO队列的队首请求超出了所在队列的过期时间时,I/O调度器会停止从标准I/O队列中调度请求,转而调度这个FIFO队列的队首请求。I/O调度程序只需检查处理队首请求,因为它是队列中等待时间最久的。

按这种方式,Deadline I/O调度器在I/O请求上加入了最后期限。虽然不能保证在过期时间前调度I/O请求,但是一般都是在过期时间左右调度请求。因此,Deadline I/O调度器能提供很好的吞吐量,而不会让任一个请求等待过长的时间。因为读请求被赋予更小的过期时间,“写饿死读”问题的发生次数降到了最低。

Anticipatory I/O调度器

Deadline I/O调度器表现很好,但是并不完美。回想一下我们关于读依赖的讨论。使用 Deadline I/O调度器时,在一系列读请求中的第一个,在它的截止时间前或马上到来时将会很快被响应,然后I/O调度程序返回,处理队列中其他I/O请求。到现在为止,暂时没什么问题。但是假设应用突然提交一个读请求,而且该请求即将到截止时间,I/O调度器响应该请求,在硬盘查找请求的数据,然后返回,再处理队列中其他请求。这样的前后查找可能持续很长事件,在很多应用中都能看到这样的情况。当延迟保持在很短的时间内时,因为要不断处理读请求并在磁盘上查找数据,所以总的吞吐量并不是很好。如果硬盘能够停下来等待下一个读请求,而不处理排序队列中的请求,性能将会得到一定的提升。不幸的是,在下次应用程序被调度并提交下一个独立的读请求之前,I/O调度器已经移动磁头了。

当存在很多这种独立的读请求时,问题又会浮现出来——每个读请求在前一个请求返回后才会执行,当应用程序得到数据,准备运行并提交下一个读请求时,I/O调度程序已经去处理其他的请求了。这样导致了恶性循环——每次查找时都要进行不必要的寻址操作:查找数据、读数据、返回。是否存在这样的情况:I/O调度器可以预知下一个提交的请求是对磁盘同一部分的访问,等待下次的读,而不必往复进行查找定位。花几毫秒的等待时间来避免“悲催”的查找操作,是很值得的。

这就是anticipatory I/O调度器(期望I/O 调度器)的工作原理。它起源于Deadline机制,但是多了预测机制。当提交一个读操作请求时,anticipatory I/O调度器会在该请求到达终止期限前调度它。和Deadline I/O调度器不同的是,anticipatory I/O调度器会等待6毫秒。如果应用程序在6毫秒内对硬盘同一部分发起另一次读请求,读请求会立刻被响应,anticipatory I/O调度器继续等待。如果6毫秒内没有收到读请求,anticipatory I/O调度器确认预测错误,然后返回进行正常操作(例如处理标准队列中的请求)。即使只有一定数目的请求预测正确,也可以节省大量的时间(为了节省寻道时间,每次进行预测是值得的)。因为大部分读是相互依赖的,预测可以节省大量的时间。

CFQ I/O调度器

尽管在方法上有所区别,但Complete Fair Queuing(完全公平队列,CFQ)I/O调度器和上述两种调度器的目标是一致的。[9]使用CFQ时,每个进程都有自己的队列,每个队列分配一个时间片。I/O 调度程序使用轮询方式访问并处理队列中的请求,直到队列的时间片耗尽或所有的请求都被处理完。后一种情况,CFQ I/O调度器将会空转一段时间(默认10毫秒),等待当前队列中新的请求。如果预测成功,I/O调度器避免了查找操作。如果预测无效,调度程序转而处理下一个进程的队列。

在每个进程的队列中,同步请求(例如读操作)被赋予比非同步请求更高的优先级。在这种情况下,CFQ更希望进行读操作,也避免了“写饿死读”的问题。由于提供了进程队列设置,CFQ调度器对所有进程都是公平的,同时全局性能也很优。

CFQ 调度器适合大多数的应用场景,是很多情况下的最佳选择。

Noop I/O调度器

Noop I/O调度程序是目前最简单的调度器。无论什么情况,它都不进行排序操作,只是简单地合并。它一般用在不需要对请求排序的特殊设备上。

固态驱动器 固态驱动器(solid state drivers,SSDs)如闪存越来越普遍。有很多驱动器如移动手机和平板,根本没有这样的旋转磁盘设备,全部都是采用闪存。像闪存这样的固态驱动器的查找定位时间要远远低于硬盘驱动器的时间,因为在查找给定数据块时没有“旋转”代价。相反,SSDs是以类似随机访问内存的方式来索引:它不但可以非常高效地读取大块连续数据,而且访问其他位置的数据耗时也很小。 因此,对于SSDs,对I/O请求排序带来的好处不是很明显,这些设备很少使用I/O调度器。对于SSDs,很多系统采用Noop I/O调度器机制,因为该机制提供了合并功能(会带来更多好处),而不是排序。但是,如果系统期望优化交互操作性能,会采用CFQ I/O调度器,对于SSDs也是如此。