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 毫秒),调度器会暂时放弃排序队列的寻道优化,优先服务该即将过期的请求。
4.2 读写分离机制
Deadline 调度器最显著的特征之一是 严格的读写分离。它分别为读请求和写请求维护独立的排序队列和 FIFO 队列,共四个核心队列:读排序队列、读 FIFO 队列、写排序队列、写 FIFO 队列。
这种分离设计的根本原因在于:
- 延迟敏感性差异:应用程序通常对读延迟更为敏感(例如,用户等待页面加载),而写操作可以通过 Page Cache 回写机制进行缓冲,对延迟容忍度更高。
- 防止写操作淹没读操作:在没有分离机制的情况下,大量的写请求会迅速填满请求队列,导致读请求被长时间阻塞,造成严重的读饥饿。
调度器在决定下一次分发哪个请求时,会优先检查读 FIFO 队列。只有当读 FIFO 队列为空,或者读请求的截止时间远未到期时,才会考虑处理写请求。这种策略确保了读请求的优先级始终高于写请求。
4.3 避免饥饿策略:请求分发逻辑
Deadline 调度器通过一套精巧的 状态机 来避免请求饥饿。其核心分发逻辑可以概括为以下步骤:
- 检查读 FIFO 队列:如果读 FIFO 队列不为空,且队首请求的截止时间即将到期(< 5ms),则立即分发该读请求。这是最高优先级操作。
- 检查写 FIFO 队列:如果读 FIFO 队列为空,且写 FIFO 队列不为空,且队首写请求的截止时间即将到期,则分发该写请求。
- 默认分发(排序队列):如果没有即将过期的请求,调度器从排序队列中选择一个请求进行分发,以优化寻道时间。此时,调度器会优先选择读排序队列中的请求,除非读队列为空。
- 写饥饿补偿:为了防止写请求被无限期推迟,Deadline 调度器引入了一个 写饥饿计数器。当连续分发了一定数量的读请求(默认是 2 个)后,调度器会强制分发一个写请求,即使该写请求的截止时间尚未到期。
以下 SVG 图展示了 Deadline 调度器的完整请求分发流程:
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 开销。