Linux 内核块设备层 blk-mq 深度工程化:从请求提交到多队列 I/O 调度
本文深入分析 Linux 块设备层的多队列架构(blk-mq),涵盖从 NVMe 驱动到 I/O 调度器的完整路径,结合内核源码与生产环境调优案例,剖析现代高性能存储的 I/O 栈实现。
一、为什么需要多队列:单队列架构的性能天花板
在传统块设备层(blk-sq)中,整个设备共享一个请求队列(struct request_queue),通过单一的自旋锁(queue_lock)保护。在 NVMe SSD 时代,单盘 IOPS 可达数十万,单队列锁竞争成为严重的扩展瓶颈。
核心问题归纳为三点:
- 锁竞争:所有 CPU 争抢同一把
queue_lock,高并发下性能退化严重 - 缓存行乒乓:
request_queue结构体在多核间反复弹跳,cache-line bouncing 导致内存带宽浪费 - NUMA 不感知:单队列无法利用 NUMA 本地性,远程内存访问增加延迟
- 硬件队列数(nr_hw_queues):通常与 NVMe Submission Queue 数量一致,建议等于物理核心数
- 软件队列(nr_cpu_ids):每个 CPU 一个软件分发队列,避免跨核同步
- map queues(blk_mq_queue_map):软件队列到硬件队列的映射,支持 irq affinity
- 使用
IORING_SETUP_SQPOLL时,内核轮询线程将 I/O 直接写入 NVMe Submission Queue - 配合
iopolld(5.17+),即使设备不支持轮询也可由内核轮询完成状态 - 推荐 sqpoll 线程绑定到独立 CPU(
sq_thread_cpu) Q2C(Queue to Completion):端到端延迟D2C(Driver to Completion):驱动到完成延迟Q2C - D2C ≈调度器排队 + 驱动程序排队延迟%util:设备利用率,>80% 表示设备饱和aqu-sz(平均队列深度):>128 说明硬件队列已成为瓶颈await(等待时间):异常高说明 I/O 调度器回堵- 消除写放大(WAF ≈ 1.0)
- 主机端 GC 减少 SSD 内部 FTL 压力
- 更适合 RocksDB/LSM-tree 结构
- NVMe 服务器:调度器优先
mq-deadline或none,避免 BFQ 复杂性 - 容器化共享存储:
kyber更适合多租户隔离 - 低延迟 OLTP:io_uring + SQPOLL + 独立 CPU 绑定 +
none调度器 - 写入密集型/混合负载:调整 vm.dirty_ratio + blk-mq nr_requests 控制回写节奏
- ZNS 部署:重新审视 RocksDB 等 LSM-tree 引擎的 Compaction 策略
- 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
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 节点 ... };
关键映射关系:
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 | ~150K | 668 | 1,200 |
| 8队列 + none | ~1,100K | 72 | 180 |
| 16队列 + none | ~1,300K | 61 | 120 |
队列数等于物理核心数时达到最优,超线程不计入。
四、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);
关键优化点:
六、性能瓶颈诊断
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
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
关注指标:
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 的优势:
生产示例:使用 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 的核心价值与工程实践。落实到运维层面:
blk-mq 与 io_uring 共同构成了 Linux 高性能 I/O 的双引擎。理解它们的协同工作方式,是存储性能工程化的核心能力。

发表评论 取消回复