4. 传统IO调度器(中):Deadline调度器原理、读写分离机制、避免饥饿策略

Deadline 调度器是 Linux 内核中一个重要的传统 IO 调度器,它通过引入 请求截止时间 和 读写分离 机制,在保证整体吞吐量的同时,显著改善了 IO 请求的延迟公平性,有效避免了请求饥饿问题。与 CFQ 调度器侧重于进程间的 IO 带宽公平分配不同,Deadline 调度器的核心设计目标是 保证每个 IO 请求在可预期的最大延迟内得到服务。

4.1 Deadline 调度器的核心设计思想

Deadline 调度器为每个 IO 请求分配一个 截止时间(deadline)。默认情况下,读请求的截止时间为 500 毫秒,写请求的截止时间为 5 秒。调度器维护两个核心队列:排序队列(Sorted Queue) 和 FIFO 队列(FIFO Queue)。

  • 排序队列:按照请求的起始扇区号进行排序,用于实现磁盘寻道优化,最大化吞吐量。
  • FIFO 队列:按照请求的截止时间进行排序,用于保证请求的延迟上限,防止饥饿。

调度器在每次分发请求时,会检查 FIFO 队列头部的请求是否即将过期。如果某个请求的剩余时间低于一个阈值(通常为 5 毫秒),调度器会暂时放弃排序队列的寻道优化,优先服务该即将过期的请求。

关键设计权衡: Deadline 调度器在“最大化吞吐量(排序队列)”和“保证延迟上限(FIFO 队列)”之间做出了明确的权衡。它允许一定程度的寻道优化损失,以换取更平滑、更可预测的 IO 延迟。

4.2 读写分离机制

Deadline 调度器最显著的特征之一是 严格的读写分离。它分别为读请求和写请求维护独立的排序队列和 FIFO 队列,共四个核心队列:读排序队列、读 FIFO 队列、写排序队列、写 FIFO 队列。

这种分离设计的根本原因在于:

  • 延迟敏感性差异:应用程序通常对读延迟更为敏感(例如,用户等待页面加载),而写操作可以通过 Page Cache 回写机制进行缓冲,对延迟容忍度更高。
  • 防止写操作淹没读操作:在没有分离机制的情况下,大量的写请求会迅速填满请求队列,导致读请求被长时间阻塞,造成严重的读饥饿。

调度器在决定下一次分发哪个请求时,会优先检查读 FIFO 队列。只有当读 FIFO 队列为空,或者读请求的截止时间远未到期时,才会考虑处理写请求。这种策略确保了读请求的优先级始终高于写请求。

实践启示: 在数据库、Web 服务器等读密集型应用中,Deadline 调度器的读写分离机制能够显著降低读操作的尾延迟(Tail Latency),提升用户体验。默认的 500ms 读截止时间和 5s 写截止时间是一个通用的平衡点,但在特定场景下可以通过内核参数进行调整。

4.3 避免饥饿策略:请求分发逻辑

Deadline 调度器通过一套精巧的 状态机 来避免请求饥饿。其核心分发逻辑可以概括为以下步骤:

  1. 检查读 FIFO 队列:如果读 FIFO 队列不为空,且队首请求的截止时间即将到期(< 5ms),则立即分发该读请求。这是最高优先级操作。
  2. 检查写 FIFO 队列:如果读 FIFO 队列为空,且写 FIFO 队列不为空,且队首写请求的截止时间即将到期,则分发该写请求。
  3. 默认分发(排序队列):如果没有即将过期的请求,调度器从排序队列中选择一个请求进行分发,以优化寻道时间。此时,调度器会优先选择读排序队列中的请求,除非读队列为空。
  4. 写饥饿补偿:为了防止写请求被无限期推迟,Deadline 调度器引入了一个 写饥饿计数器。当连续分发了一定数量的读请求(默认是 2 个)后,调度器会强制分发一个写请求,即使该写请求的截止时间尚未到期。

以下 SVG 图展示了 Deadline 调度器的完整请求分发流程:

Deadline 调度器请求分发流程图 Deadline 调度器请求分发流程 IO 请求到达 读 FIFO 队首即将过期? (剩余时间 < 5ms) 是 分发读请求 否 写 FIFO 队首即将过期? (剩余时间 < 5ms) 是 分发写请求 否 写饥饿计数器触发? (连续分发 2 个读请求后) 是 强制分发写请求 否 从排序队列分发(寻道优化) 说明: 读请求优先级最高,写请求通过截止时间和饥饿计数器获得服务机会。 排序队列用于在无紧急请求时最大化磁盘吞吐量。

4.4 关键参数与调优

Deadline 调度器提供了几个关键的可调参数,用于控制其行为。这些参数可以通过 sysfs 接口进行修改。

参数名 默认值 描述 调优建议
read_expire 500 (ms) 读请求的截止时间。 对于延迟敏感的数据库,可降低至 200-300ms。
write_expire 5000 (ms) 写请求的截止时间。 对于写密集型日志系统,可适当降低至 2000ms。
fifo_batch 16 从 FIFO 队列中连续分发的最大请求数。 增大此值可提高吞吐量,但可能增加延迟抖动。
writes_starved 2 写饥饿计数器阈值。在分发多少个读请求后强制分发一个写请求。 增大此值会进一步偏向读性能,但可能导致写延迟增加。
front_merges 1 (开启) 是否允许前向合并(新请求可以合并到现有请求之前)。 通常保持默认,关闭可能减少 CPU 开销但降低合并效率。
调优警告: 修改 read_expire 和 write_expire 参数时,应避免设置过小的值(例如 < 100ms),否则调度器会频繁触发 FIFO 分发,导致排序队列的寻道优化完全失效,整体吞吐量急剧下降。调优应始终在真实负载下进行 A/B 测试。

4.5 查看与修改 Deadline 调度器参数

以下命令展示了如何查看当前块设备的调度器及其参数,并进行动态修改。

# 1. 查看当前调度器
cat /sys/block/sda/queue/scheduler

# 2. 切换到 Deadline 调度器
echo deadline > /sys/block/sda/queue/scheduler

# 3. 查看 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
cat /sys/block/sda/queue/iosched/writes_starved

# 4. 动态修改参数(例如,将读截止时间改为 300ms)
echo 300 > /sys/block/sda/queue/iosched/read_expire

# 5. 验证修改
cat /sys/block/sda/queue/iosched/read_expire

4.6 Deadline 调度器的适用场景

Deadline 调度器在以下场景中表现优异:

  • 数据库服务器:对读延迟有严格要求的 OLTP 系统。
  • 虚拟化主机:多虚拟机共享同一块物理磁盘时,Deadline 的公平性机制能防止单个虚拟机饿死其他虚拟机。
  • 实时或近实时系统:需要 IO 延迟可预测性的场景。
  • 混合读写负载:读写比例不均衡,且读操作优先级较高的环境。

然而,在纯顺序读写或纯随机读写且无延迟要求的批量处理场景中,Deadline 调度器可能不如 NOOP 或 Kyber 调度器高效,因为它引入的 FIFO 检查逻辑会带来额外的 CPU 开销。