2018年5月13日星期日

Linux IO调度器


1.Linux IO体系

IO调度器系统(I/O Scheduler)是Linux I/O体系中的一个组件,其所处的体系位置如下图所示:

Linux整体I/O体系可以分为七层,它们分别是

  1. VFS虚拟文件系统:抽象出文件系统接口,其下可以对接ext4,xfs等多种具体的文件系统而上层使用者只需要使用VFS提供的统一接口即可。
  2. 文件系统缓存Page Cache/磁盘缓冲Buffer Cache:通过空闲内存将对文件读写的数据放入内存,以获取更快的IO响应。
  3. 映射层:内核通过映射层来确定数据在物理块设备上的位置。
  4. 通用块层:由于绝大多数情况的I/O操作是跟块设备打交道,所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备,对上提供统一的Block IO请求标准。
  5. I/O调度层:大多数的块设备都是磁盘设备,所以有必要根据这类设备的特点以及应用特点来设置一些不同的调度器。
  6. 块设备驱动:块设备驱动对外提供高级的设备操作接口。
  7. 物理硬盘:这层就是具体的物理设备。

以一次读操作为例:

  1. 系统调用read()触发VFS函数,参数为文件描述符与偏移量
  2. VFS确定请求的数据是否已经在内存缓冲区中;若数据不在内存中,确定如何执行读 操作。 
  3. 假设内核必须从块设备上读取数据,这样内核就必须确定数据在物理设备上的位置。 这由映射层(Mapping Layer)来完成。 
  4. 内核通过通用块设备层(Generic Block Layer)在块设备上执行读操作,启动I/O操作, 传输请求的数据。 
  5. 在通用块设备层之下是I/O调度层(I/O Scheduler Layer),根据内核的调度策略, 对等待的I/O等待队列排序。 
  6. 最后,块设备驱动(Block Device Driver)通过向磁盘控制器发送相应的命令,执行 真正的数据传输。

本文介绍图中蓝色部分Linux I/O调度层支持的策略以及比较。



2.Linux I/O调度器

    IO调度器的功能,是将内核接到的IO请求真正提交设备处理之前,进行合并与排序(merging and sorting),然后再分配磁盘IO资源。它的工作是管理块设备的请求队列,决定队列中请求排列顺序以及在什么时候派发请求到块设备。目标是提升“全局”吞吐量。
    IO调度器的两种主要方法:

  • 合并merging
    • 将请求相邻扇区的request合并,这样减少了对磁盘的请求量,也减少了寻址次数
  • 排序sorting
    • 一种排序方式是以扇区增长的方式来排序整个请求队列,这样对扇区时可以保持磁头直线移动即可,缩短寻址时间。
Linux 从2.4内核开始支持I/O调度器,到目前为止有5种类型:Linux 2.4内核的 Linus Elevator、Linux 2.6内核的 Deadline、 Anticipatory、 CFQ、 Noop,其中Anticipatory从Linux 2.6.33版本后被删除了。目前主流的Linux发行版本使用Deadline、 CFQ、 Noop三种I/O调度器。下面依次简单介绍:

1)预测调度器The Anticipatory I/O Scheduler(2.6.33版本后已废除)

      核心是局部性原理,它期望一个进程做完一次IO请求后还会继续在此处做IO请求。在IO操作中,有一种现象叫“假空闲”(Deceptive idleness),它的意思是一个进程在刚刚做完一波读操作后,看似是空闲了,不读了,但是实际上它是在处理这些数据,处理完这些数据之后,它还会接着读,这个时候如果IO调度器去处理另外一个进程的请求,那么当原来的假空闲进程的下一个请求来的时候,磁头又得seek到刚才的位置,这样大大增加了寻道时间和磁头旋转时间。所以,Anticipatory算法会在一个读请求做完后,再等待一定时间t(通常是6ms),如果6ms内,这个进程上还有读请求过来,那么我继续服务,否则,处理下一个进程的读写请求。
      值得一提的是,Anticipatory算法从Linux 2.6.33版本后,就被移除了,因为CFQ通过配置也能达到Anticipatory算法的效果。
      适用场景: 无,已废弃不用。

2)最后期限Deadline调度器

      Deadline调度器对一个请求的多方面特性进行权衡来进行调度,其核心在于保证每个IO请求在一定的时间内一定要被服务到,以此来避免某个请求饥饿。
      Deadline调度器对每个request赋上一个期限值(根据jiffies),并按期限值排序在fifo_list中,读请求的期限时长默认为为500ms,写请求的期限时长默认为5s。
      对读写request进行了分类管理,并且在调度处理的过程中读请求具有较高优先级。因为读请求往往是同步操作, 对延迟时间比较敏感,而写操作往往是异步操作,可以尽可能的将相邻访问地址的请求进行合并,但是,合并的效率越高,延迟时间会越长。因此,为了区别对待读写请求类型,deadline 采用两条链表对读写请求进行分类管理。

      引入分类管理之后,在读优先的情况下,写请求如果长时间得到不到调度,会出现饿死的情况,如当应用程序频繁访问文件的一部分而此时若有另一个远端的请求,那么这个请求将会在很长一段时间内得不到响应,这显然是不合理的。因此,deadline算法考虑了写饿死的情况,从而保证在读优先调度的情况下,写请求不会被饿死。
      所有的请求在生成时都会被
      适用场景: 读多写少的场景。SSD设备,虽然说SSD的随机能力强,已经不像磁盘那样要考虑寻道时间,但是批量写入的能力毕竟远大于随机写入的能力。

3)完全公正排队I/O调度器(Complete Fair Queuing, CFQ)

      每一个提交IO的进程单独自己的队列,每个队列中在,刚进入的请求与相邻请求合并在一起,并进行插入分类。队列由此按扇区方式分类。
      CFQ I/O调度器以时间片轮转调度队列,从每个队列中选取请求数(默认值为4,可配),然后进行下一轮调度。这就在进程级提供了公平。每个进程的时间片和队列长度取决于进程的IO优先级,每个进程都会有一个IO优先级,CFQ调度器将其作为考虑因素之一,来确定该进程的请求队列何时可以获取块设备的使用权。IO优先级从高到低可以分为三大类:RT(real time),BE(best try),IDLE(idle),其中RT和BE又可以再划分为8个子优先级。实际上,Linux中通常只有同步请求(read或sync write)才是针对进程而存在的,他们才会放入进程自身的请求队列,而所有同优先级的异步请求,无论来自于哪个进程,都会被放入公共的队列。
       从Linux 2.6.18起,CFQ作为默认的IO调度器。
       适用场景:  CFQ的算法相对复杂,规划上也已降低寻道时间影响为出发点,适用与大部分情况下的磁盘设备(非SSD),但是对于少量进程占用大量IO的场景就不太适用,后者如数据库应用的场景,肯定是期望优先服务数据库进程,就不太适用CFQ。

4)空操作Noop调度器

      不需要额外调度,IO保存在一个FIFO的队列中,调度器逐个处理请求即可。对于连续块的IO请求会做一些合并优化,不做排序。
      适用场景:

  • 对接的设备有其智能调度逻辑,如果您的Block Device Drivers是Raid,或者SAN,NAS等存储设备,这些设备会更好地组织IO请求,不用IO调度器去做额外工作。
  • 上层的应用程序比IO调度器更懂底层设备。或者说上层应用程序到达IO调度器的IO请求已经是它经过精心优化的,那么IO调度器就不需要画蛇添足,只需要按序执行上层传达下来的IO请求即可。
  • 对于一些非旋转磁头的存储设备,使用Noop的效果更好。因为对于旋转磁头式的磁盘来说,IO调度器的请求重组要花费一定的CPU时间,但是对于SSD磁盘来说,这些重组IO请求的CPU时间可以节省下来,因为SSD提供了更智能的请求调度算法。

3.其他

1)查询系统支持哪些调度器

#dmesg | grep -i scheduler
[   18.137870] io scheduler noop registered
[   18.141874] io scheduler deadline registered (default)
[   18.146970] io scheduler cfq registered
这里显示deadline是默认IO调度器。系统支持noop, deadline, cfq

查询特定设备的调度器
#cat /sys/block/sdb/queue/scheduler
noop [deadline] cfq

2)设置调度器

echo 'cfq' >/sys/block/<磁盘>/queue/scheduler 
可以立即生效
目前可选值: cfq/deadline/noop

3)磁盘调度相关参数


  • /sys/block/sda/queue/nr_requests 磁盘队列长度。默认只有 128 个队列,可以提高到 512 个.会更加占用内存,但能更加多的合并读写操作,速度变慢,但能读写更加多的量
  • /sys/block/sda/queue/read_ahead_kb 一次提前读多少内容,无论实际需要多少.默认一次读 128kb 远小于要读的,设置大些对读大文件非常有用,可以有效的减少读 seek 的次数,这个参数可以使用 blockdev –setra 来设置,setra 设置的是多少个扇区,所以实际的字节是除以2,比如设置 512 ,实际是读 256 k个字节.

4)每个设备的初始化调度器是怎么选择的?

如果系统级别没有明确指定调度器,则会根据每个设备是不是需要寻道的机械磁盘(rotational disk)来进行,根据/sys/block/sda/queue/rotational来确认。如果是0表示不需要寻道,通常是ssd,会选择deadline调度器。否则默认选择cfg.
要调整系统级别的默认调度器,见6)

5)为什么NVMe盘查到的scheduler为none?

使用NVMe协议的SSD(而非SATA),其自身就是忽略了传统的IO调度器,用blk-mq模块替代。
Block multiqueue (blk-mq)模块提供了多队列的排队机制。Blk-mq每个cpu一个软件队列来接受IO请求,这些软件队列再映射到一个或者多个硬件队列中进行处理。这种机制显著降低了锁的争用,适用于能够提供更强IOPS能力的设备,例如NVMe设备。
目前blk-mq模块并没有调度器支持,所以对传统SCSI接口的慢速设备可能导致极大的性能下降,所以针对这类设备并未启用。如果要强制开启,则配置scsi_mod.use_blk_mq=1.

6)永久调整系统级的默认调度器

使用shell命令修改I/O调度器,只是临时修改,系统重启后,修改的调度器就会失效,要想修改默认的调度器,有两种方法使用grubby命令或者直接编辑grub配置文件。
使用grubby命令
例如需要把I/O调度器从cfq调整成 DeadLine ,命令如下:
# grubby --grub --update-kernel=ALL --args="elevator=deadline"

上述命令通过设置内核加载参数, 这样当机器重启的时候,系统自动把所有设备的 I/O调度器变成 DeadLine 。
使用编辑器修改配置文件
也可以直接编辑grub的配置文件 ,通过修改grub配置文件,系统自动把所有设备的 I/O调度器变成cfq。操作过程如下:
#vi cat /etc/default/grub
#修改第五行,在行尾添加#
elevator= cfq 
然后保存文件,重新编译配置文件,
#grub2-mkconfig -o /boot/grub2/grub.cfg
重新启动计算机系统即可。

参考


  • 《Systems Performance: Enterprise and the Cloud》
  • https://www.ibm.com/developerworks/cn/linux/l-lo-io-scheduler-optimize-performance/index.html
  • https://www.cnblogs.com/cobbliu/p/5389556.html
  • http://www.ilinuxkernel.com/files/Linux.Kernel.IO.Scheduler.pdf
  • http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch13lev1sec5.html
  • https://serverfault.com/questions/693348/what-does-it-mean-when-linux-has-no-i-o-scheduler
  • http://tieba.baidu.com/p/2767146878?traceid=


1 条评论:

  1. 我看网上还有一个版本,page cache/buffer cache在file system之下,不是vfs/和fs之间,direct io 直接调用block io layer,请问哪个版本是正确的呢

    回复删除