blk-mq 给块层准备了两级队列:每个 CPU 一个软件暂存队列,每条设备队列一个硬件派发队列,而 I/O 调度器就夹在两者之间。一个请求会走最短路径——直接进硬件队列——除非该设备挂了调度器,或者这个请求还能被合并;这两种情况下它要先在软件队列里停留。调度器是整个栈里唯一由内核决定“请求以什么顺序到达设备”的地方。
排序在机械盘上价值极大,在闪存上几乎为零,但调度器还有第二件活,而且这件事在 NVMe 时代依然存在:决定谁先等、谁后等。这两件事的方向并不一致——为磁头效率而重排根本不关心公平,而公平本身要付出寻道的代价——所以当前内核里的四种调度器不是拿来排名的,而是拿来匹配设备与负载的。
磁头才是预算
在 7200 RPM 的盘上,平均每个请求的旋转开销是 4.17 ms,传输一个 4 KiB 块只要 0.026 ms,所以几乎全部服务时间都是寻道加旋转。fixture 重放了 2000 个随机 4 KiB 读、覆盖 100000 条磁道:按到达顺序派发时磁头走过 67907411 条磁道、整轮耗时 21954 ms,约 91 IOPS;按磁道升序派发时只走 99977 条、耗时 8645 ms,约 231 IOPS。同样的请求、同样的设备,磁头行程少了 679 倍,吞吐涨了 2.5 倍。
拆开看才说明问题:寻道在到达顺序那一轮占服务时间的 62%,在排序那一轮只占 3%,而旋转与传输两边都固定在 4.19 ms 每个请求。NVMe 上同一批请求总共只要约 40 ms,且完全没有寻道,所以排序在那里一分钱也买不到。
每种调度器是干什么的
none 就是先到先服务:在内部队列很深且没有竞争的设备上它是对的,在机械盘上是错的,因为寻道那部分开销原封不动。mq-deadline 按扇区排序,并加上分方向的超时——读的超时远早于写,每批的数量由 fifo_batch 限住——于是 none 那种无上界的尾部延迟被收进了边界。kyber 直接盯延迟:哪个队列的请求没达到读或写的目标延迟(read_lat_nsec、write_lat_nsec),它就把那个队列限流,所以一个批量写入方单靠自己无法把交互读推过目标线。
bfq 是公平那一侧的答案:一个支持 cgroup 的比例分配调度器,按权重把设备吞吐——而不只是设备时间——分给进程和分组。它默认偏向低延迟而非高吞吐,并且会让设备空转,这在机械盘上有帮助,在深队列闪存上则可能赔掉吞吐。用 cat /sys/block/<dev>/queue/scheduler 看当前调度器,列表里带方括号的就是生效的那一个;用 echo bfq > /sys/block/sda/queue/scheduler 切换,并靠 udev 规则让它在重启后保持,因为队列属性是每块设备重新创建的。
公平与排序是两个问题
当批量写入方与轻量交互客户端按 90/10 的比例发请求时,先到先服务给轻客户端分到 0.099 的服务量,平均排队位置在 1901 个请求之后——它不是被饿死,而是被淹死。换成分组公平排队,无论混合比例如何它都拿到 0.5 的服务量,代价由那个写入方承担。磁头位置一点没变,变的只是仲裁。
所以在闪存上通常选 bfq 做隔离、选 mq-deadline 要尾部有界,只有单一负载独占设备时才用 none。在机械盘上,排序本身才是更大的收益。用 cgroup v2 的说法,io.weight 给一个组一个份额,io.max 给它一个上限;这两者只有在下面的调度器愿意执行顺序时才有意义。
坏掉的样子
最经典的是机械盘跑在 [none] 上,通常因为主机是从 NVMe 镜像克隆来的,或者 udev 规则匹配了 sd* 而设备其实是 nvme0n1。特征是 %util 顶在 100% 附近、r/s 很低、await 到几十毫秒——设备是忙,不是饱和,而且忙在寻道上。用 cat /sys/block/<dev>/queue/rotational 确认:值为 1 却没有重排调度器,就是那个 bug。
第二种是平均值健康、延迟却崩了。同一个设备上的备份任务或 dd 把交互读饿住,p99 爆掉而均值几乎不动。none 和 mq-deadline 都护不住交互客户端,bfq 或 cgroup 的 io.max 可以。在闪存上还要盯 slice_idle:在没有磁头可保的设备上,为了让单个进程保持顺序而空转,纯粹是白赔吞吐。另外,rotational 为 1 的盘如果只跑 none,iostat -x 里的 aqu-sz(平均队列深度)也会明显偏高,因为请求在队列里等磁头,而不是被合并成一次扫描。
快速验证
看清设备是什么,以及它正跑着哪个调度器:
for d in /sys/block/sd* /sys/block/nvme*n*; do echo "$d rot=$(cat $d/queue/rotational) sched=$(cat $d/queue/scheduler)"; done
cat /sys/block/sda/queue/iosched/read_expire /sys/block/sda/queue/iosched/fifo_batch
iostat -x 2 5
echo mq-deadline > /sys/block/sda/queue/scheduler
第一条把每块设备的旋转标志与当前调度器配对打印,方括号里是生效的那个;rot=1 配 [none] 就是失败特征,rot=0 配 [none] 则正常。第二条给出 mq-deadline 的超时与批量参数。iostat -x 用来区分“饱和”和“只是忙”:%util 高、r/s 低、await 高,说明在排队和寻道,而不是设备到了极限。
来源
- Switching Scheduler,调度器名称、sysfs 路径与切换方法,已核验 2026-09-16。
- Multi-Queue Block IO Queueing Mechanism (blk-mq),软件暂存队列与硬件派发队列的关系,已核验 2026-09-16。
- BFQ (Budget Fair Queueing),比例分配行为、cgroup 支持与空转的取舍,已核验 2026-09-16。
- Kyber I/O scheduler tunables,Kyber 据以限流的读写目标延迟,已核验 2026-09-16。