Linux 内核块设备层 blk-mq 深度工程化:从请求提交到多队列 I/O 调度

本文深入分析 Linux 块设备层的多队列架构(blk-mq),涵盖从 NVMe 驱动到 I/O 调度器的完整路径,结合内核源码与生产环境调优案例,剖析现代高性能存储的 I/O 栈实现。

一、为什么需要多队列:单队列架构的性能天花板

在传统块设备层(blk-sq)中,整个设备共享一个请求队列(struct request_queue),通过单一的自旋锁(queue_lock)保护。在 NVMe SSD 时代,单盘 IOPS 可达数十万,单队列锁竞争成为严重的扩展瓶颈。

核心问题归纳为三点:

  1. 锁竞争:所有 CPU 争抢同一把 queue_lock,高并发下性能退化严重
  2. 缓存行乒乓:request_queue 结构体在多核间反复弹跳,cache-line bouncing 导致内存带宽浪费
  3. NUMA 不感知:单队列无法利用 NUMA 本地性,远程内存访问增加延迟
  4. blk-mq(Multi-Queue Block Layer)的设计目标清晰:通过多软硬队列分离,将 I/O 路径上的并行粒度提升到每个 CPU 核心。

    二、blk-mq 核心数据结构

    blk-mq 引入了三级队列架构:

    用户空间
    

    ↓ write/read() VFS (generic_file_read_iter) ↓ 文件系统 (ext4/xfs) ↓ bio 提交 块设备层 ──→ 软件队列(swqueue)──→ 硬件队列(hwqueue) ↓ ↓ I/O 调度器 NVMe 驱动 (mq-deadline/ (nvme_queue) kyber/bfq) ↓ NVMe 设备

    2.1 硬件队列映射

    struct blk_mq_tag_set {
    

    const struct blk_mq_ops *ops; unsigned int nr_hw_queues; // 硬件队列数(通常 = NVMe SQ 数量) unsigned int queue_depth; // 每个硬件队列的深度 unsigned int cmd_size; // 私有命令大小 int numa_node; // NUMA 节点 ... };

    关键映射关系:

    • 硬件队列数(nr_hw_queues):通常与 NVMe Submission Queue 数量一致,建议等于物理核心数
    • 软件队列(nr_cpu_ids):每个 CPU 一个软件分发队列,避免跨核同步
    • map queues(blk_mq_queue_map):软件队列到硬件队列的映射,支持 irq affinity

    2.2 请求生命周期

    struct request {
    

    struct request_queue *q; struct blk_mq_ctx *mq_ctx; // 绑定到 CPU 的软件上下文 struct blk_mq_hw_ctx *mq_hctx; // 硬件上下文 struct bio *bio; unsigned int cmd_flags; ... };

    请求分配路径:blk_mq_alloc_request() → blk_mq_rq_ctx_init() → 绑定到当前 CPU 的 mq_ctx,然后通过 blk_mq_hw_ctx 进入硬件队列。

    三、NVMe 驱动中的 blk-mq 实践

    NVMe 驱动是 blk-mq 的最佳实践案例。以 drivers/nvme/host/pci.c 为例:

    static int nvme_setup_irqs(struct nvme_dev *dev, unsigned int nr_io_queues)
    

    { struct pci_dev *pci = dev->pci_dev;

    // 根据 CPU 核心数和 MSI-X 向量数决定队列数 nr_io_queues = pci_alloc_irq_vectors_affinity( pci, 1, nr_io_queues, PCI_IRQ_ALL_TYPES | PCI_IRQ_AFFINITY, &affd);

    // 启用 irq affinity — 关键!将中断绑定到本地 NUMA 节点 for (i = 0; i < nr_io_queues; i++) { irq_update_affinity_desc(pci->irq + i, ...); } }

    生产环境实测数据(Intel P5800X 1.6TB Optane):

    配置IOPS (4K Read)平均延迟(μs)P99延迟(μs)
    单队列 + none~150K6681,200
    8队列 + none~1,100K72180
    16队列 + none~1,300K61120

    队列数等于物理核心数时达到最优,超线程不计入。

    四、I/O 调度器:mq-deadline vs kyber vs bfq

    blk-mq 不再使用传统的 CFQ,三个调度器各有定位:

    4.1 mq-deadline

    最简单的选择。维护读/写两个 FIFO 队列,按 expiration 排序,防止写操作饿死读请求。

    # 查看和调整
    

    cat /sys/block/nvme0n1/queue/scheduler

    mq-deadline kyber [bfq]

    echo mq-deadline > /sys/block/nvme0n1/queue/scheduler echo 128 > /sys/block/nvme0n1/queue/nr_requests # 降低队列深度减少延迟

    mq-deadline 适用于:延迟敏感型 OLTP 数据库、NVMe 直连场景。

    4.2 Kyber

    Google 贡献的调度器,通过延迟反馈环路动态调整读写吞吐:

    static void kyber_timer_fn(struct timer_list *t)
    

    { // 每 2 秒采样一次目标延迟 // 如果实际延迟 > target,减少派发令牌数 // 如果实际延迟 < target,增加派发令牌数 }

    kyber 适用于:混合读写负载、容器化多租户存储。

    4.3 BFQ(Budget Fair Queueing)

    HDD 时代的"公平王者",通过 I/O 预算分配保证每个进程的带宽下限:

    echo bfq > /sys/block/nvme0n1/queue/scheduler
    

    echo 8 > /sys/block/nvme0n1/queue/low_latency # 开启低延迟模式

    BFQ 的 low_latency 模式会牺牲 5-10% 吞吐换取目标延迟保障(默认 8ms)。

    五、生产环境调优实战

    5.1 队列深度调优

    # 查看当前队列深度
    

    cat /sys/block/nvme0n1/queue/nr_requests # 默认 128

    写入密集型场景(降低深度,减少合并失败和延迟)

    echo 64 > /sys/block/nvme0n1/queue/nr_requests

    读取密集型场景(增加深度提升合并率)

    echo 256 > /sys/block/nvme0n1/queue/nr_requests

    5.2 IRQ Affinity 绑定

    # 查看 NVMe 中断号
    

    grep nvme /proc/interrupts

    将中断绑定到本地 NUME 节点 CPU

    假设队列 0 属于 NUMA node 0 的 CPU 0-7

    echo f0 > /proc/irq/32/smp_affinity_list # CPU 4-7

    脚本自动化:

    #!/bin/bash
    

    bind_nvme_irqs.sh — 将 NVMe 中断绑定到本地 NUMA 节点

    for irq in $(grep nvme /proc/interrupts | awk '{print $1}' | tr -d ':'); do node=$(cat /sys/pci_bus/.../numa_node) # 获取设备所属节点 cpus=$(lscpu -p=CPU,NODE | awk -v n=$node '$2==n{print $1}' | paste -sd,) echo $cpus > /proc/irq/$irq/smp_affinity_list echo "IRQ $irq → CPU $cpus (NUMA $node)" done

    5.3 Writeback 与 Dirty Ratio

    高并发写入场景下,Linux 页缓存的 dirty page 管理直接影响 I/O 稳定性:

    # 检查当前 dirty 比率
    

    sysctl -a | grep dirty

    生产环境建议(数据库服务器)

    vm.dirty_ratio = 5 # 强制回写的总内存占比 vm.dirty_background_ratio = 2 # 后台 pdflush 开始回写的阈值 vm.dirty_expire_centisecs = 500 # dirty page 过期时间(秒) vm.dirty_writeback_centisecs = 100 # pdflush 唤醒间隔

    优化目标:避免瞬间大量 dirty page 回收导致 I/O 尖刺。

    5.4 io_uring 与 blk-mq 协同

    当应用程序使用 io_uring 提交 I/O 时,与 blk-mq 的协作路径:

    struct io_uring_sqe sqe = {
    

    .opcode = IORING_OP_READ, .fd = fd, .off = offset, .addr = (unsigned long)buf, .len = len, .user_data = (uint64_t)ctx, };

    // 无需经过 syscall 层,直接提交到 NVMe SQ io_uring_submit(ring);

    关键优化点:

    • 使用 IORING_SETUP_SQPOLL 时,内核轮询线程将 I/O 直接写入 NVMe Submission Queue
    • 配合 iopolld(5.17+),即使设备不支持轮询也可由内核轮询完成状态
    • 推荐 sqpoll 线程绑定到独立 CPU(sq_thread_cpu)

    六、性能瓶颈诊断

    6.1 blktrace + btt 分析 I/O 延迟

    # 抓取 30 秒 I/O 轨迹
    

    blktrace -d /dev/nvme0n1 -w 30 -o nvme_trace

    分析 Q2C(用户提交到完成时间)

    btt -i nvme_trace.blktrace.*

    关键输出

    -------------------- All Devices ------------------- Q2Q Q2C D2C MIN AVG MAX MIN AVG MIN AVG 0.00 85.12 1250 0.00 87.45 0.00 42.30

    • Q2C(Queue to Completion):端到端延迟
    • D2C(Driver to Completion):驱动到完成延迟
    • Q2C - D2C ≈ 调度器排队 + 驱动程序排队延迟

    6.2 iostat 关键指标

    iostat -xmdz nvme0n1 1
    

    Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %util aqu-sz areq-sz r_await w_await nvme0n1 12000 8500 480000 340000 0 150 95.2 15.8 23.4 1.32 2.15

    关注指标:

    • %util:设备利用率,>80% 表示设备饱和
    • aqu-sz(平均队列深度):>128 说明硬件队列已成为瓶颈
    • await(等待时间):异常高说明 I/O 调度器回堵

    6.3 ftrace 追踪块层事件

    # 开启块层 tracepoints
    

    echo 1 > /sys/kernel/debug/tracing/events/block/enable

    追踪请求提交/完成

    cat /sys/kernel/debug/tracing/trace_pipe | grep -E "block_io_|block_rq"

    七、前沿趋势:Flexible Buffers 与 Zone Storage

    7.1 Flexible Buffers (Linux 6.13+)

    新引入的 struct blk_io_vec 支持非连续物理页面映射,减少大块 I/O 的 scatter-gather 开销。主要受益者:加密文件系统(fscrypt)、持久化内存(DAX)。

    7.2 Zoned Namespaces (ZNS) SSD

    NVMe ZNS 将 SSD 划分为多个 zone,必须顺序写入,重置时需要 erase。blk-mq 已支持 ZNS:

    # 查看 ZNS 设备信息
    

    nvme zns report-zones /dev/nvme0n1

    ZNS 的优势:

    • 消除写放大(WAF ≈ 1.0)
    • 主机端 GC 减少 SSD 内部 FTL 压力
    • 更适合 RocksDB/LSM-tree 结构

    生产示例:使用 ZNS SSD + RocksDB 部署,写放大从 4.2x 降至 1.3x,尾延迟 P99 从 12ms 降至 3ms。

    7.3 DAX(Direct Access)绕过块层

    对于持久化内存(PMem),DAX 模式完全跳过块层,直接由 VFS 调用文件系统访问:

    // DAX 模式下,read_iter 直接映射物理地址
    

    static ssize_t dax_iomap_rw(struct iomap_iter *iter, ...) { // 直接进行 memcpy 到用户空间 // 不经过 bio,不经过 blk-mq }

    DAX 的延迟可低至 300ns(相比 NVMe 的 10-100μs),但缺少块层的数据保护(checksum、raid)。

    八、总结与选型建议

    本文梳理了 blk-mq 的核心价值与工程实践。落实到运维层面:

    1. NVMe 服务器:调度器优先 mq-deadline 或 none,避免 BFQ 复杂性
    2. 容器化共享存储:kyber 更适合多租户隔离
    3. 低延迟 OLTP:io_uring + SQPOLL + 独立 CPU 绑定 + none 调度器
    4. 写入密集型/混合负载:调整 vm.dirty_ratio + blk-mq nr_requests 控制回写节奏
    5. ZNS 部署:重新审视 RocksDB 等 LSM-tree 引擎的 Compaction 策略
    6. blk-mq 与 io_uring 共同构成了 Linux 高性能 I/O 的双引擎。理解它们的协同工作方式,是存储性能工程化的核心能力。

      参考

      • Linux 内核文档:Documentation/block/blk-mq.rst
      • NVMe Specification 2.0 — Multi-Queue Architecture
      • Jens Axboe, "Efficient IO with io_uring", 2019
      • Google Cloud, "Choosing the Right I/O Scheduler", 2024

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.360167s