blk-mq 提供 CPU 或节点本地的软件暂存队列,以及硬件派发队列。请求可以在派发前经过调度和合并,但驱动与设备行为也会影响实际服务。软件提交顺序不保证完成顺序。调度器能够影响竞争请求进入设备的方式,却不控制整个存储栈后续的每一项决定。分析时应分别记录提交、派发与完成,避免从一条队列箭头推出设备执行的完整时间线。
在旋转介质上,改善请求局部性可以减少磁头移动。闪存没有机械寻道成本,但合并、并行度、排队和仲裁仍会影响性能。高效排序与公平服务回答不同问题,都不能给出普遍适用的调度器排名。应先确认当前内核与设备实际提供哪些选项,再测量相关负载,不要因为设备名称包含 NVMe 就把所有队列政策的成本与收益都当成零。
磁头才是预算
7200 RPM 下半圈约为 4.17 ms,可作为理想化的平均旋转等待。传输与寻道成本还依赖设备和负载。原文给出 2000 个请求、67907411 与 99977 条磁道,以及 91 与 231 IOPS,但没有提供生成夹具或保留测量。这些数字只能作为未经核验的原文声称,不能写成已经测得的 Linux 调度器结果,更不能用倍数直接推算生产设备的容量。
对一组虚构磁道位置排序,可以说明磁头行程有可能缩短,但没有经过验证的设备模型或测量,就不能建立 IOPS 收益。此前的寻道百分比和约 40 ms 的 NVMe 总耗时,也有同样的证据缺口。图中更短的路径因此只表示定性关系,不是延迟预测。实际比较应保留请求序列、模型假设、设备信息与原始输出,才能判断差异是否来自调度。
每种调度器是干什么的
none 不增加调度器重排,但不承诺全局先到先完成。mq-deadline 结合扇区顺序和批次之间的到期检查,fifo_batch 限制每批数量;这些政策影响派发,并不保证端到端完成期限。kyber 通过限流尝试达到读取与同步写入的目标延迟,目标值不是每个请求都必然达标的证明。设备处理时间与后续排队仍然存在,不能把参数名当成业务延迟服务承诺。
bfq 提供按权重的服务分配和低延迟启发式,带宽或时间分配取决于配置与条件。空转可以帮助服务保证,却也可能降低内部有队列设备的吞吐,必须核对内核支持和实际设置。/sys/block/
公平与排序是两个问题
90/10 的请求发出比例,不能直接建立服务份额或平均等待位置。原文的 0.099、1901 与 0.5 没有保留夹具作为依据。在持续有积压的简化模型里,相等权重可以说明分享概念,但真实结果还取决于需求、请求大小、优先级和调度设置。应分别比较收到的服务、应用延迟与请求到达数量,不能把不同分母下的比例当成同一个指标,更不能保证轻客户端永远取得一半吞吐。
无论哪种介质,都应在代表性竞争下测试延迟、吞吐和隔离。cgroup io.max 在达到配置的 BPS 或 IOPS 上限时延迟输入输出,允许短暂突发,并不要求先选 BFQ。权重控制则依赖启用的实现与配置;BFQ 使用 io.bfq.weight,而 io.weight 可以由 I/O 成本控制器实现。上限与相对份额是不同控制,不应把二者都描述成只有某个调度器重排之后才有效。
坏掉的样子
旋转设备使用 none 值得结合负载检查,但本身不证明存在缺陷。高利用率、低 IOPS 和高 await 可以有多种原因;await 包含排队和服务时间,接近百分之百的利用率对串行设备可能表示饱和,对并行设备则不充分反映性能极限。应先确认设备拓扑、请求模式和竞争工作,再归因于寻道。rotational 标志不是一次磁头跟踪,几个平均数也不能单独证明应该切换策略。
批量任务可能恶化交互请求的长尾,即使平均值看起来正常。调度政策或控制组上限可能有帮助,但任何一个名称都不能单独证明应用级保护成立。应在受控变化前后比较分位数与有效吞吐。空转、队列深度和设备内部调度会相互影响,旋转标志本身无法选择普遍正确的配置。平均队列长度还受并发和服务时间影响,不能把它上升直接解释成请求没有被合并成一次扫描。
快速验证
首先用下面尚未执行的只读步骤观察一个明确选定的整盘设备。设置 DEVICE 前应核对拓扑与范围,不能根据名称随便选择生产设备。代码拒绝类似路径的名称,检查必需属性是否可读,并说明可选参数缺失;这第一个代码块不改变调度器。此处没有为切换策略或修改其他内核设置提供授权,也没有把已存在的运行限制当成静态修改后自动解除。
(
: "${DEVICE:?Set DEVICE to the reviewed whole-device name}"
case "$DEVICE" in
''|[!A-Za-z]*|*[!A-Za-z0-9_-]*|ALL) printf 'Invalid device name\n' >&2; exit 2 ;;
esac
queue="/sys/block/$DEVICE/queue"
[ -d "$queue" ] || { printf 'No such whole-device queue\n' >&2; exit 1; }
for field in rotational scheduler; do
[ -r "$queue/$field" ] || { printf 'Missing: %s\n' "$field" >&2; exit 1; }
value=$(cat "$queue/$field") || exit 1
printf '%s=%s\n' "$field" "$value"
done
for field in read_expire fifo_batch; do
if [ -r "$queue/iosched/$field" ]; then
value=$(cat "$queue/iosched/$field") || exit 1
printf '%s=%s\n' "$field" "$value"
else
printf '%s: unavailable for the active scheduler\n' "$field"
fi
done
LC_ALL=C iostat -x -y -t "$DEVICE" 2 5
)
带方括号的名称是当前选项,缺少 iosched 参数可能正常。-y 排除开机统计,要保留错误并确认输出确实包含所选设备。路径检查不能替代拓扑审阅。
将辅助程序保存为 scheduler_exercise.py。它只适用于明确授权、由你独占控制的可丢弃测试设备。根据上面的观察设置 EXPECTED_CURRENT 与 REQUESTED,并把 ACK_DEVICE 设成 DISPOSABLE: 后接 DEVICE。需要 Python 3.9 以上、调度器写入权限和 sysstat;此步骤会临时改变策略,不能在生产或共享设备上照抄:
"""Unexecuted proposal: temporary policy change on an exclusively controlled test device."""
import json
import re
import signal
import subprocess
import sys
from pathlib import Path
def device_name(value):
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]*", value) or value == "ALL":
raise ValueError("use one named whole device, not a path, option, number or ALL")
return value
def scheduler_state(path):
tokens = path.read_text().split()
active = [s[1:-1] for s in tokens if re.fullmatch(r"\[[A-Za-z0-9_-]+\]", s)]
if len(active) != 1 or not tokens:
raise ValueError("scheduler state must have exactly one bracketed choice")
if any(not re.fullmatch(r"(?:[A-Za-z0-9_-]+|\[[A-Za-z0-9_-]+\])", s) for s in tokens):
raise ValueError("unexpected scheduler state")
available = [s.strip("[]") for s in tokens]
if len(set(available)) != len(available):
raise ValueError("duplicate scheduler choices")
return active[0], available
def exercise(path, device, expected, requested, run=subprocess.run, emit=print):
original, available = scheduler_state(path)
if original != expected or requested not in available or requested == original:
raise ValueError("current/requested scheduler does not match the reviewed plan")
emit(json.dumps({"device": device, "original": original, "requested": requested}), flush=True)
primary_error = restore_error = None
attempted = False
try:
if scheduler_state(path) != (original, available):
raise ValueError("scheduler state changed before the test")
attempted = True # even a failed write may have changed state
path.write_text(requested + "\n")
if scheduler_state(path)[0] != requested:
raise RuntimeError("requested scheduler was not activated")
result = run(["iostat", "-x", "-y", "-t", device, "2", "5"], check=False)
if result.returncode != 0:
raise RuntimeError(f"iostat exited with status {result.returncode}")
except BaseException as error:
primary_error = f"{type(error).__name__}: {error}"
finally:
if attempted:
try:
if original not in scheduler_state(path)[1]:
raise RuntimeError("original scheduler is no longer available")
path.write_text(original + "\n")
if scheduler_state(path)[0] != original:
raise RuntimeError("original scheduler was not restored")
except BaseException as error:
restore_error = f"{type(error).__name__}: {error}"
emit(json.dumps({"primary_error": primary_error, "restore_error": restore_error,
"restored": attempted and restore_error is None}), flush=True)
# Report both errors; restoration failure never hides the primary failure.
return (1 if primary_error else 0) + (2 if restore_error else 0)
def main(args):
if len(args) != 4:
raise ValueError("usage: scheduler_exercise.py DEVICE EXPECTED_CURRENT REQUESTED DISPOSABLE:DEVICE")
device, expected, requested, acknowledgement = args
device = device_name(device)
if acknowledgement != "DISPOSABLE:" + device:
raise ValueError("explicit disposable-device acknowledgement required")
path = Path("/sys/block") / device / "queue/scheduler"
return exercise(path, device, expected, requested)
if __name__ == "__main__":
def interrupted(number, frame):
raise InterruptedError(f"received signal {number}; attempting restoration")
signal.signal(signal.SIGINT, interrupted)
signal.signal(signal.SIGTERM, interrupted)
try:
sys.exit(main(sys.argv[1:]))
except (OSError, ValueError) as error:
sys.exit(f"Scheduler test refused: {error}")
python3 scheduler_exercise.py "${DEVICE:?}" "${EXPECTED_CURRENT:?}" \
"${REQUESTED:?}" "${ACK_DEVICE:?}"
程序记录原值、验证切换、采集间隔,再单独恢复并验证原值。退出状态 0 表示成功,1 表示主流程失败,2 表示恢复失败,3 表示二者都失败;JSON 分别保留两类错误,不能用恢复失败掩盖原错误,也不能忽略恢复错误。SIGINT/SIGTERM 会触发清理尝试,但 SIGKILL、重复中断或主机故障仍可能阻止恢复,应保留原值以便人工处理。这是可逆策略练习,不是性能或公平性证据;历史数值与实际运行仍待核验。
来源
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。
Linux deadline tunables,修订依据,已核验 2026-10-18。
Linux cgroup v2 I/O controls,修订依据,已核验 2026-10-18。
iostat manual,修订依据,已核验 2026-10-18。
sysstat upstream iostat manual,-y 略过开机统计,-t 标记间隔报告时间,已核验 2026-10-19。
Python signals,身份或受控操作的适用契约,已核验 2026-10-19。
sysstat iostat option parser,身份或受控操作的适用契约,已核验 2026-10-19。