Linux 性能调优系列(二十): 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
1 | 作者:李晓辉 |
上一篇我们聊了怎么用 iostat、iotop、blktrace 把存储慢的问题找出来。今天聊下一步:找出来是盘慢了之后,怎么让系统更聪明地排队,让 I/O 更顺。
这块的核心就是 I/O 调度器。很多新手以为调度器就是一个开关,其实它就是 Linux 内核帮你决定“哪个请求先发给盘”的那套策略。选错了,SSD 的优势完全发挥不出来;选对了,同样的硬件,延迟能差好几倍。
先搞清楚你的盘是什么性格
现在服务器里大多是 SSD,已经基本把机械硬盘取代了。两者的差异直接决定了调度思路。
机械硬盘 HDD 的特点
有磁头寻道、有转盘旋转延迟。请求是放在磁道上的位置,顺序访问比随机访问快很多。所以老调度器会拼命做合并和重排,让请求尽量从头到尾走一遍,减少磁头移动。
固态硬盘 SSD 的特点
没有机械部件,访问时间在整个盘上是均匀的。寻道时间几乎为零,所以传统的“让请求排成顺序”的优化对 SSD 意义不大。SSD 的痛点反而不是顺序,反而是写放大和寿命。
SSD 调优的重点是:
- 减少写放大,别让盘重复写
- 确保存储单元寿命
- 优化速度
对应的实操常识也很直白:
- SSD 不需要预读和后端写缓存,缓存一般配成直写模式
- 日志功能在 SSD 上要谨慎,因为日志会造成不必要的双重写入,增加磨损
- 传统盘的碎片整理、磁道优化对 SSD 基本没用
所以从 HDD 时代过来的那套调优思路,到 SSD 这边要换脑子了。
多队列时代来了,blk-mq 是基础
现代Linux用的块 I/O 机制是 blk-mq,多队列调度。这套机制把 I/O 请求映射到不同的队列和执行线程,每个 CPU 核心都能独立发请求,减少锁竞争。
NVMe 之所以快,很大程度上就是它天生就支持多个硬件提交和完成队列。Linux 的多队列调度正好能把这个能力发挥出来。
怎么看当前盘在用哪个调度器?
1 | [root@localhost ~]# cat /sys/block/nvme0n1/queue/scheduler |
方括号里的就是当前激活的。没激活的就是可选项。
常用的四个调度器,分别适合谁
内核会根据设备类型自动选一个默认,多数情况下不用动。但想调优,你得知道它们的区别。
none 调度器:极简 FIFO
none 就是先进先出,配合块层的末级合并。没有复杂的排序逻辑。
适合场景:
- 高性能 SSD,特别是 NVMe
- CPU 密集型系统,存储很快,调度本身不要带来额外开销
- 虚拟机里,宿主机 HBA 驱动支持多队列时,客户机也建议用 none
现代Linux对 NVMe 的默认调度器就是 none,官方建议除非有明确理由,尽量别改。
mq-deadline:延迟优先的稳健派
mq-deadline 会给每个请求一个截止时间,尽量保证延迟。它把排队的请求分成读批次和写批次,然后按逻辑块地址递增顺序调度。
默认读批次优先于写批次,因为应用更怕读卡住。处理完一个批次后,它会检查写入操作被饿死的时长,必要时切到写。
这是 现代Linux里最通用、最稳的一个。特别是异步写入为主的场景,以及虚拟机,官方很推荐。
bfq:桌面交互友好
bfq 全称 Budget Fair Queue,目标是桌面系统和交互式任务。它给每个进程分配扇区数的预算,而不是固定时间片。这样一个应用不会把带宽全占住,系统依然响应迅速。
默认配置里 bfq 更追求最低延迟而不是最大吞吐。基于原有的 cfq 演进而来。
适合:
- 复制大文件时避免系统卡死
- 传统的 SCSI 机械硬盘
- 需要人机交互流畅的场景
kyber:自调优的延迟目标派
kyber 会自己算每个 I/O 请求的延迟,并尝试达到你设定的目标延迟。可以单独配置读请求和同步写请求的延迟目标。
适合速度很快的设备,比如 NVMe、高速 SSD,以及其他低延迟设备。它更像是一个“会自己调”的调度器。
怎么改调度器?临时改和永久改不一样
先临时试试水
想临时改一下看效果,直接写文件就行,不用重启,但重启后会丢。
1 | [root@localhost ~]# echo bfq > /sys/block/nvme0n1/queue/scheduler |
你会看到 [bfq] 被激活。
注意rhel8 起 elevator 内核参数就废弃了,rhel9 以上完全无效。所以别再去改内核参数了。
调度器还有可调参数
每个调度器下都有自己的 sysfs 可调项:
1 | [root@localhost ~]# ls /sys/block/nvme0n1/queue/iosched |
比如 mq-deadline 会有:
1 | async_depth front_merges read_expire writes_starved |
fifo_batch 控制每批最大请求数,值越小延迟越低,值越大吞吐越高。这种参数是跟调度器绑定的,换调度器参数就变了。
永久生效的两种正规做法
1. 用 tuned 配置文件,最推荐
调优服务提供了预定义配置,比如 latency-performance 和 throughput-performance。你也可以自己建一个。
最小化系统可能没安装tuned
1 | [root@localhost ~]# dnf install tuned -y |
建目录:
1 | [root@localhost ~]# mkdir /etc/tuned/profiles/my-profile |
写 tuned.conf,例子是给 nvme0n1 设 none:
1 | cat > /etc/tuned/profiles/my-profile/tuned.conf <<-EOF |
重启一下后,列出就有了
1 | [root@localhost ~]# systemctl restart tuned |
设置一下看看
1 | [root@localhost ~]# cat /sys/block/nvme0n1/queue/scheduler |
或者用 sysfs 插件调细参数:
1 | [main] |
就能看到生效了。
2. 用 udev 规则,适合按磁盘唯一标识绑定
先查磁盘的唯一序列号:
1 | [root@localhost ~]# udevadm info --query=property --name=/dev/nvme0n1 | grep -E '(WWN|SERIAL)' |
如果磁盘设备名可能发生变化,可以考虑使用 WWN(World Wide Name)标识磁盘,避免规则只依赖 nvme0n1 这样的设备名。
1 | cat > /etc/udev/rules.d/99-scheduler.rules <<'EOF' |
验证一下语法对不对
1 | [root@localhost ~]# udevadm verify /etc/udev/rules.d/99-scheduler.rules |
重载并触发:
1 | [root@localhost ~]# udevadm control --reload-rules |
这样每次盘被识别都会自动套上调度器,重启也保留。
改完怎么验证?fio 就是你的模拟器
调完调度器不能靠感觉,得用压力测试验证。fio 就是为这个生的。
安装:
1 | [root@localhost ~]# dnf install fio -y |
一个典型的随机写测试:
1 | [root@localhost ~]# fio --name=randwrite --ioengine=libaio --iodepth=1 \ |
参数拆解:
--filename:测试文件,谨慎用,别指到设备上会把盘覆盖--name/--size:作业名和文件大小--rw:read/write/randread/randwrite/readwrite/randrw,可配混合比例--bs/--bsrange:块大小,4k 是数据库常见页大小--direct=1:绕过页缓存,直接测盘速度。0 的时候测的是内存速度--ioengine:libaio 支持异步,iodepth 控制并发请求数--numjobs:并行线程数--group_reporting:合并多个 jobs 的统计--time_based --runtime=60:跑固定时间,而不是跑到文件写满
跑完会输出 clat 完成延迟、IOPS、BW 带宽。同样的工作负载下,延迟下降、IOPS 上升,就是调优有效。
记住一个原则:基准测试要重复多次,单次波动很大。相同条件下多跑几次看趋势才靠谱。
怎么选?给你一个决策小抄
- NVMe SSD -> 默认
none,别乱动 - 普通 SATA SSD,通用业务 ->
mq-deadline,稳 - 虚拟机里 ->
mq-deadline或none - 机械硬盘,交互型负载 ->
bfq - 高速盘,需要自适应低延迟 ->
kyber
先用 iostat 确认瓶颈,再用 fio 复现,最后改调度器验证。别一上来就改,一步一步验证。
