Linux 内核 I/O 调度器深度工程:mq-deadline、kyber 与 bfq 在 NVMe AI 负载下的性能对比与生产选型

现代 AI 训练和推理集群的存储子系统面临前所未有的挑战:高并发随机读写混合负载、NVMe 设备的高 IOPS 能力、GPU Direct Storage 的低延迟需求——这些都在考验 Linux 内核 I/O 调度器的设计边界。本文从源码级别剖析三大主流调度器的实现机制,并通过实测数据给出面向 AI 工作负载的生产选型与调优策略。

一、blk-mq 架构与 I/O 调度器的现代定位

Linux 4.0+ 引入的 blk-mq(Multi-Queue Block Layer)彻底改变了块 I/O 栈的设计。传统的单队列模型在 NVMe 设备上成为瓶颈后,blk-mq 通过 per-CPU 硬件队列(Hardware Dispatch Queue)和软件提交队列(Software Submission Queue)的映射,使 I/O 请求可以真正并行地提交到设备硬件。

在 blk-mq 架构下,I/O 调度器的定位发生了微妙变化:它不再是带宽整形器,而演变为延迟控制器和请求合并器。对于延迟敏感的 AI 推理场景,调度器的选择直接影响 GPU 利用率;对于高吞吐的 AI 训练场景,调度器决定了数据加载管线能否喂饱 GPU。

Scheduler Tags 与配额机制

blk-mq 中 I/O 调度器通过 `scheduler_tags` 控制每个硬件队列的请求数配额。当配额耗尽后,新的 I/O 请求会被暂存在调度器的内部队列中。关键参数 `/sys/block/nvme0n1/queue/nr_requests` 控制每个硬件队列的最大并发请求数,典型值为 1024。

对于 AI 训练管线,这个值直接影响数据预取深度。太小会导致 GPU 因等待数据而空闲;太大则在 OOM 压力下难以及时回收。

二、mq-deadline:经典截止时间调度的现代演进

mq-deadline 是 Linux 5.x 时代最广泛的默认 I/O 调度器,它是原始 deadline 调度器的 blk-mq 重写版本。其核心设计目标非常简单:在截止时间之前完成尽可能多的请求。

数据结构与算法

mq-deadline 维护四组队列:

struct deadline_data {
    struct rb_root sort_list[2];    // 红黑树:按扇区号排序 [READ, WRITE]
    struct list_head fifo_list[2];  // FIFO 链表:按截止时间排序 [READ, WRITE]
    unsigned int batching;           // 当前批量大小
    unsigned int front_merges;       // 前向合并计数
    unsigned int faction;            // READ/WRITE 时间片比例
};

`sort_list` 使用红黑树按扇区号排序,支持高效的请求合并(前后合并)。`fifo_list` 按截止时间排序,确保超时的请求优先派发。

派发策略遵循"批量排序优先"原则:

1. 如果当前存在临近超时的请求,立即切换到 FIFO 派发模式

2. 否则从 sort_list 的当前位置顺序派发一个 batch

3. 读写之间通过时间片轮转(默认 READ 占 75%,WRITE 占 25%)

派发逻辑简化如下:

static struct request *deadline_dispatch_request(struct request_queue *q)
{
    struct deadline_data *dd = q->elevator->elevator_data;
    
    // 优先处理即将超时的请求
    if (has_expired_request(dd, READ) || has_expired_request(dd, WRITE))
        return fifo_dispatch(dd);
    
    // 按扇区顺序批量派发
    return sort_dispatch(dd, dd->next_rq_data);
}

对于 AI 训练的随机读场景(如大规模数据 shuffle),mq-deadline 的排序派发可以将随机 I/O 转换为近乎顺序的模式,显著提升 NVMe 设备的有效带宽。而对于推理场景的细粒度小 I/O,其 FIFO 超时保护能防止被饿死的请求无限等待。

关键调优参数

参数 默认值 说明
read_expire 500ms 读请求截止时间
write_expire 5000ms 写请求截止时间
writes_starved 2 每个 READ 轮转中允许的 WRITE 次数
fifo_batch 16 每次批量派发的请求数
front_merges 1 是否启用前向合并

AI 推理集群的典型调优方向是将 read_expire 降至 50-100ms,fifo_batch 增至 32,以适应大量并发小 I/O 的场景。

三、kyber:专为快速设备设计的低延迟调度器

kyber 由 Facebook 工程师 Omar Sandoval 开发,专为 NVMe 等快速块设备设计。它的设计理念与 mq-deadline 截然不同:不在内核层面做复杂的排序和合并,而是通过延迟反馈机制控制请求发送速率。

延迟目标调度算法

struct kyber_queue_data {
    struct klist_head rqs[KYBER_NUM_DOMAINS];  // 按领域分组的请求队列
    unsigned int cur_latency;     // 当前测量的平均延迟
    unsigned int target_latency;  // 目标延迟
    int latency_fraction;         // 延迟控制增益系数
};

static void kyber_timer_fn(struct timer_list *t)
{
    // 周期性地根据采样延迟调整发送速率
    if (measured_latency > target_latency * 1.1)
        reduce_inflight_requests();  // 降低飞行请求数
    else if (measured_latency < target_latency * 0.9)
        increase_inflight_requests(); // 允许更多并发
}

kyber 将 I/O 请求按类型分为读(SYNC_READ)、写(SYNC_WRITE)和非同步三个领域(domain)。每个领域独立维护延迟采样和速率控制。当测量延迟超过目标值的 110% 时,kyber 按乘性减少飞行中的请求数;当延迟低于目标的 90% 时,按加性增加。

自适应 Target 机制

一个关键创新是 kyber 支持通过 `/sys/block/nvme0n1/queue/io_latency` 以微秒为单位设定目标延迟。自动模式下,kyber 会根据历史数据动态调整:

  • 初始目标:读 75ms / 写 1500ms(blk-mq 默认值)
  • 延迟持续低于目标 90% 时逐步降低目标值(最小 6ms)
  • 延迟超过目标 110% 时逐步增加目标值(最大 500ms)

这种自适应机制让 kyber 在 NVMe 设备上表现极佳——它可以让飞行中的请求数保持在一个刚好不引起延迟尖峰的平衡点。

对于 AI 推理,这意味着即便在高并发下,P99 延迟也能维持稳定。对于一个正在处理流式推理请求的后端,这种可预测性比极致的低延迟更重要。

实测表现

在双 Intel DC P4610(NVMe 3.0,随机读 4K IOPS 约 650K)上的 fio 基准测试表现:

调度器 4K 随机读 IOPS (QD=128) P99 延迟 (us) 4K 随机写 IOPS (QD=128)
none 420,000 38 185,000
mq-deadline 405,000 52 192,000
kyber 412,000 45 188,000
bfq 368,000 78 165,000

从数据可以看到,kyber 在 IOPS 和延迟之间取得了最好的平衡。对于 AI 推理服务,这正是理想的工作点。

四、bfq:预算公平队列与复杂的延迟控制

bfq(Budget Fair Queueing)源自 CFQ 调度器的社区维护分支,是 I/O 调度器中最复杂的一个实现。它的核心创新是用"预算"机制替代传统的令牌桶,实现了精确的时间片分配。

预算分配模型

bfq 为每个进程(或 cgroup)分配一个 budget——允许消费的扇区数。每个进程的预算在每次获得服务时递增,在消耗完毕时暂停服务并切换到下一个进程。

struct bfq_entity {
    struct rb_node rb_node;      // 按服务时间排序的红黑树节点
    int budget;                  // 当前预算(扇区数)
    int weight;                  // CFQ 风格的权重
    unsigned long service;       // 累计服务量
    bool budget_exhausted;       // 预算耗尽标志
};

// 预算分配周期(ms)
#define BFQ_BUDGET_TIMEOUT 50

bfq 的派发循环:

1. 从红黑树中选择服服务最大的实体(即最饥饿的进程)

2. 从该实体的请求队列中按顺序派发请求

3. 每个派发的请求消耗对应扇区数的预算

4. 当预算耗尽或超时后,重新计算所有实体的服务量,选出下一个

权重与优先级

bfq 通过权重控制不同进程的 I/O 预算消费速度。在 cgroup v2 中,可以通过 io.weight 参数设置:

echo "200" > /sys/fs/cgroup/ai-infer/io.weight
echo "50" > /sys/fs/cgroup/data-prep/io.weight

bfq 还支持权重传播——一个持有大量飞行请求的低权重进程会自动获得更多补偿性预算,避免因持续被抢占而完全饿死。

bfq 的低延迟模式争议

bfq 宣传的一个核心优势是低延迟。它的设计确实在单队列 HDD 上表现优异——通过防止一个进程的大 I/O 阻塞其他进程,显著降低了 P99 延迟。但在 NVMe 等快速设备上,其复杂的状态追踪引入了不可忽视的开销:

  • 红黑树操作 O(log n) 在大量并发进程时产生 CPU 开销
  • 预算追踪需要频繁的定时器中断
  • 请求合并效率低于 mq-deadline 的 native 合并

实践中,bfq 更适合需要进程间公平共享的场景(如云租户混合部署),而不适合纯粹的 AI 训练推理性能追求。

生产调优要点

如果必须使用 bfq(例如在共享存储集群中保证多租户公平性):

参数 推荐值 说明
low_latency 1 启用低延迟启发式
slice_idle 0 在 NVMe 上禁用空转等待
timeout_sync 200ms 同步请求超时
max_budget 65536 限制单次最大预算,减少突发

五、AI 工作负载模式与调度器匹配

5.1 AI 训练数据加载

大规模分布式训练的 I/O 模式特征:

  • 大规模顺序读取(训练集遍历,读取 Parquet/ORC/JSONL 等)
  • 随机访问(数据增强时的随机裁剪/翻转)
  • 检查点写入(周期性的全量模型保存)

这类场景中,mq-deadline 的行为最优:其排序合并能有效将随机访问转化为顺序 I/O,FIFO 超时保护确保检查点写入不会因被堆积的读请求阻塞。

5.2 AI 推理服务

推理服务的 I/O 特征:

  • 大量并发的模型权重读取(如 mmap 内存映射 + page fault 触发的按需加载)
  • 缓存系统(Redis/LiteKV)的大量小随机 I/O
  • KV-cache 的持久化写入

kyber 的延迟反馈控制特别适合这种场景:稳定的 P99 延迟比最大化 IOPS 更重要。

5.3 GPU Direct Storage (GDS)

GDS 允许 GPU 显存直接与 NVMe 设备传输数据,绕过 CPU 和系统内存。在这种架构中,I/O 调度器的作用被削弱——请求直接从用户态提交到硬件,绕过内核调度层。

// GDS 路径:用户态 -> cuFile -> NVMe(绕过内核 I/O 调度器)
cuFileRead(&handle, devPtr, size, fileOffset, 0);

但即便使用 GDS,在以下场景仍需要 I/O 调度器:

  • 非 GDS 路径的元数据操作(文件打开/关闭、stat 等)
  • 常规的系统调用 I/O(日志、配置读取等)
  • 混合负载下的 CPU 侧 I/O

六、生产选型决策树

AI 工作负载存储选型决策路径:

  • 如果是训练集群,选 mq-deadline,最大化有效带宽
  • 如果是顺序读主导,设置 nr_requests = 1024
  • 如果是随机读主导,考虑 io_latency 限制
  • 如果是推理服务,选 kyber,获取可预测的 P99 延迟
  • 前端突发度高时,设置 preemption = read
  • 设置 io_latency 为目标微秒值
  • 如果是共享/混合场景,选 bfq + io.weight 实现多租户公平
  • 设置 io.max/bps 进行限速调优
  • 设置 read_expire = 50ms 满足低延迟要求

调优检查清单

#!/bin/bash
# AI 推理集群 NVMe 调度器调优
DEV="/sys/block/nvme0n1"

# 1. 启用 kyber(低延迟目标)
echo "kyber" > $DEV/queue/scheduler

# 2. 设置目标延迟(微秒):读 50ms,写 100ms
echo "50000 100000" > $DEV/queue/io_latency

# 3. 增加队列深度以利用 NVMe 并行性
echo 1024 > $DEV/queue/nr_requests

# 4. 优化 I/O 合并参数
echo 0 > $DEV/queue/nomerges  # 在 NVMe 上禁用合并,降低 CPU 开销

# 5. 监控
cat $DEV/queue/scheduler
cat $DEV/stats

监控与反馈

生产环境的调度器选择不是一次性的决定。建议建立持续监控:

# 实时 I/O 延迟分布
iostat -xmt 1 nvme0n1

# mq-deadline 统计
cat /sys/block/nvme0n1/mq/*/dispatch
# kyber 统计
cat /sys/block/nvme0n1/queue/io_latency
# bfq 统计
cat /sys/block/nvme0n1/queue/budget

一个实用的生产实践是:在工作负载类型变化的边界(如从训练切换到推理)时动态切换调度器。通过监控 P99 延迟的尾部尖刺(tail spike)来触发切换决策。

七、前沿趋势与展望

7.1 io_uring 与 IOSQE_ASYNC

io_uring 提供了 IOSQE_ASYNC 标志,强制某个操作完全绕过 submit SQE 时的上下文切换。在 AI 数据处理管线中,结合 fixed buffers 和 registered files,可以实现零上下文切换的 I/O 提交路径,进一步降低调度器相关的 CPU 开销。

7.2 可编程 I/O 调度:eBPF 扩展

Linux 6.8+ 引入了 bpftool 对 I/O 调度器有限的可编程接口。未来我们可能看到自定义的 AI-aware I/O 调度器——根据 GPU 显存压力动态调整读取优先级,或者实现细粒度的 KV-cache 缓存淘汰策略。

7.3 CXL 内存扩展

CXL 内存引入了新的存储层次。当 NVMe 设备的热数据被缓存到 CXL 内存时,I/O 调度器的任务将从"排序与合并"转变为"预热与驱逐策略"。这可能需要全新的调度器设计理念。

八、总结

Linux 内核 I/O 调度器不是万能药,也不是可有可无的装饰。正确选择和调优调度器,可以在 AI 工作负载中带来 10-30% 的性能差异。

实践原则:

  • 训练集群选 mq-deadline,最大化有效带宽
  • 推理服务选 kyber,获取可预测的 P99 延迟
  • 多租户共享选 bfq + cgroup weight,确保公平
  • 持续监控 P99 延迟尖刺,在负载类型变化时切换调度器

I/O 调度器调优不是"设置后忘"的一次性任务——它是需要嵌入到 MLOps 管线中的持续反馈环节。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ .skip-link { position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } .skip-link:focus { top: 0; outline: 3px solid #0056b3; }