Linux 磁盘 I/O 调度器选型与切换实战

Linux 磁盘 I/O 调度器选型示意

本文教你如何为不同存储介质选择合适的 Linux 磁盘 I/O 调度器,覆盖 mq-deadline、kyber、bfq 三种主流多队列调度器的对比与切换实战。读完你会得到三件产出:按介质(NVMe / SATA SSD / HDD)划分的调度器选型矩阵;运行时与持久化两种切换方法;针对延迟抖动、长尾请求、混合读写的排查清单。

一、Linux I/O 调度器从单队列到多队列

Linux 5.0 起内核默认走多队列块层(blk-mq),传统的 cfq、deadline、noop 单队列调度器已被彻底移除。当前主线只剩三种调度器:mq-deadline 是 deadline 的多队列版本,强调读延迟上限;kyber 是 Facebook 贡献的低延迟方案,专为高 IOPS(每秒读写次数)的 NVMe 优化;bfq(Budget Fair Queueing)则在交互负载与公平性上做了大量工作。还有一个特殊选项 none,意思是不做软件层调度,把所有决策交给硬件,多见于高端 NVMe。

如何查看当前调度器?cat /proc/sys/block/sda/queue/scheduler 会列出可选与当前激活项,中括号包起来的就是激活的。为什么内核不能自动选最优?因为最优解依赖工作负载特征,纯顺序读、随机写、混合读写各有合适的调度器,内核只能给出基于介质类型的合理默认值,剩下的得靠运维实测。云服务器(基于虚拟化的弹性计算资源)使用的虚拟磁盘往往是 virtio-blk 或 virtio-scsi,调度器仍生效,相关选型可参考 云服务器选购指南

二、mq-deadline:通用基线的稳妥之选

mq-deadline 给每个 I/O 请求设一个最晚完成时间(读 500ms、写 5s),到点必须调度出去,避免被合并队列里的其他请求长期阻塞。它的核心优点是延迟可预测,最坏情况下的尾延迟比 kyber 与 bfq 更稳定。适用场景:

  • MySQL、PostgreSQL 这类对读延迟敏感的 OLTP 数据库(OnLine Transaction Processing)
  • 文件服务器与小文件大量并发的场景
  • SATA SSD 与 NVMe 上读写混合且要求公平
  • 单租户裸金属环境,不需要按进程做精细配额

切换方法:临时改 echo mq-deadline > /proc/sys/block/sda/queue/scheduler,持久化用 udev 规则。相关磁盘性能与备份策略可参考 美国 VPS 部署教程

三、kyber:NVMe 高 IOPS 场景的优选

kyber 的设计目标是把 95 分位读延迟与写延迟都压到设定的目标值之下(默认读 2ms、写 10ms),通过动态调节队列深度实现。它在高 IOPS 的 NVMe 上能持续把延迟收敛到目标线,但在 HDD 与低端 SATA SSD 上反而可能表现不如 mq-deadline。

echo kyber > /sys/block/nvme0n1/queue/scheduler
echo 1000000 > /sys/block/nvme0n1/queue/iosched/read_lat_nsec
echo 5000000 > /sys/block/nvme0n1/queue/iosched/write_lat_nsec

iosched 子目录下的两个参数允许你按业务调整目标延迟。注意 kyber 不适合长尾敏感场景,它优化的是 95 分位而非 99.9 分位,对低尾延迟苛刻的实时业务建议先用 mq-deadline 验证基线再决定。

四、bfq:桌面与多租户的公平优先

bfq 在每个进程/cgroup 上维护独立预算,按权重分配 I/O 带宽(Bandwidth,即每秒读写数据量,MB/s)。这套机制让交互式负载(IDE 编译、桌面浏览)即便在大文件传输并行时也不卡顿,是多用户共享存储的好选择。代价是 CPU 开销比 mq-deadline 高 5%-10%,纯顺序高吞吐场景下会出现轻微吞吐损失。

容器环境下 bfq 与 blkio cgroup(control group,资源隔离机制)能很好地配合:你可以给 prod 容器分 80% 权重、给 batch 容器分 20%,实测起来比单纯靠 IOPS 上限做隔离要平滑。CDN(Content Delivery Network,内容分发网络)回源磁盘上是否切 bfq 取决于源站是单服务还是多服务共用,相关边缘缓存策略可继续看 W3 Total Cache 调优实战WordPress 香港主机选购指南

五、按介质给出的选型矩阵

下表是一份简化的实战建议矩阵,仅供起点参考:

  • 企业级 NVMe(DC SSD):none 或 kyber,硬件队列深度足够时 none 反而最快
  • 消费级 NVMe / SATA SSD:mq-deadline 是稳妥基线,遇到尾延迟问题再切 kyber
  • 传统 HDD:mq-deadline,开启写合并;bfq 适合桌面但生产服务器一般不推荐
  • 虚拟化磁盘(virtio):mq-deadline,由 host 端再次调度

落地策略是先用 fio 跑一组基准(随机读、顺序写、混合读写各 60 秒),再切换调度器跑同样基准,对比 IOPS、带宽、平均延迟、99 分位延迟四项。任何一项回退超过 5% 都建议保留原调度器,调度器不是越新越好。

六、运行时切换与持久化

运行时切换非常轻量,I/O 队列会被短暂排空再重建:

echo <调度器> > /sys/block/<dev>/queue/scheduler 即可。持久化推荐用 udev 规则,避免 GRUB 命令行硬编码导致重装时丢失。规则示例放在 /etc/udev/rules.d/60-ioschedulers.rules

ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="kyber"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline"

加载方式:udevadm control --reload && udevadm trigger。验证用 cat /sys/block/*/queue/scheduler 走一遍。容器场景下宿主机的调度器对所有容器共用磁盘的 I/O 都有效,容器内无法改宿主机调度器。更多服务器侧的运维议题可继续阅读 WordPress 分类下的相关文章

调优过程中要记一份变更日志:变更日期、原值、新值、回退命令、压测对比指标。日志放到 Git 仓库或内部 Confluence 都行,关键是每次切换都能追溯。这点在多人协作的运维团队里尤其重要,避免出问题时互相甩锅又找不到上次改了什么。

七、运维侧的注意事项

调度器切换是即时生效的,但效果未必立刻可见。建议每次切完后让业务跑满一个流量峰值周期(一般 24 小时)再收集指标对比。短期看可能因为缓存命中率变化、内核统计计数器重置等原因出现误判。生产环境中遇到的常见误区还有:把所有磁盘统一切到同一种调度器,没有按介质区分;只看吞吐量却忽略尾延迟;调度器切完不持久化,重启就回滚到默认。这些都是过去几年里反复遇到的坑。

另一个容易忽视的细节是 nr_requests 参数,它控制单个队列里 pending 请求的最大数量。默认 256 对高吞吐 NVMe 偏小,可以提到 1024 或 2048;HDD 则保持默认即可。写入路径上还有 dirty_ratio、dirty_background_ratio 影响 page cache 刷盘节奏,这两个 sysctl 与 I/O 调度器配合使用收益更大。

总结与建议

I/O 调度器没有银弹,是介质特性与业务负载共同决定的局部最优解。建议你先拿 fio 跑基线、再按本文矩阵选起点、最后用 udev 持久化;任何切换都要带一组对照实验数据,避免凭直觉调参。如果你需要在多台机器上批量管理调度器,可以考虑用 Ansible 把 udev 规则与基准脚本一起下发,建立标准化的 I/O 调优流程。总结一下,稳妥的路径是 mq-deadline 兜底、kyber 与 bfq 按场景上、none 留给真正高端的硬件,定期回看基准数据并对比业务延迟指标才能持续受益。

发表评论