高IOPS存储如何验证Linux新版NVMe调度器优化?重点看IOPS、延迟与队列深度
仅看随机读的峰值 IOPS,无法证明 Linux 新版 NVMe 调度器已经完成优化。高 IOPS 存储的验收至少要同时观察 IOPS、平均延迟、P99/P99.9 尾延迟、实际队列深度和 CPU 消耗,并在相同负载、相同 NVMe 固件及相同测试参数下对比 none、mq-deadline 等可用调度器。
如果新版内核在 QD1 和 QD4 下延迟下降、在目标并发下 P99 不超出业务上限,同时 IOPS 没有明显下降,通常比单纯追求高队列深度下的峰值更有价值。相反,QD64 时 IOPS 增长了 5%,但 P99 从 200 微秒升至 800 微秒,那么它可能只适合批处理,不一定适合在线数据库或低延迟服务。
先明确“优化”要验证什么
“新版 Linux”并不是一个固定版本。不同发行版对内核版本、NVMe 默认调度器、io_uring 支持方式和队列参数的处理可能不同。2026 年进行验收时,应记录实际运行的发行版、内核、fio 版本、NVMe 固件和调度器状态,而不能仅凭发行版名称判断结果。
NVMe 设备通常使用 blk-mq 多队列框架。常见调度器包括:
none:不在块层增加复杂的请求排序,更多依赖 NVMe 控制器和硬件队列完成调度。它不是“没有队列”,而是块层调度较少。mq-deadline:为请求设置服务期限,并对读写请求进行一定排序。在混合读写或需要控制读取尾延迟的场景中,可能比none更稳定。kyber:部分内核或发行版可能提供,侧重通过令牌控制不同类型请求的延迟。是否可用必须以当前系统列出的选项为准。bfq:更常见于需要进程间公平性的场景,并不一定适合追求 NVMe 峰值 IOPS 的服务器。
调度器的效果不能脱离业务负载判断。建议把验收目标拆成两个层面:
- 设备能力目标:在指定块大小、读写比例和队列深度下,设备能提供多少 IOPS、吞吐和延迟。
- 业务服务目标:在实际并发量下,P99 或 P99.9 延迟是否满足要求,CPU 是否仍有余量,后台写入是否会拖慢前台读取。
一个实用的测试矩阵如下:
| 测试场景 | 主要观察指标 | 适合回答的问题 |
|---|---|---|
| 4 KiB 随机读,QD1 | 平均延迟、P99、单核 CPU | 单请求响应是否变快 |
| 4 KiB 随机读,QD4~QD64 | IOPS、延迟曲线、CPU | 并发上升后何时进入饱和 |
| 4 KiB 随机混合读写,70/30 | P99、P99.9、读写比例 | 写入是否拖慢读取 |
| 128 KiB 顺序读写 | MB/s、CPU、队列 | 大块传输是否受 PCIe 或设备带宽限制 |
| 持续写入或业务回放 | 温度、尾延迟、稳定 IOPS | 是否存在缓存耗尽或温控降速 |
其中 QD 是测试参数,不是越高越好。在线应用可能长期处于 QD1~QD8,批处理、日志汇聚或并行备份则可能达到 QD32 以上。如果只测一个很高的 QD,结果容易偏向设备峰值,而无法反映实际业务。
IOPS、延迟与队列深度应当一起看
IOPS 和吞吐量
IOPS 表示每秒完成的 I/O 次数,适合描述小块随机访问。吞吐量表示每秒传输的数据量,适合描述顺序读写或大块 I/O。
两者可以用以下关系互相校验:
吞吐量(MB/s,十进制)≈ IOPS × 块大小(字节)÷ 1,000,000
例如,4 KiB 随机 I/O 达到 1,050,000 IOPS:
- 4 KiB = 4 × 1024 = 4096 字节;
- 每秒传输字节数 = 1,050,000 × 4096 = 4,300,800,000 字节;
- 十进制吞吐量 = 4,300,800,000 ÷ 1,000,000 = 4300.8 MB/s。
如果把十进制吞吐量换算为网络常见的 Mbps,需要先乘以 8:
- 4300.8 MB/s × 8 = 34,406.4 Mbps;
- 也就是约 34.4 Gbps。
这个换算可以发现一些异常:如果测试报告声称 4 KiB、100 万 IOPS,却只有 800 MB/s,可能存在统计口径、块大小、压缩缓存或 IOPS 采集范围不一致的问题。
平均延迟和尾延迟
平均延迟只能说明总体水平,不能说明慢请求是否集中发生。在线数据库、虚拟化磁盘和交易服务通常更需要关注:
- P50:一半请求不超过该延迟;
- P95:95%的请求不超过该延迟;
- P99:最慢的 1% 请求之外的边界;
- P99.9:更少量但更慢的长尾请求。
调度器优化经常表现为吞吐量变化不大,但 P99 或 P99.9 有明显变化。例如,none 可能在纯随机读中获得更高峰值 IOPS,而 mq-deadline 在混合读写下通过控制请求顺序降低读请求长尾。此时不能只用 IOPS 排名决定结果。
还要区分测试工具中的延迟和操作系统监控中的延迟。fio 统计的是测试任务的 I/O 完成时间;iostat 的 await 通常是设备层观测到的平均请求等待时间,两者统计路径和聚合方式不同,不应直接认为数值必须相等。
队列深度和并发量
在异步 I/O 中,iodepth 表示每个 fio job 希望维持的在途 I/O 数量。若设置:
numjobs=4
iodepth=16
理论上的请求并发上限约为 4 × 16 = 64,但实际深度还会受到 I/O 引擎、设备队列、文件系统、CPU 调度和应用提交速度影响。
使用简单同步 I/O 引擎时,iodepth 可能无法达到预期值。测试高并发 NVMe 时,应确认 io_uring 或 libaio 是否可用,并通过 fio 输出和系统监控核对实际表现,而不是只看命令行参数。
队列深度与以下参数也不是同一个概念:

- fio 的
iodepth:测试程序主动提交的并发请求数; numjobs:fio 并行任务数;/sys/block/.../queue/nr_requests:块层请求队列相关限制;- NVMe 控制器硬件队列深度:设备自身可以接收和处理的请求数量。
根据近似的排队关系,在稳定状态下:
IOPS ≈ 实际在途 I/O 数 ÷ 平均延迟(秒)
例如,88,000 IOPS、平均延迟 11.4 微秒时:
- 88,000 × 0.0000114 ≈ 1.003;
这与 QD1 基本一致。若测试声称配置了 QD32,但 IOPS 与延迟相乘只有 4~8,说明实际并发可能没有达到设定值,或者统计范围并不一致。
测试前固定系统变量
调度器对结果的影响很容易被其他变量掩盖。至少应记录以下信息:

- Linux 发行版和内核版本;
fio版本及 I/O 引擎;- NVMe 型号、固件、容量和命名空间;
- PCIe 链路代际和通道宽度;
- 文件系统、挂载参数和测试文件位置;
- NUMA 节点、CPU 绑定及后台任务;
- NVMe 温度和是否发生温控降速;
- 测试时的读写比例、块大小、
numjobs和iodepth。
可以先执行以下只读命令收集基础信息:
uname -a
cat /etc/os-release
fio --version
lsblk -o NAME,MODEL,SERIAL,SIZE,TYPE,FSTYPE,MOUNTPOINTS
sudo nvme list
lscpu
numactl --hardware
检查当前 NVMe 分区或命名空间可用的调度器:
for dev in /sys/block/nvme*n1; do
[ -d "$dev" ] || continue
name=$(basename "$dev")
echo "[$name]"
printf "scheduler: "
cat "$dev/queue/scheduler"
printf "nr_requests: "
cat "$dev/queue/nr_requests"
printf "logical_block_size: "
cat "$dev/queue/logical_block_size"
printf "physical_block_size: "
cat "$dev/queue/physical_block_size"
printf "numa_node: "
cat "$dev/device/numa_node" 2>/dev/null || true
done
输出中的方括号表示当前选中的调度器。例如:
scheduler: [none] mq-deadline kyber
这表示当前使用的是 none,系统同时提供 mq-deadline 和 kyber。如果某个调度器没有出现在列表中,不应直接写入该名称。
测试期间可在其他终端观察设备和 CPU:
iostat -xmd 1
mpstat -P ALL 1
pidstat -dru 1
vmstat 1
iostat 中的 %util、await、读写吞吐量可以帮助判断设备是否接近饱和,但 NVMe 多队列设备的 %util 不应单独作为“满载”依据。还要结合 IOPS、延迟、CPU 使用率和队列深度判断。
如果怀疑存在温度因素,可以在每轮测试前后记录 SMART 信息:
sudo nvme smart-log /dev/nvme0
温度持续升高、功耗达到限制后 IOPS 下降,通常属于设备或散热条件变化,不能简单归因于调度器。
用相同负载比较调度器
优先使用独立测试挂载点上的文件进行验证,避免直接覆盖生产命名空间。以下示例假设 /mnt/nvme-bench 是专门用于测试的文件系统,测试文件不会与业务数据混用:
sudo mkdir -p /mnt/nvme-bench/results
sudo fallocate -l 20G /mnt/nvme-bench/fio-testfile
如果目标文件系统不支持 fallocate,应改用该文件系统支持的预分配方式。测试文件大小应覆盖实际业务工作集;若工作集明显大于 20G,就不能把 20G 文件的结果直接当成整盘长期表现。
先测单任务、不同队列深度的 4 KiB 随机读:
for qd in 1 4 8 16 32 64; do
fio \
--name=rr_qd${qd} \
--filename=/mnt/nvme-bench/fio-testfile \
--size=20G \
--rw=randread \
--bs=4k \
--ioengine=io_uring \
--iodepth=${qd} \
--numjobs=1 \
--direct=1 \
--time_based=1 \
--runtime=60 \
--ramp_time=15 \
--group_reporting=1 \
--percentile_list=50:90:95:99:99.9 \
--output-format=json \
--output=/mnt/nvme-bench/results/rr_qd${qd}.json
done
这个测试重点是观察队列深度从 1 增加到 64 时的曲线,而不是只保存一个最高 IOPS 数字。若系统没有可用的 io_uring,先用以下命令检查引擎:
fio --enghelp
然后选择当前系统实际支持的异步引擎,并在所有对照组中保持一致。
混合读写更接近数据库、日志和虚拟化场景。以下示例表示 70% 随机读、30% 随机写:
fio \
--name=mix_70r30w_qd32 \
--filename=/mnt/nvme-bench/fio-testfile \
--size=20G \
--rw=randrw \
--rwmixread=70 \
--bs=4k \
--ioengine=io_uring \
--iodepth=32 \
--numjobs=1 \
--direct=1 \
--time_based=1 \
--runtime=120 \
--ramp_time=30 \
--group_reporting=1 \
--percentile_list=50:90:95:99:99.9 \
--output-format=json \
--output=/mnt/nvme-bench/results/mix_70r30w_qd32.json
混合写入会修改测试文件内容,测试路径必须是专用位置。不要把生产数据库文件、系统根分区或未备份的数据目录作为 filename。如果直接对 /dev/nvmeXnY 做块设备测试,测试可能覆盖文件系统和分区表;只有在独立、已卸载、确认无业务数据的测试命名空间上才适用,并且应先完成备份。测试结束后的回滚方式是停止测试、恢复备份或重新创建测试文件系统,而不是依赖文件删除恢复数据。
切换调度器前,应先确认当前工作负载已停止,并记录原始选项。临时切换只适合测试机或维护窗口:
path=/sys/block/nvme0n1/queue/scheduler
cat "$path"
echo mq-deadline | sudo tee "$path"
cat "$path"
echo none | sudo tee "$path"
cat "$path"
只有当 cat "$path" 的输出包含目标调度器时才可以执行对应命令。恢复时写回测试前记录的原始调度器,例如:
echo none | sudo tee /sys/block/nvme0n1/queue/scheduler
切换前应保留原始值和测试结果,确认该设备没有其他业务使用者。调度器写入 sysfs 通常是运行期修改,不等同于持久化配置;重启后可能恢复发行版默认值。不要在生产高峰期直接切换,也不要为了追求单项分数同时修改 nr_requests、CPU 亲和性和文件系统参数,否则无法判断提升来自哪个变量。
如何阅读一组对照结果
下面是一组用于说明读法的参考数据,不代表某个具体型号的实测结果。测试条件为 4 KiB 随机读、单 job、io_uring、直接 I/O:

| 调度器 | QD | IOPS | 平均延迟 | P99 延迟 | CPU 使用率 |
|---|---|---|---|---|---|
| none | 1 | 88,000 | 11.4 µs | 18 µs | 7% |
| mq-deadline | 1 | 84,000 | 11.9 µs | 20 µs | 8% |
| none | 8 | 510,000 | 15.7 µs | 36 µs | 24% |
| mq-deadline | 8 | 495,000 | 16.2 µs | 43 µs | 26% |
| none | 32 | 1,050,000 | 30.5 µs | 78 µs | 68% |
| mq-deadline | 32 | 1,000,000 | 32.0 µs | 112 µs | 72% |
| none | 64 | 1,100,000 | 58.2 µs | 230 µs | 91% |
| mq-deadline | 64 | 1,070,000 | 59.8 µs | 185 µs | 93% |
从这组数据可以得出几个不同结论:
- QD1~QD32 下,
none的 IOPS 高约 5%,平均延迟也略低; - QD64 下,
mq-deadline的 P99 低于none,但 IOPS 仍略低; - QD64 时 CPU 使用率超过 90%,继续增加队列深度未必能带来有效收益;
- 如果业务要求 P99 不超过 200 微秒,QD64 的
none可能不合格,即使它的 IOPS 更高。
混合读写可能出现相反结果:
| 调度器 | 总 IOPS | 读取 P99 | 写入 P99 | CPU 使用率 |
|---|---|---|---|---|
| none | 760,000 | 480 µs | 620 µs | 74% |
| mq-deadline | 742,000 | 315 µs | 690 µs | 77% |
此时 mq-deadline 的总 IOPS 低约 2.4%,但读取 P99 下降约 34%。如果前台请求是读、后台任务是写,这种结果可能更符合业务要求;如果任务是离线数据导入,吞吐量优先,则 none 可能更合适。
单轮结果不能直接作为验收结论。建议每个场景至少运行三轮,保留预热阶段和完整延迟百分位数据。若同一场景的 IOPS 波动已经达到几个百分点,先处理温度、后台进程、CPU 频率或文件系统缓存等环境因素,再比较调度器。两种方案差异很小时,应看重复测试的波动范围,而不是把 1%~2%的差别包装成确定性优化。
常见结果与原因对应关系
IOPS 增加,但 P99 快速恶化
这通常表示系统进入饱和区。队列加深后,更多请求同时等待,设备虽然完成了更多 I/O,但长尾延迟明显扩大。此时应找到“性能拐点”:在 QD1、QD4、QD8、QD16、QD32、QD64 中,选择满足业务 P99 的最高稳定点,而不是继续追逐峰值。
平均延迟下降,但 P99 没有改善
可能是少量长请求仍然存在,平均值被大量快速请求稀释。应进一步观察 P99.9,并同时查看混合读写、持续写入和温度变化。对于在线业务,P99 通常比平均延迟更适合作为验收指标。
请求的 iodepth 很高,但 IOPS 没有随之增长
可能原因包括:
ioengine实际为同步模式;numjobs太少或应用提交速度不足;- CPU 线程被调度、锁竞争或中断处理限制;
- NVMe 控制器已达到硬件队列或带宽上限;
- 文件系统、加密层或虚拟化层成为瓶颈。
这时要同时查看 fio 输出中的实际延迟和 CPU 使用率。如果 QD32、QD64 的 IOPS 基本持平而延迟持续上升,通常已经超过设备的有效并发区间。
新内核 IOPS 提高,但系统态 CPU 明显升高
这不一定是坏结果,但要看服务器余量。如果新版内核在相同 IOPS 下消耗更多系统态 CPU,业务进程、网络处理或数据库线程可能受到挤压。可以用 mpstat 区分用户态、系统态、I/O 等待和窃取时间,再结合 pidstat 判断是测试进程还是其他任务消耗资源。
如果性能提升只在 CPU 已接近 100% 时出现,生产环境的收益可能不如测试结果明显。对于多租户主机,还要观察其他虚拟机或容器是否受到 CPU 抢占影响。
纯随机读改善,混合读写反而变差
这说明调度器对读写公平性或尾延迟的处理与业务模型不一致。纯随机读容易让设备保持稳定队列,而混合写入会带来闪存内部整理、写放大和缓存压力。应使用实际读写比例、块大小和持续时间复测,不要用纯读结果替代混合负载验收。
设定验收边界,而不是寻找单一排名
如果业务已经给出服务等级目标,可以将其转换为明确的通过条件。例如某在线服务可设定以下示例边界:
- 4 KiB 随机读,QD1:P99 不高于 30 µs;
- 目标并发 QD32:IOPS 不低于 1,000,000;
- 目标并发 QD32:P99 不高于 150 µs;
- 混合 70/30 负载:读取 P99 不高于 400 µs;
- 测试期间 CPU 使用率不高于 80%;
- 连续运行 10 分钟后,性能下降不超过预设比例。
这些数值只是验收模板,实际阈值应来自应用 SLO。若没有明确 SLO,可以使用“稳定拐点 + 资源余量”的方法:选择延迟开始明显上升前的队列深度,并为 CPU、温度和后台任务保留约 20%~30%的余量。
容量判断也不能只看设备标称 IOPS。比如业务预计需要 700,000 次/秒的 4 KiB I/O:
- 700,000 × 4096 = 2,867,200,000 字节/秒;
- 十进制吞吐量约为 2867.2 MB/s;
- 换算为比特速率是 2867.2 × 8 = 22,937.6 Mbps,约 22.94 Gbps。
这只是主机可见的业务数据量,还没有计入写放大、文件系统元数据、复制、校验、加密和后台整理。如果实测设备在满足 P99 约束时只能稳定提供 600,000 IOPS,那么即使厂商峰值标称达到更高数值,也不能把它作为该业务的有效容量。
需要重新复测的条件
以下变化会使旧结果失去可比性,应重新保留基线并运行完整测试矩阵:
- Linux 内核、发行版或
fio版本变化; - NVMe 固件、控制器固件或设备容量变化;
- 调度器、
nr_requests、I/O 引擎或 CPU 绑定变化; - 文件系统、挂载参数或测试文件大小变化;
- PCIe 链路代际、通道宽度或 NUMA 位置变化;
- 读写比例、块大小、业务并发或数据工作集变化;
- 服务器散热条件、机箱风道或持续运行时间变化;
- 同一主机增加数据库、日志、备份或虚拟机负载。
最终的验收记录应至少包含版本信息、调度器、测试命令、每轮 IOPS、平均延迟、P99/P99.9、实际队列深度、CPU 使用率和温度。只有当新版 Linux 或候选调度器在目标业务负载下同时满足延迟边界、有效 IOPS 和资源余量,才能认为优化达到可交付状态;若结果只在高 QD 峰值测试中成立,则应将其限定为批处理或吞吐型场景,不宜直接推广到所有 NVMe 业务。




