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

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优

上一篇我们聊了怎么用 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 复现,最后改调度器验证。别一上来就改,一步一步验证。