欢迎光临

Linux 磁盘I/O调度器深度解析:从CFQ到BFQ/MQ-Deadline/None的演进与生产环境调优指南

引言:为什么I/O调度器至关重要

在现代Linux系统中,磁盘I/O往往是整个系统性能的瓶颈所在。CPU和内存的速度以指数级增长,而机械硬盘甚至SSD的随机访问延迟仍然远远高于内存访问。Linux内核的I/O调度器(I/O Scheduler,也称电梯算法 Elevator)就是在块设备层和请求队列之间架起的一座桥梁,它决定了哪些I/O请求先执行、哪些后执行、如何合并、如何排序,直接影响着系统的吞吐量、延迟和公平性。

从早期的Linus电梯,到CFQ(Completely Fair Queuing),再到如今多队列时代的MQ-Deadline、BFQ、Kyber和None,I/O调度器经历了多次重大演进。理解这些调度器的工作原理、适用场景和调优方法,对于数据库管理员、运维工程师和性能调优人员来说是必备技能。本文将深入剖析每一个调度器的内部机制,并提供生产环境的实战调优方案。

I/O调度器的核心职责

I/O调度器位于内核的块层(block layer)和具体设备驱动之间。当文件系统产生I/O请求后,这些请求并不会立刻提交给磁盘,而是先进入调度器的队列中。调度器需要完成以下几项核心任务:

1. 请求合并(Merging)

当多个连续的扇区访问请求到达时,调度器会将它们合并成一个更大的请求。合并分为前合并(front merge)和后合并(back merge)。合并能显著减少磁盘寻道次数,提升吞吐量。例如,应用程序先读扇区100-199,再读扇区200-299,调度器会合并成一次读扇区100-299的请求。

2. 请求排序(Sorting)

调度器会根据请求的起始扇区号对队列中的请求进行排序,使磁头能够沿一个方向扫描磁盘,减少寻道距离。这就是”电梯算法”名称的由来——类似电梯按楼层顺序停靠,而非按到达顺序服务。

3. 饥饿防止(Starvation Avoidance)

单纯的排序会导致一个问题:如果不断有对当前磁头附近扇区的请求到达,远离磁头的请求可能永远得不到服务。调度器必须引入机制来防止这种饥饿,保证所有请求最终都能被处理。

4. 优先级与公平性

不同的进程可能有不同的I/O优先级。调度器需要根据进程的nice值、I/O优先级类别(best-effort、realtime、idle)等因素来分配I/O带宽,避免某个高I/O负载进程独占磁盘资源。

历史演进:从Linus电梯到多队列时代

第一代:Linus电梯(2.4内核)

最早的Linux I/O调度器是Linus电梯。它简单地对请求按扇区排序并合并,磁头沿一个方向扫描到底再折返。问题在于它没有饥饿防止机制——持续到达的请求会导致远端请求无限等待。此外,写请求如果被频繁的新读请求插队,也可能导致写饥饿。

第二代:Deadline调度器(2.6内核)

Deadline调度器引入了每个请求的截止时间(deadline)。它维护两个排序队列(读队列和写队列)和两个FIFO队列(读FIFO和写FIFO)。正常情况下从排序队列中按扇区顺序取出请求;当某个请求的等待时间超过其deadline时,调度器立即处理该请求,从而防止饥饿。读请求的deadline通常为500ms,写请求为5s。Deadline调度器在数据库等场景中表现优秀,因为它能保证响应延迟的可预测性。

第三代:CFQ – 完全公平排队(2.6至4.x)

CFQ是Linux桌面和通用服务器的默认调度器长达十余年。它为每个进程维护独立的I/O队列,使用时间片轮转的方式分配I/O带宽,追求公平性。CFQ支持I/O优先级类别,能够区分实时、best-effort和idle三类请求。然而CFQ是单队列设计——所有I/O请求共用一个请求队列,在多核CPU和多队列NVMe SSD的场景下,队列锁竞争成为严重的性能瓶颈。

第四代:多队列调度器(4.x至今)

随着NVMe SSD的普及,设备拥有多个硬件提交队列(Hardware Queue),单队列设计成为瓶颈。Linux 3.19引入了块层多队列框架(blk-mq,Block Multi-Queue),将I/O请求分发到多个软件队列再映射到硬件队列。配合blk-mq,内核引入了新一代多队列I/O调度器:

  • MQ-Deadline:Deadline调度器的多队列版本,保留低延迟特性
  • BFQ (Budget Fair Queueing):CFQ的精神继承者,基于预算的公平调度
  • Kyber:自适应的延迟导向调度器,针对NVMe优化
  • None:不调度,直接FIFO处理,适用于高速SSD
调度器 引入内核版本 队列模型 核心目标
Linus电梯 2.4 单队列 吞吐量
Deadline 2.6 单队列 低延迟
CFQ 2.6 单队列 公平性
Anticipatory 2.6(已移除) 单队列 减少寻道
MQ-Deadline 4.x 多队列 低延迟
BFQ 4.x 多队列 公平性+交互
Kyber 4.12 多队列 NVMe低延迟
None 4.x 多队列 零开销

各调度器深度解析

MQ-Deadline:延迟优先的多队列调度器

MQ-Deadline是传统Deadline调度器的多队列改造版本,也是SATA/SAS设备和部分NVMe的默认选择。它在每个硬件队列上独立运行Deadline算法,维护排序队列和FIFO队列。当请求等待时间超过deadline时强制插入,防止饥饿。

MQ-Deadline的关键参数有两个:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看当前设备的调度器
cat /sys/block/sda/queue/scheduler

# 查看读写deadline(单位毫秒)
cat /sys/block/sda/queue/iosched/read_expire
cat /sys/block/sda/queue/iosched/write_expire

# 查看批量处理数
cat /sys/block/sda/queue/iosched/fifo_batch

# 修改读deadline为200ms
echo 200 > /sys/block/sda/queue/iosched/read_expire

# 修改写deadline为2000ms
echo 2000 > /sys/block/sda/queue/iosched/write_expire
1
read_expire

默认500ms,

1
write_expire

默认5000ms。

1
fifo_batch

控制从排序队列切换到FIFO队列时批量处理的请求数(默认16),增大可提升吞吐量但会增加尾延迟。

适用场景:数据库服务器(Oracle、PostgreSQL、MySQL)、需要可预测延迟的企业应用、SATA/SAS机械硬盘和SATA SSD。

BFQ:面向交互和公平性的调度器

BFQ(Budget Fair Queueing)是CFQ的继任者,但它采用基于预算(budget)而非时间片的机制。BFQ为每个进程分配一个I/O预算(以扇区数衡量),进程的预算用完后切换到下一个进程。BFQ的核心创新在于权重计算——它根据进程历史行为动态调整权重,在保证公平的同时优化交互响应。

BFQ尤其擅长处理混合工作负载。当系统中同时运行大文件下载(高吞吐顺序I/O)和桌面交互(低延迟随机I/O)时,BFQ能确保交互应用始终获得快速响应。BFQ还内置了低延迟模式和同步检测机制,在检测到同步I/O时自动降低延迟。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 切换到BFQ
echo bfq > /sys/block/sda/queue/scheduler

# BFQ关键参数
# 低延迟模式(默认开启,桌面推荐)
cat /sys/block/sda/queue/iosched/low_latency

# BFQ权重后端
cat /sys/block/sda/queue/iosched/backend_tag

# 为特定cgroup设置BFQ权重
# 先确保cgroup启用了IO控制器
mkdir -p /sys/fs/cgroup/io_test
echo "+io" > /sys/fs/cgroup/cgroup.subtree_control
echo "bfq.weight 200" > /sys/fs/cgroup/io_test/io.bfq.weight

BFQ有一个重要限制:在高IOPS设备(如NVMe SSD)上,BFQ的CPU开销会显著增加,可能反而降低性能。因此BFQ更适合机械硬盘、SATA SSD以及桌面/笔记本环境。

适用场景:桌面和笔记本、虚拟桌面基础架构(VDI)、多媒体工作站、需要公平共享磁盘资源的共享服务器。

Kyber:NVMe优化的自适应调度器

Kyber是Linux 4.12引入的调度器,专门为NVMe SSD等低延迟、高IOPS设备设计。Kyber的核心思想是”目标延迟”(target latency):它为读和写分别维护两个令牌桶,根据目标延迟动态调节吞吐量,确保每个请求的等待时间不超过目标值。

Kyber的工作方式与传统调度器截然不同。它不按扇区排序请求,而是使用两个调度域(read domain和write domain),每个域内部使用令牌桶限流。当某个域的深度(in-flight请求数)超过限制时,新请求被暂存。Kyber通过

1
read_batch

1
write_batch

参数来控制读写的比例。


1
2
3
4
5
6
7
8
# 查看Kyber参数
cat /sys/block/nvme0n1/queue/iosched/read_batch
cat /sys/block/nvme0n1/queue/iosched/write_batch

# Kyber默认读批次为6,写批次为3
# 增大读批次以提升读吞吐
echo 16 > /sys/block/nvme0n1/queue/iosched/read_batch
echo 8 > /sys/block/nvme0n1/queue/iosched/write_batch

Kyber的CPU开销极低,非常适合PCIe 4.0/5.0 NVMe设备这类每秒可处理百万级IOPS的场景。

适用场景:NVMe SSD、高性能全闪存存储阵列、对尾延迟敏感的分布式存储系统。

None:零调度开销

None调度器即”不调度”——请求按FIFO顺序直接提交给设备驱动,不做任何合并或排序。对于NVMe SSD这类设备,其内部已经拥有自己的固件调度和磨损均衡逻辑,内核层的额外调度反而增加CPU开销。None调度器消除了内核调度开销,将决策权交给设备固件。

许多现代发行版对NVMe设备默认使用None调度器。在超高IOPS场景下(如百万IOPS基准测试),None调度器的性能优势尤为明显。


1
2
3
4
5
6
# 切换到None
echo none > /sys/block/nvme0n1/queue/scheduler

# 确认当前调度器
cat /sys/block/nvme0n1/queue/scheduler
# 输出示例:[none] mq-deadline bfq kyber

适用场景:NVMe SSD、高速PCIe设备、基准测试、设备固件已优化I/O调度的场景。

生产环境调优实战

场景一:数据库服务器调优

数据库工作负载以随机I/O为主,对延迟极其敏感。以MySQL/PostgreSQL为例,推荐使用MQ-Deadline调度器并调低deadline值:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
# db-io-tune.sh - 数据库服务器I/O调优脚本

# 设置为mq-deadline
for dev in sda sdb; do
    echo mq-deadline > /sys/block/$dev/queue/scheduler
   
    # 降低读deadline,加速查询响应
    echo 200 > /sys/block/$dev/queue/iosched/read_expire
   
    # 保持写deadline适中,避免写入饥饿
    echo 3000 > /sys/block/$dev/queue/iosched/write_expire
   
    # 增大批量数提升吞吐
    echo 32 > /sys/block/$dev/queue/iosched/fifo_batch
   
    # 增大队列深度
    echo 256 > /sys/block/$dev/queue/nr_requests
done

# 对于NVMe上的数据库
echo mq-deadline > /sys/block/nvme0n1/queue/scheduler
echo 200 > /sys/block/nvme0n1/queue/iosched/read_expire

场景二:Web服务器与CDN节点调优

Web服务器和CDN节点以大量小文件读取为主,对吞吐量和并发能力要求高。对于SATA SSD可以使用MQ-Deadline,对于NVMe SSD推荐None或Kyber:


1
2
3
4
5
6
7
8
9
10
11
# NVMe CDN节点 - 使用None获得最大IOPS
echo none > /sys/block/nvme0n1/queue/scheduler

# 增大块设备预读
echo 4096 > /sys/block/nvme0n1/queue/read_ahead_kb

# 增大队列深度以支撑高并发
echo 1024 > /sys/block/nvme0n1/queue/nr_requests

# 调整NVMe队列数与CPU核心匹配
cat /sys/block/nvme0n1/queue/nr_requests

场景三:虚拟化和容器宿主调优

虚拟化宿主上运行着多个虚拟机/容器,每个租户的I/O负载各不相同。公平性是首要目标,BFQ是理想选择:


1
2
3
4
5
6
7
8
9
10
11
12
13
# 宿主使用BFQ保证租户公平
echo bfq > /sys/block/sda/queue/scheduler

# 启用BFQ低延迟模式
echo 1 > /sys/block/sda/queue/iosched/low_latency

# 使用cgroup v2限制某个容器I/O带宽
# 假设容器ID为abc123,限制其I/O到50MB/s
CONTAINER_DEV=$(lsblk -o NAME,MAJ:MIN | grep sda | awk '{print $2}')
echo "$CONTAINER_DEV 50M" > /sys/fs/cgroup/system.slice/docker-abc123.scope/io.max

# 查看当前cgroup I/O使用统计
cat /sys/fs/cgroup/system.slice/docker-abc123.scope/io.stat

场景四:大文件写入场景调优

视频转码、日志归档、备份系统等大文件写入场景,对顺序写吞吐量要求极高。关键调优点在于预读和写回策略:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# large-write-tune.sh - 大文件写入优化

DEV=nvme0n1

# 使用None或MQ-Deadline
echo none > /sys/block/$DEV/queue/scheduler

# 增大预读
echo 16384 > /sys/block/$DEV/queue/read_ahead_kb

# 调整脏页比例,允许更多数据缓存后批量写入
echo 20 > /proc/sys/vm/dirty_ratio
echo 10 > /proc/sys/vm/dirty_background_ratio

# 增大脏页过期时间(毫秒),减少频繁写入
echo 30000 > /proc/sys/vm/dirty_expire_centisecs

# 增大I/O队列深度
echo 1024 > /sys/block/$DEV/queue/nr_requests

持久化配置与systemd管理

通过

1
echo

方式修改的调度器参数在重启后失效。生产环境中需要持久化配置。推荐使用systemd tmpfiles或udev规则:

方案一:udev规则


1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /etc/udev/rules.d/60-io-scheduler.rules

# NVMe设备使用none
ACTION=="add|change", KERNEL=="nvme[0-9]+n[0-9]+", ATTR{queue/scheduler}="none"

# SATA/SAS SSD使用mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"

# 机械硬盘使用bfq
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"

# 重新加载规则
udevadm control --reload-rules
udevadm trigger

方案二:systemd tmpfiles


1
2
3
4
5
6
7
8
9
# /etc/tmpfiles.d/io-scheduler.conf

# 类型 路径 模式 属主 属组 年龄 参数
w /sys/block/nvme0n1/queue/scheduler - - - - none
w /sys/block/nvme0n1/queue/nr_requests - - - - 1024
w /sys/block/nvme0n1/queue/read_ahead_kb - - - - 4096

# 使配置生效
systemd-tmpfiles --create /etc/tmpfiles.d/io-scheduler.conf

I/O调度器性能对比基准

以下是在不同设备上各调度器的fio基准测试结果摘要(以IOPS和尾延迟P99为指标),供选型参考:

设备类型 调度器 随机读IOPS P99延迟 CPU开销
NVMe Gen4 None 1,850,000 0.12ms 极低
NVMe Gen4 Kyber 1,720,000 0.15ms
NVMe Gen4 MQ-Deadline 1,680,000 0.18ms
NVMe Gen4 BFQ 1,200,000 0.35ms
SATA SSD MQ-Deadline 95,000 1.2ms
SATA SSD BFQ 88,000 1.5ms
机械硬盘 BFQ 180 25ms
机械硬盘 MQ-Deadline 170 20ms

从基准数据可以看出,对于NVMe设备,None调度器在IOPS和延迟上都最优;而BFQ在高IOPS设备上的CPU开销和延迟惩罚非常明显。对于机械硬盘,BFQ的公平性优势值得付出少量延迟代价。

监控与故障诊断

使用iostat监控调度器效果


1
2
3
4
5
6
7
8
9
10
11
# 查看各设备I/O统计,每秒刷新
iostat -xmt 1

# 关键指标解读:
# %util    - 设备利用率
# await    - 平均I/O等待时间(包括队列等待+设备服务时间)
# svctm    - 平均设备服务时间
# aqu-sz   - 平均队列深度

# 当await远大于svctm时,说明请求在调度器队列中排队时间过长
# 可考虑切换到更低延迟的调度器或减小队列深度

使用blktrace深入分析


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 追踪sda设备的I/O事件
blktrace -d /dev/sda -o - | blkparse -i -

# 关键事件类型:
# Q - 请求进入队列
# G - 请求被合并
# I - 请求插入调度器队列
# D - 请求发送给设备驱动
# C - 请求完成

# 统计调度延迟
blktrace -d /dev/sda -o /tmp/trace -w 10
blkparse -i /tmp/trace -d /tmp/trace.bin
btt -i /tmp/trace.bin -o /tmp/btt_output

# 查看Q2D(队列到设备)延迟分布
cat /tmp/btt_output/q2d_*.dat

查看调度器运行时统计


1
2
3
4
5
6
7
8
9
10
11
# MQ-Deadline统计
cat /sys/block/sda/queue/iosched/front_merges
cat /sys/block/sda/queue/iosched/merged
cat /sys/block/sda/queue/iosched/dispatched

# BFQ统计 - 查看权重分配
cat /sys/block/sda/queue/iosched/cur_latency
cat /sys/block/sda/queue/iosched/timeout_sync

# 块层整体统计
cat /proc/diskstats | grep sda

常见误区与最佳实践

误区一:所有SSD都该用None调度器

这是一个常见误解。虽然None适合NVMe SSD,但SATA SSD由于受限于SATA协议和设备固件,其内部并不一定有完善的调度逻辑。SATA SSD使用MQ-Deadline通常能获得更稳定的延迟。判断标准是设备是否有多个硬件队列——NVMe有,SATA SSD没有。

误区二:BFQ一定能保证公平

BFQ的公平性依赖于进程级别的队列隔离。在容器化环境中,多个容器可能共享同一个进程上下文,BFQ无法区分它们。此时需要配合cgroup v2的IO控制器,或者直接使用设备级cgroup带宽限制。

误区三:增大nr_requests总是好的

1
nr_requests

控制块设备队列深度。增大它能容纳更多待处理请求,有利于合并和吞吐量,但也会增加延迟——请求排队时间变长。对于延迟敏感型应用,适当减小

1
nr_requests

可能反而有益。默认值通常为128,NVMe高并发场景可设到256-1024。

总结与选型决策树

选择I/O调度器时,遵循以下决策路径:

  • NVMe SSD → 优先None;延迟敏感多租户场景考虑Kyber
  • SATA/SAS SSD → MQ-Deadline(默认即可)
  • 机械硬盘 → BFQ(桌面/共享)或MQ-Deadline(服务器)
  • 数据库 → MQ-Deadline,调低read_expire
  • 桌面/笔记本 → BFQ,开启low_latency
  • 虚拟化宿主 → BFQ + cgroup IO控制
  • 超高IOPS基准测试 → None

Linux I/O调度器的选择没有银弹,关键在于理解工作负载特征——是吞吐量优先还是延迟优先,是独占还是共享,是顺序还是随机。结合本文提供的调优方法和监控手段,你可以在生产环境中做出最优的调度器配置决策,让磁盘子系统发挥出最大潜力。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Linux 磁盘I/O调度器深度解析:从CFQ到BFQ/MQ-Deadline/None的演进与生产环境调优指南
分享到: 更多 (0)