科普文:软件架构Linux系列之【Linux内核的IO调度算法及调度器scheduler】
概叙
科普文:软件架构Linux系列之【并发问题的根源:CPU缓存模型详解】-CSDN博客
科普文:软件架构Linux系列之【Linux的文件预读readahead】_linux预读文件的内容-CSDN博客
科普文:软件架构Linux系列之【图解存储 IO性能优化与瓶颈分析】-CSDN博客


从上面冯诺依曼结构下的cpu、内存、外存之间的延迟就可以看出,磁盘I/O性能的发展远远滞后于CPU和内存,因而成为现代计算机系统的一个主要瓶颈。
存储IO性能优化一直是一个老大难的问题,同时多数时候存储性能问题是一个全局性问题,需要通盘考虑应用、DB、主机、网络、存储设备全IO链路栈的各层可能出现的性能问题或是性能瓶颈。
很多时候计算资源不够,可以搞分布式MapReduce。但是当IO不够(网卡IO,硬盘IO,内存IO等)的时候,我们经常缺少有效的直接的手段去解决这个问题,因为最难扩展的计算资源莫过于IO资源。
相对于CPU计算速度的提升,IO速度提升一直都相对缓慢。
因此操作系统的设计者们为了缓解IO压力,尤其是硬盘IO压力,做了很多的研究和实践工作。

科普文:软件架构Linux系列之【Linux内核源码分析:程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通信(IPC)5个子系统】作者零声-CSDN博客

因为文件系统IO很多时候非常的底层,除了内核开发人员,很少有人去认真地关注IO的具体情况。因此在这篇文章中,我主要介绍的是Linux文件系统IO,希望能以一个系统运维工程师的角度为大家分析一下当前Linux文件系统中的IO算法的特性。

通常情况下,磁盘的读写延迟是由磁头移动到柱面造成的。
为了解决这种延迟,内核主要采用了两种策略:缓存和IO调度算法。
调度算法概念
- 当向设备写入数据块或从设备读出数据块时,请求会被放置在一个队列中等待完成。
- 每个块设备都有自己的队列。
- I/O调度程序负责维护这些队列的顺序,以更有效地利用介质。I/O调度程序将无序的I/O操作变为有序的I/O操作。
- 在进行调度之前,内核必须首先确定队列中一共有多少个请求。

IO调度器的总体目标是希望让磁头能够总是往一个方向移动,移动到底了再往反方向走,这恰恰就是现实生活中的电梯模型,所以IO调度器也被叫做电梯. (elevator)而相应的算法也就被叫做电梯算法.
而Linux中IO调度的电梯算法有4种:
- 预期算法as(Anticipatory)
- 完全公平队列算法cfq(Complete Fairness Queueing)
- 最后期限算法deadline
- NOOP算法noop(No Operation)
具体使用哪种算法我们可以在启动的时候通过内核参数elevator来指定.四种算法对应Linux内核的4种IO调度器。
NOOP算法noop(No Operation)
不对I/O请求排序,但会合并请求,除此之外无其他任何优化。
它用FIFO队列顺序提交I/O请求。
NOOP算法适用于如SSD的随机访问设备,因为随机访问设备不存在磁头移动造成的寻道时间,无需做多余的事情。
最后期限算法deadline
最后期限算法维护了三个队列,分别是具备合并和排序功能的请求队列、读请求队列和写请求队列。这三个队列均为带有超时时间的FIFO队列。
当有新的I/O请求时,会被同时插入普通队列和读/写队列,然后I/O调度器正常处理普通队列中的请求。
当调度器发现读/写请求队列中有请求即将超时时,会优先处理这些请求,以保证尽量不产生饥饿请求。
此算法在全局吞吐量和延迟方面做了权衡,牺牲了一定的全局吞吐量来避免饥饿请求的可能。
当系统存在大量顺序I/O请求的时候,此算法可能导致I/O请求无法被很好的排序,引发频繁寻道。
完全公平队列算法cfq(Complete Fairness Queueing)
每个提交I/O请求的进程都会有自己的I/O队列,此算法将I/O请求按照进程分别放入进程对应的队列中。
CFQ的公平是针对进程而言的,它以时间片算法为前提,轮转调动队列,默认从当前队列中取出4个I/O请求处理,然后处理下一个队列中的4个请求,确保每个进程享有的I/O资源是均衡的。
预期算法as(Anticipatory)
是基于预测的I/O算法,和最后期限算法类似,维护了三个队列。
但是此算法在处理完一个I/O请求之后不会立即返回处理下一个请求,而是等待片刻(默认6ms),等待期间如果有新来的相邻扇区的请求,会直接处理此新来的相邻扇区的请求。
当等待时间结束后,才会处理下一个队列的请求。此算法适合写入较多的环境,不适合MySQL等随机读取较多的数据库环境。
IO调度器(IO Scheduler)

IO调度器(IO Scheduler)是操作系统用来决定块设备上IO操作提交顺序的方法。
IO调度器是Linux内核用来合并对磁盘的I/O请求以提高性能的机制。
内核中实现的IO调度器主要有四种--Noop,Deadline,CFG, Anticipatory。
IO调度器有以下四种算法:
-
Noop: 对于固定磁盘I/O优先权的简单传递。
-
Anticipatory: 对于低延迟,常用于SSD设备。
-
Deadline: 遵循优先级和顺序,尝试在最后一刻提交读I/O。
-
Cfq: 传统的I/O调度器,实现了对任何进程的公平队列。
每个块设备或者块设备的分区,都对应有自身的请求队列(request_queue),而每个请求队列都可以选择一个I/O调度器来协调所递交的request。
I/O调度器的基本目的是将请求按照它们对应在块设备上的扇区号进行排列,以减少磁头的移动,提高效率。每个设备的请求队列里的请求将按顺序被响应。
实际上,除了这个队列,每个调度器自身都维护有不同数量的队列,用来对递交上来的request进行处理,而排在队列最前面的request将适时被移动到请求队列中等待响应。
IO调度器在内核栈中所处位置如下:

IO调度器(IO Scheduler)存在的目的有两个:
- 一是提高IO吞吐量,
- 二是降低IO响应时间。
然而IO吞吐量和IO响应时间往往是矛盾的,为了尽量平衡这两者,IO调度器提供了多种调度算法来适应不同的IO请求场景。
其中,对数据库这种随机读写的场景最有利的算法是DEANLINE。
IO scheduler 的作用主要是为了减少磁盘转动的需求。
主要通过2种方式实现:
- 合并
- 排序
每个设备都会自己对应请求队列,所有的请求在被处理之前都会在请求队列上。在新来一个请求的时候如果发现这个请求和前面的某个请求请求的位置是相邻的,那么就可以合并为一个请求。
如果不能找到合并的,就会按照磁盘的转动方向进行排序。
通常IO scheduler 的作用就是为了在进行合并和排序的同时,也不会太影响单个请求的处理时间。
Linux IO协议栈框图
很多人都知道随机读写和顺序读写在数据吞吐量上有很大的区别。
这个是因为随机读写和顺序读写在IO寻址次数和系统调用次数上有很大的不同。

这张图很清晰的把linux IO协议栈的层次给勾出来了,而且内容很与时俱进,特别是SCSI设备的层次对大家理解sg3这样的包非常有帮助,强烈推荐大家好好研习!
原图位置:https://www.thomas-krenn.com/en/oss/linux-io-stack-diagram/linux-io-stack-diagram_v0.1.pdf
应用程序:
这没什么好说的,通过相关系统条用(如open/read/write)发起IO请求,属于IO请求的源头;
文件系统:
应用程序的请求直接到达文件系统层。文件系统又分为VFS和具体文件系统(ext3、ext4等),VFS对应用层提供统一的访问接口,而ext3等文件系统则具体实现了这些接口。另外,为了提供IO性能,在该层还实现了诸如page cache等功能。同时,用户也可以选择绕过page cache,而是直接使用direct模式进行IO(如数据库)。
块设备层:
文件系统将IO请求打包提交给块设备层,该层会对这些IO请求作合并、排序、调度等,然后以新的格式发往更底层。在该层次上实现了多种电梯调度算法,如cfq、deadline等。
SCSI层:
块设备层将请求发往SCSI层,SCSI就开始真实处理这些IO请求,但是SCSI层又对其内部按照功能划分了不同层次:
* SCSI高层:高层驱动负责管理disk,接收块设备层发出的IO请求,打包成SCSI层可识别的命令格式,继续往下发;
* SCSI中层:中层负责通用功能,如错误处理,超时重试等;
* SCSI低层:底层负责识别物理设备,将其抽象提供给高层,同时接收高层派发的scsi命令,交给物理设备处理。
各层接口
清晰的接口能让复杂的系统变得容易理解和维护。

应用程序 => 文件系统
做开发的人可能都应该了解,通过诸如open/read/pread/write/writev等POSIX接口来调用文件系统各种功能。由于其普遍性,在这里就不一一描述接口形式了。
文件系统 => 块设备层
这里我们将文件系统当成一个整体,并不区分VFS和具体文件系统。块设备层对文件系统提供的接口为submit_bio(),接口形式如下:
void submit_bio(int rw, struct bio *bio)
文件系统向块设备提交的每个bio请求都设置了完成回调函数,记录在bio->bi_end_io。bio请求完成后,通过该字段通知文件系统。
块设备层 => SCSI上层
scsi_reuqest_fn()和struct request_queue。
老实来说,块设备层和SCSI上层之间分的没有那么清楚,耦合的稍微紧密,块设备层看到的IO请求结构是request。而SCSI层看到的IO命令则是scsi_cmnd。
每个scsi设备(如scsi disk)均维护了一个请求队列request_queue,而每个scsi设备对上层呈现的其实是一个块设备。因此,块设备和scsi设备有着天然的联系,request_queue则是连接块设备层和SCSI层的纽带。块设备层对request请求最终会派发至request_queue中。而在特定条件下通过泄流机制将request_queue中积攒的request派发至SCSI层处理。而泄流的实际处理过程就是scsi_request_fn()函数,因此说它是块设备层和SCSI上层的接口也不为过,虽然不是特别准确。在scsi_reuqest_fn内会进行request至scsi_cmnd的转换。
SCSI上层 => SCSI中间层
SCSI上层在收到块设备层发起的scsi命令后马不停蹄又将其转发至SCSI中间层。SCSI上层至SCSI中间层的接口是 scsi_dispatch_cmd
static void scsi_request_fn(struct request_queue *q) {
......
// 设置scsi命令完成回调函数
cmd->scsi_done = scsi_done;
rtn = scsi_dispatch_cmd(cmd);
......
}
SCSI中间层 => SCSI低层
SCSI中间层收到块设备层发下来的scsi_cmnd命令后,中间层作自己处理后,然后再将该命令继续往下传递,接下来该命令到了scsi底层,而传递的接口是 queuecommand()
static int scsi_dispatch_cmd(struct scsi_cmnd *cmd) {
......
rtn = host->hostt->queuecommand(host, cmd);
......
}
host为该设备所属的主机适配器结构。任何一个SCSI主机适配器都需要实现queuecommand接口。注意这个提交过程是异步的,无需等待该命令完成便直接返回。
scsi 命令完成后,会通过记录在命令内的完成函数回调上层处理,具体是cmd->scsi_done。
存储系统可以认为有两个部分:
- 第一个部分站在用户的角度,提供读写的接口。这里的名称空间是以流为中心的。
- 第二部分站在存储设备的角度,提供读写接口,这里的名称空间是以块为中心。插在两者中间提供承接的是文件系统(不是指VFS,我指的是Ext4这样的文件系统)。
简单理解,你的应用程序发出一个读写请求,文件系统负责定位这个读写请求的位置,换成块设备的块,然后把这个请求发到设备上,把文件写入内存中,你的应用程序就从内存中获得数据。
所以,你会发现,上下两个部分是个异步的运行模型,对上半部分来说,不到没有办法的时候(比如你第一次要磁盘中的数据),让数据一直留在内存中是最好的。
因为谁知道你后面还会不会修改这个地方呢?如果你还要读写,同步到磁盘中不是白干了?
所以,数据什么时候和磁盘同步,是个独立的逻辑,和你的应用的要求(比如主动sync),内存空间的大小(比如你Pagecache占据太多的内存了),都有关系。
这个文档不是讨论Cache部分的行为,这个部分理解到这里为止。详细的可以去看前面的mmap内存文件映射。
科普文:软件架构Linux系列之【读懂Linux内存管理:mmap(Memory-Mapped File)内存映射文件】-CSDN博客
科普文:软件架构Linux系列之【详解Linux内存管理】-CSDN博客
科普文:软件架构Linux系列之【图解Linux内存映射】-CSDN博客
本文主要关注下半部分的通用部分的模型,也就是上面图中Block Layer的运行模型。
Block Layer调度模型
数据从Pagecache同步到磁盘上,发出的请求称为一个request,一个request包含一组bio,每个bio包含要同步的数据pages,你要把Page和磁盘的数据进行同步。
和网络子系统不同,磁盘的调度是有要求的,不是说你发一个page,我就帮你写进去,你再发一个page,我就给你再写一个进去。你要写磁盘的一个地方,磁盘要先把磁头物理上移动到那个轨道上,然后才能写,你让磁头这样移来移去的,磁盘的性能就很难看了。
Linux的IO调度器称为evelator(电梯),因为Linus开始实现这个系统的时候,使用的就是电梯算法。
坐过电梯很容易理解什么是电梯算法,电梯的算法是:电梯总是从一个方向,把人送到有需要的最高的位置,然后反过来,把人送到有需要的最低一个位置。这样效率是最高的,因为电梯不用根据先后顺序,不断调整方向,走更多的冤枉路。
为了实现这个算法,我们需要一个plug的概念。这个概念类似马桶的冲水器,你先把冲水器用塞子堵住,然后开始接水,等水满了,你一次把塞子拔掉,冲水器中的水就一次冲出去了。在真正冲水之前,你就有机会把数据进行合并,排序,保证你的“电梯”可以从一头走到另一头,然后从另一头回来。
我们前面讲IO系统的时候就提过磁盘调度子系统的ftrace跟踪,这里我们深入看看blktrace跟踪到的事件的含义:
请求相关
- Q - queued:bio请求进入调度
- G - get request:分配request
- I - inserted:request进入io调度器
调度相关
- B - bounced:硬件无法访问内存,需要把内存降低到硬件可访问
- M - back merge:请求和前一个从后面合并
- F - front merge:请求和前一个从前面合并
- X - split:请求分析为多个request(很可能是因为硬件不支持太大的请求)
- A - remap:基于分区等,重新映射request的位置
- R - requeue: Request重新回到调度队列
- S - sleep:调度器进入休眠P - plug:调度队列插入设备(准备合并)
- U - unplug:调度队列离开设备(全部一次写入设备中)
- T - unplug due to timer超时,而不是数据足够发起的unplug
发出相关
- C - complete:完成一个request的调度(无论成功还是失败)
- D - issued:发送到设备,这个是从下层硬件驱动发起的
我们通过对这些事件的跟踪,对照硬件的特性大概就可以知道运行的模型是否正常了。
[多调度器支持]
但情况千变万化,不是每种磁盘,每个场景都可以用一样的算法。
所以,现在Linux可以支持多个IO调度器,你可以给每个磁盘制定不同的调度算法,这个在/sys/block/<dev>/queue/scheduler中设置。
它的用户接口设计得很好,你看看它的内容就明白我的意思了:noop [deadline] cfq
上面三个是我的PC上支持的三个算法,其中被选中的算法是deadline,写另一个名字进去可以选定另一个调度算法。
我们简单理解一下这三个算法:
- noop是no operation,就是不调度的算法,有什么请求都直接写下去。这通常用于两种情形:你的磁盘是比如SSD那样的内存存储设备,根本不需要调度,往下写就对了。第二种情形是你的磁盘比较高级,自带调度器,OS不需要自作聪明,有什么请求直接往下扔就好了。这两种情况就应该选noop算法
- deadline是一个改良的电梯算法,基本上和电梯算法一样,但加了一条,如果部分请求等太久了(deadline到了,默认读请求500ms,写请求5s),电梯就要立即给我掉头,先处理这个请求。
- CFQ,这个名字是不是有种特别熟悉的感觉?对了,它就是我们前面我们讲CPU调度器时提到的CFS,完全公平调度。这个算法按任务分成多个队列,按队列的“完全公平”进行调度。
利用这个算法,可以通过ionice设定每个任务不同的优先级,提供给调度器进行分级调度。
例如:
ionice -c1 -n3 dd if=/dev/zero of=test.hd bs=4096 count=1000
这个命令要求后面的dd命令按三级实时策略进行调度(实时策略需要root权限)。
ionice不需要CFS就可以工作,只是没有CFQ,策略并不能很好得到执行。通常我们在Desktop上使用deadline,在服务器上用QFQ。
对调优来说,切换调度器和相关调度参数是最简单的方案。更多的其他考虑,那就是看你怎么跟踪了。
[多队列]
另一方面,一个块设备一个队列的策略,面对新的存储设备,也开始变得不合时宜了。我们这样来体会一下这个问题:
普通的物理硬盘,每秒可以响应几百个request(IOPS)
SSD1,这个大约是6万
SSD2,~9万
SSD3,~50万
SSD4,~60万
SSD5,~78.5万
一个队列?开玩笑!
一个队列会带来如下问题:
1. 全部IO中断会集中到一个CPU上
2. 不同NUMA节点也会集中到一个CPU上
3. 所有CPU访问一个队列会造成前面提到的Amdahl模型的spinlock问题
所以,和网络一样,现在块设备子系统也支持多队列模型,称为mq-blk。也和队列网卡一样,这个特性需要你的硬件支持多队列,如果你使用scsi库,你需要你的设备支持mq-scsi。使用m-blk后,io调度器直接作用在每个queue上。
[AIO]
AIO的名字叫异步IO,其实本质上并非是对文件系统的异步封装,而是从块设备层直接出访问接口,只是这个接口恰好是异步的,所以才叫AIO。
AIO的实现在内核fs/aio.c中,现在好像还没有对应的库封装(请注意,posix的aio(7)文档描述的AIO接口是POSIX库自己对文件系统的封装,而Linux内核的AIO现在没有库的封装,需要用户自己做系统调用来实现。除非特别指出,本文涉及的AIO接口都指向Linux内核的AIO,而不是Posix的AIO。
AIO包括如下系统调用:
- io_setup() 创建一个context上下文
- io_destroy()删除context
- io_submit() 发出命令
- io_get_events() 接收命令(事件)
- io_cancel() 取消
这个接口不用过多解释,猜都能猜到怎么用(具体细节自己上网搜),这里想说明的是调度模型。本质上,AIO的“事件”就是我们前面分析块设备接口中的一个“命令”(读写都可以),一个io_submit()就是一次plug(),unplug()的过程,也就是你发起一个submit,发出一组iocb,每个iocb就是一个读写操作,submit的时候就先plug,把请求堵住,然后把iocb灌进去,然后unplug,把请求发出去。
所以,对AIO的跟踪不会比一般的跟踪复杂,因为都是跟踪块设备层的方法。
4种IO调度器(IO Scheduler)
1、NOOP Scheduler(电梯式调度程序)

FIFO
- noop是什么? noop是一种输入输出调度算法 . NOOP, No Operation. 什么都不做,请求来一个处理一个。这种方式事实起来简单,也更有效。问题就是disk seek 太多,对于传统磁盘,这是不能接受的。但对于SSD 磁盘就可以,因为SSD 磁盘不需要转动。
- noop的别称 又称为电梯调度算法.
- noop原理是怎样的?
将输入输出请求放到一个FIFO队列中,然后按次序执行队列中的输入输出请求:当来一个新请求时:
-
如果能合并就合并
-
如果不能合并,就会尝试排序。如果队列上的请求都已经很老了,这个新的请求就不能插队,只能放到最后面。否则就插到合适的位置
-
如果既不能合并,又没有合适的位置插入,就放到请求队列的最后。
-
适用场景
4.1 在不希望修改输入输出请求先后顺序的场景下;
4.2 在输入输出之下具有更加智能调度算法的设备,如NAS存储设备;
4.3 上层应用程序已经精心优化过的输入输出请求;
4.4 非旋转磁头式的磁盘设备,如SSD磁盘
特点:
- 在Linux2.4或更早的版本的调度程序,那时只有这一种I/O调度算法.
- NOOP实现了一个简单的FIFO队列,它像电梯的工作主法一样对I/O请求进行组织,当有一个新的请求到来时,它将请求合并到最近的请求之后,以此来保证请求同一介质.
- NOOP倾向饿死读而利于写.
- NOOP对于闪存设备,RAM,嵌入式系统是最好的选择.
- 电梯算法饿死读请求的解释:
- 因为写请求比读请求更容易.
- 写请求通过文件系统cache,不需要等一次写完成,就可以开始下一次写操作,写请求通过合并,堆积到I/O队列中.
- 读请求需要等到它前面所有的读操作完成,才能进行下一次读操作.在读操作之间有几毫秒时间,而写请求在这之间就到来,饿死了后面的读请求.
2、CFQ Scheduler(完全公平排队I/O调度程序)
CFQ(Completely Fair Queuing)算法,顾名思义,绝对公平算法。它试图为竞争块设备使用权的所有进程分配一个请求队列和一个时间片,在调度器分配给进程的时间片内,进程可以将其读写请求发送给底层块设备,当进程的时间片消耗完,进程的请求队列将被挂起,等待调度。
每个进程的时间片和每个进程的队列长度取决于进程的IO优先级,每个进程都会有一个IO优先级,CFQ调度器将会将其作为考虑的因素之一,来确定该进程的请求队列何时可以获取块设备的使用权。
IO优先级从高到低可以分为三大类:
- RT(real time)
- BE(best try)
- IDLE(idle)
其中RT和BE又可以再划分为8个子优先级。可以通过ionice 去查看和修改。优先级越高,被处理得越早,用于这个进程的时间片也越多,一次处理的请求数也会越多。
实际上,我们已经知道CFQ调度器的公平是针对于进程而言的,而只有同步请求(read或syn write)才是针对进程而存在的,他们会放入进程自身的请求队列,而所有同优先级的异步请求,无论来自于哪个进程,都会被放入公共的队列,异步请求的队列总共有8(RT)+8(BE)+1(IDLE)=17个。
从Linux 2.6.18起,CFQ作为默认的IO调度算法。对于通用的服务器来说,CFQ是较好的选择。具体使用哪种调度算法还是要根据具体的业务场景去做足benchmark来选择,不能仅靠别人的文字来决定。
特点:
- 在最新的内核版本和发行版中,都选择CFQ做为默认的I/O调度器,对于通用的服务器也是最好的选择.
- CFQ试图均匀地分布对I/O带宽的访问,避免进程被饿死并实现较低的延迟,是deadline和as调度器的折中.
- CFQ对于多媒体应用(video,audio)和桌面系统是最好的选择.
- CFQ赋予I/O请求一个优先级,而I/O优先级请求独立于进程优先级,高优先级的进程的读写不能自动地继承高的I/O优先级.
工作原理:
- CFQ为每个进程/线程,单独创建一个队列来管理该进程所产生的请求,也就是说每个进程一个队列,各队列之间的调度使用时间片来调度,以此来保证每个进程都能被很好的分配到I/O带宽.I/O调度器每次执行一个进程的4次请求.
3、DEADLINE Scheduler(截止时间调度程序)
DEADLINE在CFQ的基础上,解决了IO请求饿死的极端情况。
除了CFQ本身具有的IO排序队列之外,DEADLINE额外分别为读IO和写IO提供了FIFO队列。
你可以通过以下命令查看当前使用的IO调度器:

读FIFO队列的最大等待时间为500ms,写FIFO队列的最大等待时间为5s(当然这些参数都是可以手动设置的)。
FIFO队列内的IO请求优先级要比CFQ队列中的高,而读FIFO队列的优先级又比写FIFO队列的优先级高。优先级可以表示如下:
“FIFO(Read) > FIFO(Write) > CFQ”
deadline 算法保证对于既定的 IO 请求以最小的延迟时间,从这一点理解,对于 DSS 应用应该会是很适合的。
deadline 实际上是对Elevator 的一种改进:
. 避免有些请求太长时间不能被处理。
. 区分对待读操作和写操作。
deadline IO 维护3个队列。第一个队列和Elevator 一样, 尽量按照物理位置排序。第二个队列和第三个队列都是按照时间排序,不同的是一个是读操作一个是写操作。
deadline IO 之所以区分读和写是因为设计者认为如果应用程序发了一个读请求,一般就会阻塞到那里,一直等到的结果返回。而写请求则不是通常是应用请求写到内存即可,由后台进程再写回磁盘。应用程序一般不等写完成就继续往下走。所以读请求应该比写请求有更高的优先级。
在这种设计下,每个新增请求都会先放到第一个队列,算法和Elevator的方式一样,同时也会增加到读或者写队列的尾端。这样首先处理一些第一队列的请求,同时检测第二/三队列前几个请求是否等了太长时间,如果已经超过一个阀值,就会去处理一下。这个阀值对于读请求时 5ms, 对于写请求时5s.
个人认为对于记录数据库变更日志的分区,例如oracle 的online log, mysql 的binlog 等等,最好不要使用这种分区。因为这类写请求通常是调用fsync 的。如果写完不成,也会很影响应用性能的。
特点:
- 通过时间以及硬盘区域进行分类,这个分类和合并要求类似于noop的调度程序.
- Deadline确保了在一个截止时间内服务请求,这个截止时间是可调整的,而默认读期限短于写期限.这样就防止了写操作因为不能被读取而饿死的现象.
- Deadline对数据库环境(ORACLE RAC,MYSQL等)是最好的选择.
4、ANTICIPATORY Scheduler(预料I/O调度程序)
CFQ和DEADLINE考虑的焦点在于满足零散IO请求上。对于连续的IO请求,比如顺序读,并没有做优化。
为了满足随机IO和顺序IO混合的场景,Linux还支持ANTICIPATORY调度算法。ANTICIPATORY的在DEADLINE的基础上,为每个读IO都设置了6ms的等待时间窗口。如果在这6ms内OS收到了相邻位置的读IO请求,就可以立即满足。
特点:
- 本质上与Deadline一样,但在最后一次读操作后,要等待6ms,才能继续进行对其它I/O请求进行调度.
- 可以从应用程序中预订一个新的读请求,改进读操作的执行,但以一些写操作为代价.
- 它会在每个6ms中插入新的I/O操作,而会将一些小写入流合并成一个大写入流,用写入延时换取最大的写入吞吐量.
- AS适合于写入较多的环境,比如文件服务器
- AS对数据库环境表现很差.
更改IO调度算法
在RHEL5/OEL5以及之后的版本中(比如RHEL6和RHEL7),可以针对每块磁盘制定I/O Scheduler,修改完毕立刻生效,比如:
$ cat /sys/block/sda1/queue/scheduler
[noop] anticipatory deadline cfq
#修改为cfq
$ echo 'cfq'>/sys/block/sda1/queue/scheduler
#立刻生效
$ cat /sys/block/sda1/queue/scheduler
noop anticipatory deadline [cfq]
一些磁盘相关的内核参数
/sys/block/sda/queue/nr_requests 磁盘队列长度。默认只有 128 个队列,可以提高到 512 个.会更加占用内存,但能更加多的合并读写操作,速度变慢,但能读写更加多的量
/sys/block/sda/queue/iosched/antic_expire 等待时间 。读取附近产生的新请时等待多长时间
/sys/block/sda/queue/read_ahead_kb
这个参数对顺序读非常有用,意思是,一次提前读多少内容,无论实际需要多少.默认一次读 128kb 远小于要读的,设置大些对读大文件非常有用,可以有效的减少读 seek 的次数,这个参数可以使用 blockdev –setra 来设置,setra 设置的是多少个扇区,所以实际的字节是除以2,比如设置 512 ,实际是读 256 个字节.
/proc/sys/vm/dirty_ratio
这个参数控制文件系统的文件系统写缓冲区的大小,单位是百分比,表示系统内存的百分比,表示当写缓冲使用到系统内存多少的时候,开始向磁盘写出数 据.增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能.但是,当你需要持续、恒定的写入场合时,应该降低其数值,一般启动上缺省是 10.下面是增大的方法: echo ’40’>
/proc/sys/vm/dirty_background_ratio
这个参数控制文件系统的pdflush进程,在何时刷新磁盘.单位是百分比,表示系统内存的百分比,意思是当写缓冲使用到系统内存多少的时候, pdflush开始向磁盘写出数据.增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能.但是,当你需要持续、恒定的写入场合时,应该降低其数值,一般启动上缺省是 5.下面是增大的方法: echo ’20’ >
/proc/sys/vm/dirty_writeback_centisecs
这个参数控制内核的脏数据刷新进程pdflush的运行间隔.单位是 1/100 秒.缺省数值是500,也就是 5 秒.如果你的系统是持续地写入动作,那么实际上还是降低这个数值比较好,这样可以把尖峰的写操作削平成多次写操作.设置方法如下: echo ‘200’ > /proc/sys/vm/dirty_writeback_centisecs 如果你的系统是短期地尖峰式的写操作,并且写入数据不大(几十M/次)且内存有比较多富裕,那么应该增大此数值: echo ‘1000’ > /proc/sys/vm/dirty_writeback_centisecs
/proc/sys/vm/dirty_expire_centisecs
这个参数声明Linux内核写缓冲区里面的数据多“旧”了之后,pdflush进程就开始考虑写到磁盘中去.单位是 1/100秒.缺省是 30000,也就是 30 秒的数据就算旧了,将会刷新磁盘.对于特别重载的写操作来说,这个值适当缩小也是好的,但也不能缩小太多,因为缩小太多也会导致IO提高太快.建议设置为 1500,也就是15秒算旧. echo ‘1500’ > /proc/sys/vm/dirty_expire_centisecs 当然,如果你的系统内存比较大,并且写入模式是间歇式的,并且每次写入的数据不大(比如几十M),那么这个值还是大些的好.
实际场景
在说完以上这些调度程序的内在原理,结合实际我们来看看我们线上的使用场景。IO调度器算法的选择,既取决于硬件特征,也取决于应用场景。
- 数据归档业务 — 大量顺序请求,CFQ调度够用(程序不多的情况下Deadline无明显优势,可能会有劣势)
- 云计算业务 — 大量进程同时读写单个控制器,适合Deadline调度
- 跑在SSD上的业务 — 寻址不再是瓶颈,可以考虑NOOP调度,节省CPU对IO排序的消耗。

在线上云环境一般情况下我们推荐使用Deadline算法。但是当使用SSD硬盘的时候,我们可以使用NOOP算法。在服务器只做大文件创建归档的时候,我们选择CFQ算法。
参考:
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)