BFQ I/O 调度器:多租户场景下的延迟控制工程实践

引言:为什么我们需要 I/O 调度器?

在云计算时代,一台物理机运行着数十甚至上百个虚拟机或容器,它们共享同一块 NVMe 磁盘。当一个租户执行大量 I/O 操作时,其他租户的读写请求可能被饿死,导致延迟飙升。Linux 内核提供了多种 I/O 调度器来应对这一问题,其中 BFQ (Budget Fair Queueing) 以其出色的延迟公平性成为生产环境的首选。

BFQ 最早在 Linux 4.12 中被标记为可选,到 5.x 趋于成熟。它不仅能保证各进程获得公平的 I/O 带宽分配,还能在交互场景下实现接近 noop 调度器的低延迟。本文将深入 BFQ 的内部机制,探讨其在多租户场景下的工程部署方案,并提供完整的调优指南。

BFQ 的核心算法

Budget 机制:超越 CFQ 的公平性

BFQ 的核心创新在于引入了 Budget 概念。不同于 CFQ 简单地按时间片旋转分配,BFQ 为每个进程分配一个 Budget(以扇区数为单位),进程消耗完 Budget 后才让出磁盘。


┌─────────────────────────────────────────────────────────────┐
│                    BFQ Scheduling Model                       │
│                                                              │
│   Process A (Budget=32)  ████░░░░░░░░░░░░░░░░░░░            │
│   Process B (Budget=16)  ░░░░████░░░░░░░░░░░░░░░            │
│   Process C (Budget=24)  ░░░░░░░░████░░░░░░░░░░░            │
│                                                              │
│   "Budget 耗尽 → 切换 → 重新分配 Budget"                     │
│   避免了时间片轮转下"IO大小不公平"的问题                      │
└─────────────────────────────────────────────────────────────┘

Budget 的大小不是固定的,而是根据进程的 I/O 模式自适应调整。频繁发出小 I/O 的进程累计会有更多调度机会,而发出大 I/O 的进程每次能处理更多数据。这种机制天然适合混合工作负载。

调度层级:树形分组结构

BFQ 的调度结构分为三层:

层级 作用 数据结构
服务树 (Service Tree) 管理所有活跃进程的调度实体 红黑树 + 时间戳排序
权重分配 按优先级权重计算 Budget 线性映射

服务树始终选择 最小 vtime(虚拟时间)的进程进行调度,这类似于 CFS 的 vruntime 机制,保证了长期公平性。

内核实现深度解析

数据结构核心


// include/linux/bfq.h / kernel/block/bfq-*
struct bfq_entity {
    struct rb_node        rb_node;        // 红黑树节点
    u64                   finish;         // 虚拟完成时间
    u64                   start;          // 调度开始时间
    unsigned int          weight;         // 调度权重
    unsigned int          budget;         // 当前预算 (扇区数)
    
    // 相关联的调度实体和服务树
    struct bfq_sched_data *sched_data;
    struct bfq_entity     *parent;
};

struct bfq_data {
    struct request_queue  *queue;         // 关联的请求队列
    struct bfq_entity     *in_service;    // 当前服务的实体
    struct rb_root        queue_tree;     // 活跃实体红黑树
    
    // 延迟控制参数
    unsigned int          bfq_max_budget; // 单次最大 Budget
    unsigned int          bfq_timeout;    // 饥饿超时时间
    u64                   last_finish;    // 上次完成时间戳
};

I/O 路径分析

BFQ 的 I/O 请求路径可以分为以下步骤:


用户 write() → VFS → page cache → bio → blk-mq → BFQ dispatch → NVMe
                                                              ↓
                                                     Budget 扣减
                                                              ↓
                                                    触发 Budget 耗尽检查
                                                              ↓
                                                    选择下一个进程调度

关键路径上的几个决策点:

  1. Budget 计算:根据进程权重和已服务量重新计算
  2. 饥饿检测:如果进程等待时间超过 bfq_timeout,立即提升优先级
  3. 调度器选择:从服务树中选择最小 vtime 的实体

延迟控制机制

BFQ 通过两个核心机制控制延迟:

1. 饥饿超时 (bfq_timeout)

默认 250ms,任何等待超过此时间的请求会被标记为饥饿,BFQ 优先调度它。这保证了即使在高负载下,交互式任务的响应时间也有上界。

2. 低延迟模式 (low_latency)

当检测到交互式 I/O 模式(频繁小 I/O)时,BFQ 会自动进入 low_latency 模式,缩短调度粒度,减少尾部延迟。

工程部署实战

1. 启用 BFQ 调度器


# 查看当前调度器
cat /sys/block/nvme0n1/queue/scheduler
# 输出: [mq-deadline] none bfq

# 立即切换为 BFQ(临时)
echo bfq > /sys/block/nvme0n1/queue/scheduler

# 永久生效(通过 kernel cmdline)
# GRUB_CMDLINE_LINUX="scsi_mod.use_blk_mq=y intel_iommu=on"
# 或在 udev 规则中设置:
# /etc/udev/rules.d/60-scheduler.rules
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="bfq"

2. cgroup v2 级权重配置

BFQ 原生支持 cgroup v2 的 IO 权重控制,可以为不同租户容器分配不同的 I/O 优先级:


# 查看 cgroup IO 权重接口
cat /sys/fs/cgroup/io.weight
# 默认权重 100,范围 1-10000

# 为高优先级租户分配更多 I/O 权重
mkdir /sys/fs/cgroup/tenant-gold
echo "default 800" > /sys/fs/cgroup/tenant-gold/io.weight

# 为低优先级租户限制 I/O
mkdir /sys/fs/cgroup/tenant-budget
echo "default 50" > /sys/fs/cgroup/tenant-budget/io.weight

# 将容器进程移入对应 cgroup
echo $(pgrep -f "gold-app") > /sys/fs/cgroup/tenant-gold/cgroup.procs
echo $(pgrep -f "budget-app") > /sys/fs/cgroup/tenant-budget/cgroup.procs

3. BFQ sysctl 调优参数


# 通过 sysctl 调整 BFQ 行为(如果内核编译支持)
# 或通过 sysfs 接口

# 调低 Budget 粒度(更细粒度调度,适合延迟敏感型负载)
echo 128 > /sys/block/nvme0n1/queue/iosched/slice_idle

# 开启低延迟模式(适合桌面/交互场景)
echo 1 > /sys/block/nvme0n1/queue/iosched/low_latency

# 调整权重范围
cat /sys/block/nvme0n1/queue/iosched/weight

4. BFQ监控系统

使用 eBPF 或 BPFtrace 跟踪 BFQ 调度事件:


# BPFtrace 脚本:追踪 BFQ 调度延迟
#!/usr/bin/env bpftrace
kprobe:bfq_dispatch_request {
    $bfq = (struct bfq_data *)arg0;
    $budget = $bfq->entity.budget;
    printf("BFQ dispatch: pid=%d budget=%d weight=%d\n", 
           pid, $budget, $bfq->entity.weight);
}

kprobe:bfq_remove_request {
    printf("BFQ remove: pid=%d\n", pid);
}

5. 生产环境性能验证

推荐使用 fio 进行负载测试:


# 回归测试:模拟混合读写
fio --name=bfq-test \
    --ioengine=io_uring \
    --direct=1 \
    --rw=randrw \
    --rwmixread=70 \
    --bsrange=4k-128k \
    --numjobs=8 \
    --time_based \
    --runtime=60 \
    --size=1G \
    --group_reporting

# 测试 99th 尾延迟
fio --name=latency-test \
    --ioengine=io_uring \
    --direct=1 \
    --rw=randread \
    --bs=4k \
    --numjobs=1 \
    --time_based \
    --runtime=30 \
    --lat_percentiles=1 \
    --percentile_list=99:99.9:99.99

BFQ vs 其他调度器

对比测试

调度器 平均延迟 99th 尾延迟 公平性 吞吐 适用场景
none 最低 低 无 最高 单租户裸盘
mq-deadline 中 中 部分 高 通用 NVMe
kyber 中低 中 弱 高 高速 NVMe
BFQ 中 中 强 中高 多租户/混合负载

选型建议

  • 纯数据库节点(单租户高性能):使用 none 或 mq-deadline
  • 通用云主机(多租户混合):使用 BFQ + cgroup v2 权重
  • 实时性要求极高:使用 mq-deadline 配合 ioprio
  • 低延迟交互:开启 BFQ low_latency 模式

生产案例

案例一:虚拟化平台 I/O 隔离

某云平台在 200 核物理机上运行 50+ 虚拟机,发现某些重 I/O 租户影响到其他用户的数据库查询。

部署方案:


硬件: Dell R750, 3.2TB NVMe, Intel Xeon 4316
内核: 5.15+, BFQ scheduler
调度策略:
  ├── 默认 cgroup: weight=100
  ├── 高优租户:   weight=400 (数据库)
  ├── 标准租户:   weight=100 (Web应用)
  └── 归档租户:   weight=50  (备份任务)

效果:

  • 高优租户 P99 读延迟从 12ms 降至 3.5ms
  • 标准租户延迟稳定在 8ms 内
  • 归档任务吞吐未明显下降(仍可达 400MB/s)

案例二:K8s 集群磁盘 QoS

在 K8s 环境中,通过 CSI driver 为不同 StorageClass 配置 BFQ 权重:


apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: premium-io
parameters:
  csi.storage.k8s.io/fstype: ext4
  io-scheduler: bfq
  io-weight: "800"
mountOptions:
  - discard
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard-io
parameters:
  csi.storage.k8s.io/fstype: ext4
  io-scheduler: bfq
  io-weight: "100"

然后在 Pod 层面通过 cgroup 注入:


apiVersion: v1
kind: Pod
metadata:
  annotations:
    io.kubernetes.cri-o.io.weight: "400"
spec:
  containers:
    - name: app
      volumeMounts:
        - mountPath: /data
          name: fast-storage
  volumes:
    - name: fast-storage
      persistentVolumeClaim:
        claimName: premium-pvc

进阶:与 io_uring 的协同

BFQ 与 io_uring 的结合是近年内核社区的重要方向。io_uring 提交的请求同样经过 BFQ 调度层,但 io_uring 的批量提交特性使得 BFQ 的 Budget 计算更加高效。


// 使用 io_uring 时与 BFQ 配合的关键配置
// 通过 uring_cmd 或 PBUF ring 进一步优化
struct io_uring_params params = {
    .sq_thread_idle = 2000,  // SQ 线程空闲超时
    .flags = IORING_SETUP_SQPOLL,  // 内核轮询
};

对于 io_uring 高性能场景,可以通过 IORING_SETUP_ATTACH_WQ 共享 WQ 减少上下文切换,同时 BFQ 会为 WQ 关联的进程分配优先 Budget。

总结

BFQ 是 Linux I/O 调度领域最成熟的公平调度器,其 Budget 机制在保证尾延迟可控的同时不牺牲吞吐量。在多租户云环境中,通过 cgroup v2 权重配合 sysfs 调优可以构建优质的 I/O 隔离方案。

关键要点回顾:

  • BFQ Budget 机制比传统时间片轮转更适合混合 I/O 负载
  • cgroup v2 io.weight 是生产环境多租户隔离的核心工具
  • low_latency 参数对交互式负载至关重要
  • BFQ 与 io_uring 协同工作良好,适合高性能 I/O 架构
  • 对于极致低延迟单租户场景,考虑 none + ioprio 或专用 SPDK

参考文档:kernel/block/bfq-*.c、Documentation/block/bfq-iosched.rst、Linux kernel v5.15+ 源码
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部