1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com

上一篇我们聊了怎么用 iostat、iotop、blktrace 把存储慢的问题找出来。今天聊下一步:找出来是盘慢了之后,怎么让系统更聪明地排队,让 I/O 更顺。

这块的核心就是 I/O 调度器。很多新手以为调度器就是一个开关,其实它就是 Linux 内核帮你决定“哪个请求先发给盘”的那套策略。选错了,SSD 的优势完全发挥不出来;选对了,同样的硬件,延迟能差好几倍。

先搞清楚你的盘是什么性格

现在服务器里大多是 SSD,已经基本把机械硬盘取代了。两者的差异直接决定了调度思路。

机械硬盘 HDD 的特点
有磁头寻道、有转盘旋转延迟。请求是放在磁道上的位置,顺序访问比随机访问快很多。所以老调度器会拼命做合并和重排,让请求尽量从头到尾走一遍,减少磁头移动。

固态硬盘 SSD 的特点
没有机械部件,访问时间在整个盘上是均匀的。寻道时间几乎为零,所以传统的“让请求排成顺序”的优化对 SSD 意义不大。SSD 的痛点反而不是顺序,反而是写放大和寿命。

SSD 调优的重点是:

  • 减少写放大,别让盘重复写
  • 确保存储单元寿命
  • 优化速度

对应的实操常识也很直白:

  • SSD 不需要预读和后端写缓存,缓存一般配成直写模式
  • 日志功能在 SSD 上要谨慎,因为日志会造成不必要的双重写入,增加磨损
  • 传统盘的碎片整理、磁道优化对 SSD 基本没用

所以从 HDD 时代过来的那套调优思路,到 SSD 这边要换脑子了。

多队列时代来了,blk-mq 是基础

现代Linux用的块 I/O 机制是 blk-mq,多队列调度。这套机制把 I/O 请求映射到不同的队列和执行线程,每个 CPU 核心都能独立发请求,减少锁竞争。

NVMe 之所以快,很大程度上就是它天生就支持多个硬件提交和完成队列。Linux 的多队列调度正好能把这个能力发挥出来。

怎么看当前盘在用哪个调度器?

1
2
[root@localhost ~]# cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq

方括号里的就是当前激活的。没激活的就是可选项。

常用的四个调度器,分别适合谁

内核会根据设备类型自动选一个默认,多数情况下不用动。但想调优,你得知道它们的区别。

none 调度器:极简 FIFO

none 就是先进先出,配合块层的末级合并。没有复杂的排序逻辑。

适合场景:

  • 高性能 SSD,特别是 NVMe
  • CPU 密集型系统,存储很快,调度本身不要带来额外开销
  • 虚拟机里,宿主机 HBA 驱动支持多队列时,客户机也建议用 none

现代Linux对 NVMe 的默认调度器就是 none,官方建议除非有明确理由,尽量别改。

mq-deadline:延迟优先的稳健派

mq-deadline 会给每个请求一个截止时间,尽量保证延迟。它把排队的请求分成读批次和写批次,然后按逻辑块地址递增顺序调度。

默认读批次优先于写批次,因为应用更怕读卡住。处理完一个批次后,它会检查写入操作被饿死的时长,必要时切到写。

这是 现代Linux里最通用、最稳的一个。特别是异步写入为主的场景,以及虚拟机,官方很推荐。

bfq:桌面交互友好

bfq 全称 Budget Fair Queue,目标是桌面系统和交互式任务。它给每个进程分配扇区数的预算,而不是固定时间片。这样一个应用不会把带宽全占住,系统依然响应迅速。

默认配置里 bfq 更追求最低延迟而不是最大吞吐。基于原有的 cfq 演进而来。

适合:

  • 复制大文件时避免系统卡死
  • 传统的 SCSI 机械硬盘
  • 需要人机交互流畅的场景

kyber:自调优的延迟目标派

kyber 会自己算每个 I/O 请求的延迟,并尝试达到你设定的目标延迟。可以单独配置读请求和同步写请求的延迟目标。

适合速度很快的设备,比如 NVMe、高速 SSD,以及其他低延迟设备。它更像是一个“会自己调”的调度器。

怎么改调度器?临时改和永久改不一样

先临时试试水

想临时改一下看效果,直接写文件就行,不用重启,但重启后会丢。

1
2
3
[root@localhost ~]# echo bfq > /sys/block/nvme0n1/queue/scheduler
[root@localhost ~]# cat /sys/block/nvme0n1/queue/scheduler
none mq-deadline kyber [bfq]

你会看到 [bfq] 被激活。

注意rhel8 起 elevator 内核参数就废弃了,rhel9 以上完全无效。所以别再去改内核参数了。

调度器还有可调参数

每个调度器下都有自己的 sysfs 可调项:

1
2
3
[root@localhost ~]# ls /sys/block/nvme0n1/queue/iosched
back_seek_max fifo_expire_async low_latency slice_idle strict_guarantees
back_seek_penalty fifo_expire_sync max_budget slice_idle_us timeout_sync

比如 mq-deadline 会有:

1
2
async_depth front_merges read_expire writes_starved
fifo_batch prio_aging_expire write_expire

fifo_batch 控制每批最大请求数,值越小延迟越低,值越大吞吐越高。这种参数是跟调度器绑定的,换调度器参数就变了。

永久生效的两种正规做法

1. 用 tuned 配置文件,最推荐

调优服务提供了预定义配置,比如 latency-performance 和 throughput-performance。你也可以自己建一个。

最小化系统可能没安装tuned

1
2
3
[root@localhost ~]# dnf install tuned -y
[root@localhost ~]# systemctl enable tuned --now

建目录:

1
[root@localhost ~]# mkdir /etc/tuned/profiles/my-profile

写 tuned.conf,例子是给 nvme0n1 设 none:

1
2
3
4
5
6
7
8
9
cat > /etc/tuned/profiles/my-profile/tuned.conf <<-EOF
[main]
include=virtual-guest
summary=Setting the Disk Scheduler Using TuneD

[disk]
devices=nvme0n1
elevator=none
EOF

重启一下后,列出就有了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@localhost ~]# systemctl restart tuned

[root@localhost ~]# tuned-adm list
Available profiles:
- accelerator-performance - Throughput performance based tuning with disabled higher latency STOP states
- aws - Optimize for aws ec2 instances
- balanced - General non-specialized tuned profile
- balanced-battery - Balanced profile biased towards power savings changes for battery
- desktop - Optimize for the desktop use-case
- hpc-compute - Optimize for HPC compute workloads
- intel-sst - Configure for Intel Speed Select Base Frequency
- latency-performance - Optimize for deterministic performance at the cost of increased power consumption
- my-profile - Setting the Disk Scheduler Using TuneD
- network-latency - Optimize for deterministic performance at the cost of increased power consumption, focused on low latency network performance
- network-throughput - Optimize for streaming network throughput, generally only necessary on older CPUs or 40G+ networks
- optimize-serial-console - Optimize for serial console use.
- powersave - Optimize for low power consumption
- throughput-performance - Broadly applicable tuning that provides excellent performance across a variety of common server workloads
- virtual-guest - Optimize for running inside a virtual guest
- virtual-host - Optimize for running KVM guests
Current active profile: virtual-guest

设置一下看看

1
2
3
4
5
[root@localhost ~]# cat /sys/block/nvme0n1/queue/scheduler
none mq-deadline kyber [bfq]
[root@localhost ~]# tuned-adm profile my-profile
[root@localhost ~]# cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq

或者用 sysfs 插件调细参数:

1
2
3
4
5
[main]
include=latency-performance

[sysfs]
/sys/block/nvme0n1/queue/iosched/fifo_batch=1

就能看到生效了。

2. 用 udev 规则,适合按磁盘唯一标识绑定

先查磁盘的唯一序列号:

1
2
3
4
[root@localhost ~]# udevadm info --query=property --name=/dev/nvme0n1 | grep -E '(WWN|SERIAL)'
ID_SERIAL_SHORT=VMware NVME_0000
ID_WWN=eui.d37dabc7cdd4c1a3000c29667605c408
ID_SERIAL=VMware_Virtual_NVMe_Disk_VMware_NVME_0000_1

如果磁盘设备名可能发生变化,可以考虑使用 WWN(World Wide Name)标识磁盘,避免规则只依赖 nvme0n1 这样的设备名。

1
2
3
cat > /etc/udev/rules.d/99-scheduler.rules <<'EOF'
ACTION=="add|change", SUBSYSTEM=="block", ENV{ID_WWN}=="eui.d37dabc7cdd4c1a3000c29667605c408", ATTR{queue/scheduler}="mq-deadline"
EOF

验证一下语法对不对

1
2
3
4
5
[root@localhost ~]# udevadm verify /etc/udev/rules.d/99-scheduler.rules

1 udev rules files have been checked.
Success: 1
Fail: 0

重载并触发:

1
2
3
4
5
[root@localhost ~]# udevadm control --reload-rules
[root@localhost ~]# udevadm trigger --type=devices --action=change
[root@localhost ~]# cat /sys/block/nvme0n1/queue/scheduler
none [mq-deadline] kyber bfq

这样每次盘被识别都会自动套上调度器,重启也保留。

改完怎么验证?fio 就是你的模拟器

调完调度器不能靠感觉,得用压力测试验证。fio 就是为这个生的。

安装:

1
[root@localhost ~]# dnf install fio -y

一个典型的随机写测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[root@localhost ~]# fio --name=randwrite --ioengine=libaio --iodepth=1 \
--rw=randwrite --bs=4k --direct=1 --size=512M --numjobs=2 \
--group_reporting --filename=/tmp/testfile
randwrite: (g=0): rw=randwrite, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=1
...
fio-3.36
Starting 2 processes
randwrite: Laying out IO file (1 file / 512MiB)
Jobs: 1 (f=1): [_(1),w(1)][100.0%][w=70.5MiB/s][w=18.0k IOPS][eta 00m:00s]
randwrite: (groupid=0, jobs=2): err= 0: pid=27635: Mon Oct 12 16:51:56 2026
write: IOPS=17.6k, BW=68.7MiB/s (72.1MB/s)(1024MiB/14900msec); 0 zone resets
slat (usec): min=4, max=1138, avg=21.42, stdev=17.68
clat (nsec): min=790, max=1364.6k, avg=89825.76, stdev=47994.49
lat (usec): min=37, max=1770, avg=111.24, stdev=51.99
clat percentiles (usec):
| 1.00th=[ 38], 5.00th=[ 47], 10.00th=[ 52], 20.00th=[ 61],
| 30.00th=[ 68], 40.00th=[ 74], 50.00th=[ 81], 60.00th=[ 88],
| 70.00th=[ 97], 80.00th=[ 110], 90.00th=[ 133], 95.00th=[ 163],
| 99.00th=[ 251], 99.50th=[ 314], 99.90th=[ 635], 99.95th=[ 717],
| 99.99th=[ 930]
bw ( KiB/s): min=49904, max=88880, per=100.00%, avg=70794.17, stdev=4712.90, samples=58
iops : min=12476, max=22220, avg=17698.52, stdev=1178.21, samples=58
lat (nsec) : 1000=0.01%
lat (usec) : 2=0.05%, 4=0.01%, 10=0.01%, 20=0.01%, 50=8.45%
lat (usec) : 100=64.49%, 250=25.97%, 500=0.81%, 750=0.16%, 1000=0.03%
lat (msec) : 2=0.01%
cpu : usr=0.16%, sys=17.54%, ctx=312933, majf=0, minf=28
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=0,262144,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=1

Run status group 0 (all jobs):
WRITE: bw=68.7MiB/s (72.1MB/s), 68.7MiB/s-68.7MiB/s (72.1MB/s-72.1MB/s), io=1024MiB (1074MB), run=14900-14900msec

Disk stats (read/write):
dm-0: ios=1/262067, sectors=8/2096536, merge=0/0, ticks=0/21429, in_queue=21429, util=80.26%, aggrios=1/262150, aggsectors=8/2097208, aggrmerge=0/1, aggrticks=1/20052, aggrin_queue=20052, aggrutil=73.53%
nvme0n1: ios=1/262150, sectors=8/2097208, merge=0/1, ticks=1/20052, in_queue=20052, util=73.53%

参数拆解:

  • --filename:测试文件,谨慎用,别指到设备上会把盘覆盖
  • --name / --size:作业名和文件大小
  • --rw:read/write/randread/randwrite/readwrite/randrw,可配混合比例
  • --bs / --bsrange:块大小,4k 是数据库常见页大小
  • --direct=1:绕过页缓存,直接测盘速度。0 的时候测的是内存速度
  • --ioengine:libaio 支持异步,iodepth 控制并发请求数
  • --numjobs:并行线程数
  • --group_reporting:合并多个 jobs 的统计
  • --time_based --runtime=60:跑固定时间,而不是跑到文件写满

跑完会输出 clat 完成延迟、IOPS、BW 带宽。同样的工作负载下,延迟下降、IOPS 上升,就是调优有效。

记住一个原则:基准测试要重复多次,单次波动很大。相同条件下多跑几次看趋势才靠谱。

怎么选?给你一个决策小抄

  • NVMe SSD -> 默认 none,别乱动
  • 普通 SATA SSD,通用业务 -> mq-deadline,稳
  • 虚拟机里 -> mq-deadline 或 none
  • 机械硬盘,交互型负载 -> bfq
  • 高速盘,需要自适应低延迟 -> kyber

先用 iostat 确认瓶颈,再用 fio 复现,最后改调度器验证。别一上来就改,一步一步验证。