4、I/O 调度器调优:CFQ、Deadline、NOOP、BFQ 调度器原理与选择
说到文件系统调优,I/O 调度器是个绕不开的坎儿。很多人觉得它就是个排队算法,随便选一个就行。我刚开始做嵌入式的时候也这么想,直到有一次在某个工业控制板上,因为调度器选错了,导致数据采集周期性卡顿,排查了整整两天。
说白了,I/O 调度器就是内核在块设备层的一个中间人。它决定上层发下来的 I/O 请求,以什么顺序、什么时机交给底层驱动去处理。选对了,系统吞吐量和响应时间都能上一个台阶;选错了,CPU 空转、磁盘忙死,用户体验直接崩掉。
核心观点:没有最好的调度器,只有最适合当前负载的调度器。嵌入式场景下,硬件资源有限,选型更要谨慎。
4.1 四种主流调度器原理
Linux 内核目前主流的 I/O 调度器有四个:CFQ、Deadline、NOOP 和 BFQ。我一个个说,每个都带点实际项目里的体会。
4.1.1 CFQ(完全公平队列)
CFQ 是 Linux 内核用了很多年的默认调度器。它的核心思想是「公平」——每个进程都有自己的 I/O 队列,调度器按时间片轮转,保证每个进程都能分到差不多的 I/O 带宽。
听起来很美好对吧?但实际用起来,我遇到过一个问题:在某个多进程并发写入的嵌入式设备上,CFQ 的公平性反而成了瓶颈。因为每个进程都要等自己的时间片,导致整体吞吐量上不去,尤其是 SSD 这种低延迟设备,CFQ 的调度开销反而拖了后腿。
- 优点:公平性好,适合多进程混合负载,能防止某个进程饿死。
- 缺点:调度开销大,不适合高速设备(如 NVMe SSD),延迟不稳定。
- 适用场景:传统 HDD、多用户服务器、桌面系统。
4.1.2 Deadline(截止时间调度器)
Deadline 的设计目标很明确——保证每个 I/O 请求在「截止时间」内得到响应。它维护了两个队列:一个按扇区排序(提高吞吐量),一个按过期时间排序(保证延迟)。
我个人习惯在实时性要求高的嵌入式系统上用 Deadline。比如某个摄像头数据采集项目,要求每 10ms 内必须完成一次写入,CFQ 偶尔会超时,换成 Deadline 后,延迟抖动明显减小了。
- 优点:读写延迟可控,不会出现长时间饥饿,适合混合读写场景。
- 缺点:对纯顺序读写场景优化不如 CFQ 的某些变种。
- 适用场景:数据库、实时系统、嵌入式控制设备。
4.1.3 NOOP(空操作调度器)
NOOP 说白了就是个 FIFO 队列,它几乎不做任何调度操作,只做简单的合并。请求来了,直接扔给驱动层。
你可能会问:「那它有什么用?」嗯,这里要注意——NOOP 最适合那些底层硬件自己会做调度的情况。比如 NVMe SSD,它内部已经有复杂的命令队列和调度逻辑,内核再插一手反而多余。我在一个基于 NVMe 的嵌入式存储节点上测试过,NOOP 比 CFQ 吞吐量高了将近 15%。
- 优点:零调度开销,CPU 占用最低,适合自调度硬件。
- 缺点:没有任何 QoS 保证,不适合 HDD 或共享存储。
- 适用场景:NVMe SSD、高性能计算节点、虚拟化宿主机。
4.1.4 BFQ(预算公平队列)
BFQ 是 CFQ 的进化版,它引入了「预算」的概念。每个进程不仅分到时间片,还分到 I/O 预算(可以理解为一次能发多少个请求)。BFQ 的目标是提供更细粒度的公平性,尤其是针对交互式应用。
我记得有个项目是做 Android 平板,用户反馈刷微博时滑动卡顿。排查下来发现是后台下载任务抢了 I/O 带宽。换成 BFQ 后,前台应用的响应明显变快了。BFQ 对延迟敏感型进程有特殊的优先级提升机制,这一点在桌面和移动设备上很管用。
- 优点:交互式体验好,延迟低,公平性比 CFQ 更精细。
- 缺点:调度算法复杂,CPU 开销比 CFQ 还高。
- 适用场景:桌面系统、Android 设备、多媒体播放器。
| 调度器 | 核心思想 | CPU 开销 | 延迟控制 | 推荐硬件 |
|---|---|---|---|---|
| CFQ | 时间片轮转公平 | 高 | 一般 | HDD |
| Deadline | 截止时间保证 | 中 | 好 | HDD / 混合负载 |
| NOOP | FIFO 零调度 | 低 | 无 | NVMe SSD |
| BFQ | 预算公平 | 高 | 优秀 | 桌面 / 移动设备 |
我的经验:在嵌入式 Linux 上,如果拿不准选哪个,先试试 Deadline。它在大多数场景下表现都不差,而且延迟可控。等跑完压力测试再根据瓶颈调整。
4.2 如何查看和修改当前调度器
查看当前块设备的调度器很简单,直接读 sysfs 文件就行:
# 查看所有可用的调度器,方括号里的是当前使用的
cat /sys/block/sda/queue/scheduler
# 输出示例:[mq-deadline] none
# 注意:blk-mq 下调度器名字可能带 mq- 前缀
修改调度器也只需要 echo 一下:
# 临时修改为 deadline
echo deadline > /sys/block/sda/queue/scheduler
# 永久修改(以 Ubuntu 为例,修改 grub 参数)
# 在 GRUB_CMDLINE_LINUX 中添加 elevator=deadline
# 然后 update-grub 重启
注意:修改调度器后最好用 fio 或 iostat 验证一下效果。我曾经在某个项目里直接改了调度器没验证,结果写入延迟反而变高了,白白浪费了半天时间。
4.3 ionice 命令使用
ionice 是 Linux 提供的 I/O 优先级控制工具。它允许你给进程设置 I/O 调度优先级,让重要的进程优先获得 I/O 带宽。
它的原理其实很简单——ionice 会修改进程的 I/O 优先级,然后 CFQ 或 BFQ 这类调度器会根据这个优先级来分配 I/O 资源。Deadline 和 NOOP 对 ionice 的支持有限,这一点要注意。
ionice 有三种调度类:
- Idle (3):只有系统空闲时才允许 I/O,适合后台备份、日志清理。
- Best-effort (2):默认类,可以设置 0-7 的优先级(0 最高)。
- Real-time (1):最高优先级,会抢占其他进程的 I/O,慎用。
# 查看进程的 I/O 优先级
ionice -p 1234
# 设置进程为 best-effort,优先级 0(最高)
ionice -c 2 -n 0 -p 1234
# 启动一个新命令,设置 idle 类
ionice -c 3 tar czf backup.tar.gz /data
我曾经在一个视频监控项目里用过 ionice。录像进程需要持续写入,但偶尔会有日志压缩任务抢带宽。我把录像进程设为 best-effort 0,日志压缩设为 idle,问题就解决了。效果立竿见影。
小技巧:ionice 配合 cgroups 使用效果更好。cgroups 可以限制整个控制组的 I/O 带宽,而 ionice 控制单个进程的优先级,两者互补。
4.4 blk-mq 多队列调度
blk-mq(Block Multi-Queue)是 Linux 内核从 3.13 开始引入的新块层架构。它的核心思想是把单队列拆成多个队列,每个 CPU 核心一个软件队列,然后硬件层面也可以有多个硬件队列。
为什么要这么做?因为传统单队列在高速 SSD 上成了瓶颈。你想想看,所有 CPU 都要抢同一个锁来提交 I/O 请求,并发一高,锁竞争就把性能吃掉了。blk-mq 把队列分散到各个 CPU 上,每个 CPU 操作自己的队列,几乎不需要锁。
blk-mq 下的调度器名字会带 mq- 前缀,比如:
- mq-deadline:Deadline 的多队列版本,推荐用于大多数场景。
- none:相当于 NOOP,适合 NVMe SSD。
- bfq-mq:BFQ 的多队列版本,但 CPU 开销更高。
# 查看 blk-mq 下的调度器
cat /sys/block/nvme0n1/queue/scheduler
# 输出示例:[none] mq-deadline
我建议在较新的内核(4.0+)上,如果硬件支持,优先使用 blk-mq。它带来的性能提升在高速设备上非常明显。我在一个 8 核 ARM 平台上测试过,blk-mq 配合 mq-deadline,随机读写 IOPS 比传统单队列高了将近 30%。
避坑指南:blk-mq 在老旧硬件或某些特殊驱动上可能有兼容性问题。我曾经在一块老款 SATA 控制器上启用 blk-mq,结果系统频繁报 I/O 超时。所以升级前一定要在目标硬件上充分测试。
4.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到从应用层到硬件层,调度器处在什么位置,以及不同调度器的适用路径。
从这张图可以看得很清楚:应用层通过 ionice 设置优先级,经过 VFS 和文件系统层后,到达 I/O 调度器。调度器根据选型不同,采用不同的排队策略,然后交给 blk-mq 多队列层(如果内核支持),最终下发到硬件设备。
选型路径其实不复杂:先看硬件类型,再看负载特征,最后用 fio 压测验证。别光看理论,跑一遍数据比什么都管用。