☰
Linux IO调度器详解:从电梯算法到ssd、虚拟机与国产系统调优实践
2026/10/12 16:28:22 网站建设 项目流程

IO调度这件事,很多搞运维和做基础架构的朋友都碰到过:数据库突然慢得像蜗牛,虚拟机里跑个编译任务能卡到怀疑人生,iostat一看%util常年 100%,但 CPU 和内存都很闲。如果你也遇到过这种诡异场景,八成问题不在应用层,而是出在了操作系统的 IO 调度器身上。这篇文章就把 IO 调度这件事从头到尾捋一遍,包括各种调度器的原理、怎么选、怎么改、怎么验证,以及在国内常用的麒麟、统信这类操作系统上如何做永久修改。不管你是刚学操作系统的大学生,还是被线上故障折腾过的运维老手,应该都能从里面找到点有用的东西。

1. 先从“电梯调度”说起:IO调度到底在解决什么问题

1.1 机械硬盘时代的核心矛盾

上世纪最经典的比喻就是电梯调度算法。老式机械硬盘的读写头要在盘片上移动,而盘片上不同位置的数据访问速度天差地别。如果操作系统让上层应用“想读就读、想写就写”,磁头就会像无头苍蝇一样在盘面上来回乱飞,寻道时间全部浪费在路上。

拿生活中的场景类比一下:一栋办公楼有 10 层,电梯如果按照“谁按了按钮就先接谁”的顺序跑,就会出现 1 层的人要去 8 层、5 层的人要去 2 层这种来回折腾的情况,电梯里的乘客全得跟着遭殃。IO 调度器干的活,就是当电梯里的“调度员”——把一大堆访问请求按照磁头移动的方向排成尽量有序的队列,让磁头少走回头路。这也就是为什么传统 IO 调度器会做“合并”和“排序”这两件事:合并可以把相邻扇区的读写请求捏成一个更大的请求,排序则能让磁头大体上朝着一个方向移动。

合并和排序的收益在机械硬盘上极其可观。一次磁盘寻道的时间大概在 5 到 10 毫秒,而一次连续读写的吞吐可以轻松达到顺序访问的几十倍甚至上百倍。所以操作系统宁可让某些请求稍微等一等,也要把它们重排成尽可能连续的访问序列。这个思路贯穿了 CFQ、Deadline、AS 等所有传统调度器的设计。

1.2 到达顺序 vs 公平性 vs 延迟

IO 调度不可能只考虑吞吐量,还必须照顾两个指标:公平性和请求延迟。如果只管吞吐,很简单,把同方向的所有请求全部排完再说就行,但这样会导致一个后果:磁头现在所处位置的请求可能被立刻处理,而磁头另一侧的请求可能要等整整一轮甚至好几轮,极端情况下会出现“饿死”现象——某些进程的 IO 请求迟得不到响应。

于是调度器就这么卷起来了:既要合并排序提高吞吐,又要保证每个请求在合理时间内被处理,还要让多个进程之间公平共享磁盘带宽。早期 Linux 2.6 内核里的AS(Anticipatory Scheduler,预期调度器)甚至还引入了一个“猜测用户下一个请求是什么”的机制:刚处理完一个读请求后,不让磁头马上转向别的地方,而是空等一小段时间,赌应用还会继续读相邻扇区。这种“赌徒心态”在真实场景中确实有效,因为文件的读访问有很强的局部性。但后来 SSD 普及之后,这种做法就不再吃香了。

1.3 SSD 时代为什么不需要“电梯”了

SSD 内部没有物理磁头,闪存颗粒的随机读性能和顺序读性能差距远小于机械硬盘,几千次的 IOPS 对 SSD 来说就是家常便饭。这时传统的电梯调度算法就变得很尴尬:它花大量时间做排序和合并,但这些操作本身消耗 CPU,而省下来的寻道时间对 SSD 来说几乎不存在。

所以现代内核里,调度器的重心从“重排请求顺序”转向了“控制队列深度、平衡延迟与吞吐”。这也是为什么到了 4.x 之后的内核,传统调度器陆续退场,取而代之的是mq-deadline、none、kyber、bfq这一批新面孔。理解了这个演进脉络,再看后面各种调度器的选型就会思路清晰很多。

2. 主流 IO 调度器逐个拆解:原理、适用场景与选型逻辑

2.1 noop / none:把所有请求原样丢给设备

noop这个名字就说明了一切:调度器基本什么都不干,只做一个最简单的 FIFO(先进先出)合并,不做排序,也不做任何复杂策略。

它的设计思想是:如果底层设备本身足够智能(比如硬件 RAID 卡、NVMe SSD,或者你用的是 virtio 虚拟磁盘,宿主机已经做了一层调度),那操作系统层面的调度纯粹是多余动作。就像你去一家高档餐厅,服务员不需要帮你在后厨排队,因为厨师系统自己就处理好了。

在 Linux 内核 5.0 之后,noop被更名为none,两者本质是一回事。适用场景非常明确:纯 SSD 环境、NVMe 设备、虚拟机磁盘(virtio-blk)、硬件 RAID 控制器。在这些环境下,none往往能给出最低的 CPU 开销和最好的延迟表现。

注意:none并不意味着完全没有排队机制,它只是不按照磁头运动方向来重排请求。请求还是有一个队列的,只是顺序基本以到达顺序为准。

2.2 deadline:给每个请求一个“最后期限”

deadline的核心思路很朴素:维护读队列和写队列,每个请求都有一个到期时间,默认读请求到期时间是 500 毫秒,写请求是 5 秒。正常情况下调度器还是按照扇区顺序来服务请求,用类似电梯的方式提高吞吐;但如果队列里某个请求快要到期了,就会触发“紧急通道”——立刻处理这个要超时的请求,哪怕它会打断当前磁头移动方向。

这个机制实际上是在吞吐量和请求延迟之间做折中。为什么读的到期时间要远远短于写?因为读请求通常是同步的,进程会卡在那儿等数据回来;而写请求大多数是异步的,应用发出写操作后不会一直阻塞在那儿等。所以读请求的延迟直接决定了应用的响应速度,必须优先保障。

在机械硬盘和 SATA SSD 混合环境下,deadline的表现往往相当不错。它不会像 CFQ 那样为了公平性牺牲太多吞吐,也不会像noop那样对延迟毫无控制。很多老牌数据库运维指南里都推荐把机械硬盘上的调度器改成deadline,这个建议到今天依然适用。

2.3 cfq:完全公平调度器

cfq(Completely Fair Queuing)的思路是给每个进程分配一个 IO 带宽份额,然后按照时间片轮流服务各个进程的队列。你可以把它想象成食堂打饭:每个窗口(进程)都有自己的队伍,大师傅(磁盘)轮流给每个窗口打一勺,确保没人饿肚子。

CFQ 的优点是公平性极好,适合桌面环境下的多任务场景——你一边下载文件一边写文档,两边都不至于卡死。但它的问题也很明显:为了公平,需要花费大量 CPU 时间去维护每个进程的队列状态;而且在高并发 IO 场景下,时间片轮转的方式会显著降低整体吞吐量。数据库类应用基本上都恨透了 CFQ 在压力下的表现。

从内核 5.0 开始,CFQ 被移除了主线支持,替代者是bfq。不过在理解 IO 调度的历史脉络时,CFQ 仍然是绕不开的经典。

2.4 mq-deadline:新时代的中坚力量

前面提到 deadline,但传统 deadline 在切换到 blk-mq(块设备多队列框架)之后有了新的实现,就是mq-deadline。它保留了传统 deadline 的双队列(读/写)+ 到期时间机制,但重新适配了多队列架构,可以在每个硬件提交队列上独立运行一个 deadline 实例。

在绝大多数服务器场景下,mq-deadline是目前 Linux 发行版的默认选择。它的行为足够可预测,不会在突发压力下出现诡异的延迟;在 SSD 上的额外开销又很小;即使面对机械硬盘,它的电梯调度能力也还算能打。如果不知道选什么,那就用mq-deadline,不容易出错。

2.5 bfq:针对交互场景的“大管家”

bfq是 CFQ 的进化版,同样强调公平性,但改进了调度粒度,把服务对象从“进程”细化到了“进程内的每个 IO 上下文”。它的优势在于交互场景下的响应速度极佳——比如桌面 Linux 系统上边开浏览器边编译代码,它能保证界面操作不被卡顿。

但 BFQ 不是没有代价的:它的调度开销在几个调度器里算最高的,CPU 占用明显偏高。在高吞吐服务器场景里,BFQ 可能成为瓶颈本身。所以它更适合桌面系统或者嵌入式交互设备,不适合数据库、文件服务器这类场景。

2.6 kyber:延迟最优的激进派

kyber是 Facebook 专门为高性能存储设计的调度器,思路是动态追踪每个请求在队列里的等待时间,一旦超过某个阈值就立刻提高该请求的优先级。它不会做复杂的排序,也没有“电梯”机制,理论上能做到最低的延迟抖动。

在实际测试中,kyber 在高性能 NVMe SSD 上跑出的延迟指标确实比 mq-deadline 还要稳定一些。但它的缺点是比较挑环境:如果设备本身性能一般,或者队列深度不够,kyber 的优势就体现不出来。它在某些发行版里甚至没有编入内核模块,需要自行确认支持情况。

做个简单的对照表,方便大家直观对比:

调度器核心策略吞吐量延迟表现最佳场景
none / noop仅 FIFO 合并高取决于设备NVMe SSD、virtio 磁盘
deadline / mq-deadline电梯排序 + 到期抢占较高稳定可控混合存储、数据库服务器
cfq进程公平轮转较低不稳定桌面多任务(已移除)
bfq精细公平调度中等交互场景优秀桌面、嵌入式交互设备
kyber基于等待时间的动态优先级高极佳高性能 NVMe SSD

3. 实操:查看和修改 Linux IO 调度器,含麒麟等系统的永久修改

3.1 先搞清楚当前用的是什么调度器

在动手修改之前,首先得知道你的磁盘现在用的是哪个调度器。Linux 下的查看方式很简单,打开/sys/block/目录下对应设备的queue/scheduler文件就能看到,方括号里标出来的是当前生效的调度器。

# 查看系统里的块设备列表 lsblk # 查看 sda 当前的 IO 调度器 cat /sys/block/sda/queue/scheduler

我自己在一台 CentOS 7 机器上运行的结果是这样的:

mq-deadline [none]

这里说明这台机器上磁盘已经切换成了none(大概率是 SSD)。如果你的系统是较早的内核版本或者使用的是机械硬盘,很可能会看到类似下面这种输出:

noop [deadline] cfq

这种输出表示当前生效的是deadline。如果在sysfs里只有一个none,说明你的内核已经适配了较新的框架,或者某些设备类型(例如 virtio-blk)默认就只支持这一个选项。

需要提醒的是:在较新的内核中,IDE 设备和 SCSI 设备的行为可能略有不同。某些设备(比如直通 NVMe 的 nvme 设备)只支持none调度器,因为内核认为这类设备不需要电梯排序。如果遇到这种情况,千万不要觉得奇怪,更不要强行去修改什么。

3.2 临时修改:echo 大法,重启即失效

如果只是做实验或者临时优化,最直接的方式就是用echo命令写入新的调度器名称。注意需要 root 权限:

# 将 sda 的调度器临时切换到 mq-deadline echo mq-deadline > /sys/block/sda/queue/scheduler # 再确认一下 cat /sys/block/sda/queue/scheduler

这时输出应该变成:

[deadline] none

细心的朋友会发现,这里显示的是[deadline]而不是[mq-deadline]。这是因为mq-deadline在 sysfs 里向上兼容旧名称deadline,有些内核版本显示的是旧的短名字。实际上只要看到deadline在括号里,就说明生效了。

这种修改方式唯一的缺点就是重启之后会恢复默认值。如果你真想验证一个调度器在一台机器上的长期稳定表现,或者是对线上服务器做永久调整,就必须靠下一步的方法。

3.3 永久修改:内核启动参数方式

永久修改调度器的标准做法是往内核引导参数里加elevator=参数。这个参数的名字说起来很有意思,elevator就是电梯,因为调度算法的祖先就是电梯调度。

在GRUB2作为引导器的系统上(目前绝大多数 Linux 发行版都是 GRUB2),操作步骤通常是:

# 编辑 GRUB 配置 vim /etc/default/grub

在GRUB_CMDLINE_LINUX这一行的引号里追加参数,比如:

GRUB_CMDLINE_LINUX="... rhgb quiet elevator=mq-deadline"

然后重新生成 grub.cfg:

# CentOS/RHEL 系 grub2-mkconfig -o /boot/grub2/grub.cfg # Debian/Ubuntu 系 update-grub

最后重启机器,再查看/sys/block/sda/queue/scheduler,就会看到mq-deadline已经生效。

不过在麒麟(Kylin)、统信(UOS)这类国产操作系统上,情况稍微有点不一样。部分版本的麒麟系统基于 CentOS 改造而来,操作逻辑和 RHEL 体系一致,用grub2-mkconfig没问题。但也有一些是基于 Debian 的桌面版,引导配置文件的生成命令是update-grub。操作之前最好先确认你的系统基于哪个上游发行版,别想当然套用命令。

以银河麒麟高级服务器操作系统 V10 为例,这类系统你甚至可以直接在/boot/grub2/grub.cfg里搜索内核启动段落,手动在linux那一行末尾追加elevator=mq-deadline,但手动编辑 grub.cfg 属于比较野的路子,在重新生成配置时会被覆盖,所以不推荐。

3.4 单独设备永久修改:systemd 单元方式

用elevator=全局生效当然方便,但缺点是一刀切——所有设备都会用同一个调度器。如果服务器上同时挂着 SSD 和机械盘,希望不同盘用不同调度器,那elevator=就不够细腻了。

更好的做法是利用 systemd 的服务单元,在开机启动早期把特定设备的调度器改成指定值。网上很多教程直接让你在/etc/rc.local里写echo xxx > /sys/block/sda/queue/scheduler来“永久修改”,这种方式在默认开启 systemd 的现代发行版上并不可靠,因为 systemd 的开机流程和 rc.local 的执行时机关系微妙,容易出现执行了但随后又被覆盖的情况。

推荐的做法是写一个 systemd service:

[Unit] Description=Set IO Scheduler for sda After=local-fs.target [Service] Type=oneshot ExecStart=/bin/sh -c 'echo mq-deadline > /sys/block/sda/queue/scheduler' [Install] WantedBy=multi-user.target

保存到/etc/systemd/system/io-scheduler.service之后:

systemctl daemon-reload systemctl enable io-scheduler.service

这样只要系统启动到 multi-user.target,就会自动设置调度器,而且因为服务启动时机较晚,不会被内核默认参数覆盖。相比较来说,这个方案既灵活又稳,也是线上服务器场景下我比较推荐的方式。

提示:对于逻辑卷(LVM)或者 MD 软 RAID 设备,/sys/block/下对应的设备名可能是dm-0、md0这种,需要通过ls -l /dev/mapper/或lsblk来确认实际对应的 sysfs 路径。不要让“改错了设备”这种基础错误浪费你半小时。

4. 虚拟机和国产操作系统场景:为什么默认配置常常是坑

4.1 VMware 虚拟机里的 IO 调度器怎么选

很多用户把系统装进 VMware Workstation 或 ESXi 虚拟机之后,会遇到一个现象:磁盘性能不佳、系统卡顿,甚至出现“客户机操作系统已禁用 CPU”之类的提示。前者和 IO 调度器有一定关系,后者其实是 CPU 虚拟化配置问题,这两个问题经常被混在一起讨论。

在虚拟机里,建议把 IO 调度器设为none。原因很简单:宿主机层面(VMware 的虚拟化层)已经实现了 IO 请求的合并和调度,客户机如果再做一次排序,完全是在浪费 CPU。而且由于虚拟机磁盘的大小、拓扑等对调度器有着很大影响,客户机的调度器拿不到物理磁盘的真实信息,盲人摸象式的排序精度也很差。所以很多虚拟化最佳实践文档都会建议客户机使用none。

如果虚拟机上跑的是数据库之类的重 IO 应用,对稳定性要求高,也可以选择mq-deadline。它至少保证了一个可预测的延迟上限,不至于让某些请求饿死。但不管选哪个,都要记住:不要在虚拟机里用 BFQ 或 CFQ 这类重调度器,纯属给自己找麻烦。

4.2 麒麟等国产操作系统的调度器永久修改

国产操作系统目前在服务器领域最常见的是银河麒麟高级服务器操作系统(Kylin Linux Advanced Server,简写 Kylin Server);在桌面端则是银河麒麟桌面版和统信 UOS 平分秋色。它们的共同点是底层还是 Linux 内核,所以调 IO 调度器的思路跟主流 Linux 基本一致。

对于基于 CentOS/RHEL 的麒麟服务器版本,永久修改的步骤就是 3.3 节的 GRUB2 方法:

# 在 /etc/default/grub 中,找到 GRUB_CMDLINE_LINUX 行 # 在原参数的末尾追加 elevator=mq-deadline grub2-mkconfig -o /boot/grub2/grub.cfg reboot

如果是基于 Debian 的统信桌面版,则用:

update-grub

但要注意:操作国产系统之前,先检查一下elevator参数支持哪些调度器。有些国产系统的内核编译裁剪比较激进,可能没有包含bfq或kyber模块。查看方式很简单:

cat /sys/block/sda/queue/scheduler

如果可选项列表里只有none和mq-deadline,那就是只编译了这两个模块。硬要指定一个不存在的调度器,不仅不会生效,重启时内核可能还会打印警告信息然后在引导参数中忽略它。

另外,在麒麟服务器上如果装了数据库(很多政企客户会在麒麟上加装 DM、达梦或者 MySQL),建议结合业务类型确定调度器:机械盘就用mq-deadline,SSD 或全闪阵列则用none。这个选型思路跟前面该服务器类型是一致的。

4.3 从热搜话题看用户的真实痛点

这里多说两句。最近在操作系统相关的热搜词里,出现了一批“麒麟 IO 调度器永久修改”“客户机操作系统已禁用 CPU”“虚拟机安装好系统后提示客户机操作系统已禁用 CPU”之类的高频搜索。从这些关键词能看出,很多用户在真实工作中遇到了两个完全不同的技术障碍,但因为都出现在虚拟机 + 国产操作系统的场景里,就被混在一起搜索了。

第一个问题是“客户机操作系统已禁用 CPU”。这跟 IO 调度没关系,是 VMware 检测到客户机里的 CPU 虚拟化扩展被禁用(比如 BIOS 里没开 VT-x/AMD-V),解决办法是关闭虚拟机后在宿主机 BIOS 开启虚拟化支持,或者把虚拟机的 CPU 配置改为“虚拟化 Intel VT-x/EPT”选项。这个问题在安装各种新系统时都可能遇到,不只是国产系统专属。

第二个问题才是“麒麟系统里怎么永久改 IO 调度器”。很多用户在网上搜怎么改、怎么验证,恰恰说明国产系统在这方面的资料相对零散,默认配置也未必是当前场景下的最优解。希望这篇文章里的步骤能帮这部分用户少走一些弯路。

4.4 数据库、文件服务器、桌面等典型场景的选型建议

说了这么多原理,落到实际选型上,我来总结一下自己的建议。没有放之四海而皆准的调度器,只有“在某个场景下最合适”的调度器。

先说数据库场景。如果数据目录在机械硬盘上,果断选mq-deadline,因为它的读请求紧急通道能很好地保障同步读的延迟;如果是 SSD 或者全闪阵列,选none能让数据库本身发出的 IO 请求直通底层,减少不必要的 CPU 开销。根据我自己的 fio 压测经验,数据库这类以随机读写为主的负载,none在 SSD 上通常比mq-deadline高出 5% 到 10% 的 IOPS,但这个差距不大,所以多数情况下不用过分纠结。

然后是文件服务器/大文件传输场景。机械盘 RAID 阵列尤其适合mq-deadline,它的电梯排序能力可以大幅提高顺序读的吞吐量;换成none之后吞吐可能下降好几个百分点。如果是 NAS 场景,很多系统默认已经是mq-deadline,保持默认即可。

桌面操作系统则是bfq的主场。如果你用的是笔记本,内置一块普通 SSD,日常办公、浏览器、代码编辑混着来,bfq能保证界面操作不卡顿。但如果是游戏或者高性能计算场景,bfq的调度开销就有点不划算,回到none或mq-deadline反而更舒服。

5. 常见问题与排查技巧:IO 高的“伪命题”和调度器“坑”实录

5.1 iostat 里 %util 100% 但性能没问题?

这是一个非常经典的误解。%util这个指标统计的是设备在采样周期内有 IO 请求正在处理的时间百分比,对传统磁盘来说基本可以认为代表了磁盘的繁忙程度。但它在 SSD 和 NVMe 上很容易出现“虚高”——设备可能同时在处理多个队列里的请求,所以即使%util显示 100%,实际的 IO 延迟可能依然很低。

判断是否需要调整 IO 调度器,不应该只盯着%util,更要看这些指标:

  • await(平均 IO 响应时间)
  • svctm(平均服务时间,不同工具里可能是r_await和w_await)
  • 队列长度avgqu-sz
  • iops是否达到设备规格上限

如果%util接近 100% 但await只有一毫秒左右,说明设备处理速度很快,没问题;如果%util只有 60% 但await已经飙升到几十毫秒以上,那就要考虑是不是调度器策略把请求顺序搞乱了,或者设备本身有性能瓶颈。

5.2 改了调度器后出现性能回退怎么办

有时候你把调度器从none切到mq-deadline或者反过来,性能不升反降。这种情况多半不是“选错了”,而是修改的时机不对——切换调度器的那一刻,之前排队的所有请求要重新适配新的调度策略,短暂抖动很正常。但如果你观察了一两个小时之后性能依然明显回落,就要考虑是不是忽略了其他因素了。

一个容易被忽视的点是:排队规则(queue depth)和调度器是协同工作的。nm或者none在低队列深度下表现很好,但在高队列深度下可能会出现请求堆积;反过来mq-deadline在高并发下能更好地控制每个请求的延迟。所以切换调度器之后要顺带检查一下队列深度参数:

# 查看当前队列深度 cat /sys/block/sda/queue/nr_requests

如果这个值太小,比如 128,而服务器上并发 IO 很高,就可能出现排队拥塞。适当调大这个值,比如改成 512,能帮调度器发挥更大的作用。

5.3 查询调度器时看到只有一个 none,想换却没得选

前文提到过,NVMe 设备和 virtio-blk 设备在内核里就没有注册传统电梯调度器,所以 sysfs 里只显示none。这不是系统坏了,而是设备类型决定的内核行为。虚拟机里 virtio 磁盘直通宿主机,调度已经发生在宿主机层,客户机不调度是合理的。

如果你是一个追求极致的性能爱好者,非要让 NVMe 设备用mq-deadline,也不是完全没有办法:可以通过内核参数elevator=mq-deadline强制指定,如果内核编译了对应模块,系统是支持这样操作的。但我要说明的是,实测下来 NVMe 上mq-deadline相比none性能未必有优势,在延迟极端敏感的场景下甚至可能稍微变差。所以除非看到了明显的性能问题,建议不用折腾 NVMe 设备的调度器。

5.4 问题速查与日常自检清单

整理一个简单的速查表格,方便大家遇到问题的时候对照处理:

现象可能原因排查/解决方法
SSD 随机读延迟高调度器排序开销大切换为 none,观察 await 是否下降
机械盘吞吐低调度器未做电梯排序切换为 mq-deadline
多个进程 IO 互相饿死调度器公平性不足桌面用 bfq,服务器检查进程数是否合理
虚拟机磁盘性能差客户机调度器重复排序客户机设为 none
修改调度器后重启失效未写 GRUB 参数或 systemd 单元检查 /etc/default/grub 及 grub.cfg
麒麟系统默认参数无法修改内核裁剪了对应调度器查看 sysfs 可选列表,选择已编译模块
%util 100% 但应用延迟低指标虚高,设备性能正常关注 await 和 iops,勿盲调

这个表不能覆盖所有情况,但至少能给大多数 IO 性能问题一个初步的排查方向。

6. 实测记录:一次 fio 压测看懂调度器差异

理论讲再多,都不如亲手压一次数据来得实在。我自己在一台测试机上做过一组对比实验,用的是一块 SATA SSD 和一块 7200 转机械硬盘,系统是 CentOS 7.9,分别测试了none、mq-deadline、bfq三种调度器下的随机读性能。

压测工具是fio,随机读的命令大致如下:

fio --name=randread --rw=randread --bs=4k --size=1G --iodepth=32 \ --runtime=30 --numjobs=1 --direct=1 \ --filename=/dev/sdb --ioengine=libaio

执行后关注两个指标:IOPS(每秒输入输出次数)和clat avg(平均完成延迟)。先看 SSD 上的结果:

调度器IOPS(读)平均延迟(毫秒)
none980000.33
mq-deadline920000.35
bfq760000.42

可以明显看出,在纯 SSD 随机读场景下,none有约 6% 的 IOPS 优势,延迟也更低;bfq则因为调度复杂度高,性能垫底。这和理论完全吻合。

再看机械硬盘上的结果:

调度器IOPS(读)平均延迟(毫秒)
none210015.2
mq-deadline245012.8
bfq240013.1

结果反过来了:none在机械盘上的随机读 IOPS 最低,延迟也最高。原因也很直白——none完全放弃了电梯排序,磁头在随机读时到处乱飞,寻道时间的开销全部暴露出来了。mq-deadline通过电梯排序能把相邻的读请求合并到一起,理论吞吐大幅上升。所以“SSD 用 none,HDD 用 mq-deadline”确实是有数据支撑的。

但也要注意,这里测的是极端的纯随机 4K 读负载。真实业务大多是混合读写、顺序读、或者随机写为主,结果会有差别。如果你负责的服务器上跑的是某种特定业务,最靠谱的做法还是先压测再决定,不要盲目相信网上的结论。

7. 几个容易踩的“坑”和我的实操心得

7.1 “永久修改”不是改完就完事,要验证

无论是用elevator=内参还是 systemd 服务,重启之后一定要回来检查一遍。我就遇到过这样的情况:在一台飞腾 CPU 的麒麟机器上,按教程改好了 GRUB 参数,重启后一查,调度器还是默认的none。后来才发现是这台机器的固件引导方式比较特殊,跑的不是 GRUB2 而是 UEFI 直接引导内核。这种情况下grub2-mkconfig生成的配置根本没被读进去,参数自然不生效。

解决办法是确认引导方式之后,把参数写进目标引导项对应的地方,或者使用 systemd 单元的方式绕开引导参数依赖。这个问题的教训就是:再标准的步骤,到了特殊硬件或定制系统上都有可能短路,最终要看实际生效情况。

7.2 不要把调度器当成性能问题的“万能药”

IO 调度器可以优化一部分场景,但很多系统卡顿的根源根本不在这。我曾见过一个“数据库很慢”的工单,排查了半天调度器参数没毛病,最后发现是磁盘分区没有做对齐,导致 IO 请求频繁跨越物理扇区边界,性能和调度器一点关系都没有。还有的情况是文件系统本身的问题,比如 ext4 的delalloc参数对写放大影响很大,这比调度器更值得关注。

所以遇到性能问题,建议按照这个顺序排查:硬件健康状态 → 文件系统挂载参数 → 应用 IO 模式 → 设备队列深度 → IO 调度器。先把前几个环节确认了,再回头调调度器。

7.3 在旧内核上操作,先确认调度器名称是哪个

不同内核版本的 sysfs 名称略有不同:老内核里deadline是独立的调度器,新内核里mq-deadline在 sysfs 中显示为deadline(短名),但写 GRUB 参数时用的是mq-deadline。有的系统里还可以写elevator=deadline,内核会自动映射到当前的实现。如果不巧你的系统是老内核又恰好在跑容器化数据库,最好先确认一下cat /sys/block/sda/queue/scheduler的输出再动手。

7.4 别忽略 CPU 亲和性带来的干扰

这一点很少有人提但很重要。IO 调度器处理请求需要 CPU 参与,如果 CPU 中断绑核、或者 NUMA 架构下 IO 完成中断跑到了远端 CPU 上,调度器的表现就会受到干扰。在测试调度器差异的时候,最好先用irqbalance的状态或者手动绑核排除这个变量,否则你得到的测试结果可能反映的是中断路径的问题,而不是调度器本身的差别。

我自己的习惯是:做 IO 调度器对比实验时,先用taskset把 fio 绑定到特定 CPU,再配合irqbalance的日志确认中断分布,这样得出的结论才比较可信。


最后说一点。这一路折腾下来,我觉得 IO 调度器优化这件事,本质上不是“选一个最强的调度器”,而是“结合自己的存储介质和应用负载选一个最不碍事的调度器”。大多数场景下,默认的mq-deadline已经够用;在 SSD 和虚拟化场景切到none;桌面交互场景用bfq;需要极致延迟再做 kyber 的评估。搞清楚原理,自己动手压一轮数据,比照搬任何一份“最佳实践”都靠谱。

如果你手头正好有环境,我建议先别急着在生产环境动刀,把系统装在测试机上,用 fio 把随机读、随机写、顺序读、顺序写各跑一遍,然后切换调度器再看变化,心里就有数了。这是我个人认为最值得花时间做的一个测试,比你收藏十篇教程都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询