磁盘调度器到底在做什么?
很多做数据库运维的同学,在排查性能问题时,通常会把注意力放在 SQL、索引、连接池、内存参数上。磁盘调度器这个位于内核 IO 路径里的“小角色”,往往被忽略。但在一段时间的高负载下,你会发现同样的硬件、同样的数据库版本,只是换了一个调度器,TPS 和尾延迟的表现可能完全不一样。

磁盘调度器是 Linux 内核负责管理 IO 请求队列的组件。它的核心任务有两个:一是对请求进行合并和排序,二是决定把请求交给底层设备的顺序。听起来很简单,但这个决策过程对数据库这种以随机小 IO 为主的工作负载,影响远比想象中大。
传统磁盘(HDD)的寻道开销很大,所以调度器的核心目标是减少磁头移动,把随机 IO 变成顺序 IO,或者至少降低寻道次数。对于 SSD 和 NVMe 来说,寻道不再存在,但请求的合并、队列深度管理、公平性控制,仍然会影响延迟分布。
数据库的 IO 特征为什么特别敏感?
数据库的 IO 特征和普通文件服务器很不一样。事务提交时的 fsync、redo log 的写、数据文件的随机读取、脏页刷新,这些都是高频操作。调度器如果引入不合理的排队或延迟,就会让某个本来几毫秒的 fsync 变成几十毫秒,然后事务延迟随之升高,连接池开始堆积,最后表现为数据库吞吐下降。
举一个场景:一台服务器上同时运行着数据库和若干业务应用。如果使用 BFQ,它会尽量公平地为每个进程分配 IO 带宽。数据库进程可能只分到一部分时间片,而其它应用的日志写入、文件备份也会争抢 IO 调度器的注意力。结果就是数据库的 IO 延迟出现周期性尖峰,事务提交时间不稳定。这就是调度器在“公平”和“低延迟”之间做出的取舍。
另一个问题是请求合并。Deadline 或 mq-deadline 会对相邻的 IO 请求进行合并,这本来是为了提高吞吐。但数据库的随机 IO 往往没有太多相邻的块,合并的收益有限,反而可能让某些请求在队列里等待更久,增加了尾延迟。
不同调度器的真实差异在哪?
Linux 传统上主要有三种调度器:CFQ、Deadline、NOOP。CFQ 按进程分配时间片,保证公平但延迟很大;Deadline 保证请求截止时间,兼顾吞吐和延迟;NOOP 几乎不做排序,直接把请求交给底层设备。后来内核引入了多队列块层(blk-mq),对应出现了 none、mq-deadline、kyber、bfq。none 就是 NOOP 的替代,适合 NVMe 这类高性能设备;mq-deadline 是 Deadline 的多队列版本;kyber 动态调整队列深度;bfq 是 CFQ 的改进版。
很多内核发行版默认使用的是 mq-deadline 或者 bfq,但这并不一定适合你的数据库工作负载。真正麻烦的地方在于,这些影响不是线性变化的。当队列深度不高、负载轻的时候,调度器差异可能只有几微秒;一旦负载上来,请求排队、合并、优先级调整等因素会叠加,导致数据库性能出现“断崖式”下降。
有一个常见误区:很多人认为 SSD 没有寻道时间,所以调度器无所谓。其实对于 NVMe 设备,IO 路径中的任何额外处理都会消耗 CPU,并影响延迟分布。none 调度器将请求直接传给设备,能最大程度降低内核干预。但如果你用的是老旧的 SATA SSD,或者机械硬盘,无脑选 none 反而可能因为队列管理不当造成延迟上升。
另一个误区是“生产环境必须用 mq-deadline”。这是一个相对安全的选择,但并不是最优的。在纯数据库场景下,mq-deadline 的 deadline 机制可以限制请求的最大延迟,但它仍然会做一些排序和合并,对于随机 IO 占比高的数据库,收益有限。更直接的做法是使用 none,让硬件自身的队列和 IO 调度逻辑去处理。
生产环境怎么选调度器?
首先,需要确认你的磁盘类型和内核版本。使用 lsblk -d -o name,rota,type 可以查看是否为旋转盘(rota=1 表示 HDD,0 表示 SSD)。然后查看当前的调度器:cat /sys/block/sda/queue/scheduler。输出中括号内的就是当前使用的调度器。
# 查看磁盘类型和调度器
$ lsblk -d -o name,rota,type
NAME ROTA TYPE
sda 1 disk
sdb 0 disk
$ cat /sys/block/sdb/queue/scheduler
[none] mq-deadline kyber bfq
如果你确认这是数据库使用的数据盘,并且是 SSD,可以尝试切换为 none:
# 临时切换调度器(重启失效)
$ echo none > /sys/block/sdb/queue/scheduler
如果验证有效,建议通过 udev 规则固化配置。例如,针对所有 SSD 设备设置 none 调度器:
# /etc/udev/rules.d/60-io-scheduler.rules
ACTION=="add|change", SUBSYSTEM=="block", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none"
注意,udev 规则中 ATTR{queue/scheduler} 的写法在内核版本间可能略有差异,而且多队列设备有些属性是只读的,需要通过 /sys/block/*/queue/scheduler 写入,如果失败可能需要使用系统特定的工具,或者直接在启动脚本中配置。
在调整调度器后,务必进行压力测试。不要只看平均延迟,要看 P99 和 P99.9 延迟,以及数据库的 TPS 曲线。可以用 fio 模拟随机读写和 fsync 模式,但更真实的方式是直接跑数据库自带的 benchmark,比如 sysbench 或 pgbench。
下面是一张调度器对比表,供你快速判断:
| 调度器 | 算法特征 | 适用场景 | 对数据库的影响 |
|---|---|---|---|
| none / NOOP | 先进先出,基本不排序 | 高性能 NVMe SSD、硬件已经有很好调度 | 延迟低,但可能无法应对突发 IO |
| mq-deadline | 按截止时间调度,合并相邻请求 | 传统 SSD、HDD,通用型业务 | 保证最坏延迟,但可能引入少量额外排序 |
| kyber | 根据设备性能动态调整队列深度 | 云盘、网络存储 | 自适应好,但调试参数多 |
| bfq | 按进程公平分配 IO 带宽 | 桌面、混合负载、多租户 | 数据库易被其它进程干扰,延迟不稳定 |
| CFQ(旧版) | 完全公平队列,按进程分时间片 | 早期 Linux 通用场景 | 数据库延迟高,已不推荐 |
注意:上面只是常见参考,最终还是要以你的硬件和负载实测为准。
落地建议和避坑清单
这里给出一些我看来比较实用的建议:
- 数据库专用服务器上,如果数据盘是 SSD/NVMe,优先考虑 none 调度器;如果是 HDD,用 mq-deadline 通常更稳。
- 如果一台机器上除了数据库还有其它应用,一定要评估调度器公平性带来的影响。BFQ 可能让数据库延迟恶化,mq-deadline 比 BFQ 好一些,none 则完全不保证公平。
- 定期检查你的内核默认调度器。很多发行版默认使用 mq-deadline,但云厂商的镜像可能是 none,不要假设。
- 数据库的日志盘和数据盘最好分开,并且根据各自特性设置调度器,比如日志盘用 none,数据盘用 mq-deadline,不要一刀切。
- 切换调度器后观察一段时间,特别是在业务高峰期。尾延迟和锁等待时间会给出最直接的反馈。
不要忘记,调度器只是 IO 路径中的一个环节。文件系统(ext4、xfs)、挂载参数(noatime、barrier)、IO 队列深度、中断合并(IRQ 均衡)都会影响数据库性能。调度器的选择是必要的,但不是万能的。它更像是一个基础层面的调优项,你得先保证基础是对的,再去优化上层。
最后说一点:Linux 内核的 IO 调度器本身也在演进。老旧的 CFQ 已经被移除,新的 eBPF 调度器也在实验阶段。对于数据库工程师来说,理解调度器的工作机制和权衡,比死记硬背某个“最佳实践”更有价值。因为硬件的更替速度远快于内核参数,只有真正理解原理,才能在新的存储介质上做出合理的判断。
希望这篇文章能帮你理解磁盘调度器对数据库性能的影响机制,并且在下一次排查数据库 IO 延迟时,多一个可以验证的方向。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/498/