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 管线中的持续反馈环节。

发表评论 取消回复