块设备驱动性能调优 设备驱动工程实践IO优化 - 第4节

4. 多队列块层(blk-mq)架构

传统块设备层使用单请求队列(single request queue)模型,所有 I/O 请求在提交到设备驱动前,都经过一个全局自旋锁保护的队列。随着 NVMe 固态硬盘和多核 CPU 的普及,单队列模型成为性能瓶颈:高并发场景下,全局锁争用导致 CPU 利用率不升反降,IOPS 无法随核数线性扩展。

单队列瓶颈关键数据: 在 32 核系统上,单队列模型在 1M IOPS 时自旋锁开销占比超过 40%,有效吞吐量仅为理论峰值的 1/3。

多队列块层(blk-mq)在 Linux 3.13 引入,核心思想是将单队列拆分为多组软件队列(Software Queue)和硬件分发队列(Hardware Dispatch Queue),每个 CPU 或 CPU 组拥有独立的提交路径,消除锁竞争,实现 I/O 路径的近乎线性扩展。

4.1.1 单队列 vs 多队列架构对比

维度单队列(Single Queue)多队列(blk-mq)
队列数量1 个全局请求队列多组软件队列 + 硬件队列
锁竞争全局自旋锁,高并发严重每 CPU 独立队列,无锁或本地锁
扩展性随核数增加性能下降接近线性扩展
硬件适配仅支持单硬件队列(如 SATA)原生支持多硬件队列(如 NVMe)
典型 IOPS(32核)~800K~5M+
演进驱动力: NVMe 固态硬盘原生支持多个硬件提交队列和完成队列,单队列模型无法利用这一硬件能力。blk-mq 让软件队列与硬件队列一一映射,实现端到端的无锁 I/O 路径。

4.2 blk-mq 核心架构:软件队列与硬件队列

blk-mq 将 I/O 路径分为三层:软件队列(Software Queue)、硬件分发队列(Hardware Dispatch Queue) 和 硬件队列(Hardware Queue)。软件队列负责接收来自文件系统的 I/O 请求,按块编号排序合并;硬件分发队列负责将请求批量派发给设备驱动;硬件队列对应物理设备的提交端口。

blk-mq 三层架构:软件队列 → 硬件分发队列 → 硬件队列 软件队列 0 CPU 0 本地提交 排序 + 合并 软件队列 1 CPU 1 本地提交 排序 + 合并 软件队列 N CPU N 本地提交 排序 + 合并 硬件分发队列 0 批量派发 + 流控 硬件分发队列 1 批量派发 + 流控 硬件分发队列 N 批量派发 + 流控 硬件队列 0 NVMe SQ 0 硬件队列 1 NVMe SQ 1 硬件队列 N NVMe SQ N 软件队列按 CPU 划分,硬件分发队列负责流控和批量派发,硬件队列直接映射设备提交端口

4.2.1 软件队列(Software Queue)

软件队列是 blk-mq 的 I/O 入口,每个 CPU 拥有一个独立的软件队列(或每 CPU 组一个)。请求在提交时直接放入当前 CPU 对应的软件队列,无需加锁。软件队列内部维护一个红黑树,按块编号排序,实现相邻请求的合并(plugging)。

  • 每 CPU 独立: 避免跨 CPU 锁竞争,提升提交效率。
  • 排序合并: 将相邻的读/写请求合并为更大的 I/O,减少设备寻道和命令开销。
  • 批量提交: 当软件队列达到一定阈值或定时器触发时,将一批请求下发给硬件分发队列。

4.2.2 硬件分发队列(Hardware Dispatch Queue)

硬件分发队列是软件队列与硬件队列之间的缓冲层,负责流控和批量派发。它从多个软件队列收集请求,按优先级和流控策略派发给对应的硬件队列。硬件分发队列的数量通常等于硬件队列的数量,但可以通过内核参数调整。

流控关键: 硬件分发队列使用令牌桶或基于深度的流控,防止硬件队列溢出。当硬件队列满时,分发队列会暂停派发,等待硬件完成中断释放槽位。

4.2.3 硬件队列(Hardware Queue)

硬件队列直接对应物理设备的提交端口。对于 NVMe 设备,每个硬件队列对应一个 Submission Queue(SQ)和 Completion Queue(CQ)对。设备驱动通过 MMIO 寄存器将请求提交到硬件队列,硬件完成 I/O 后通过中断通知驱动。

硬件类型硬件队列数量典型映射方式
NVMe SSD1~64K 队列每 CPU 或每 NUMA 节点映射
virtio-blk1~256 队列每 vCPU 映射
SATA/SAS1 队列单队列回退模式

4.3 CPU 亲和性与队列映射

blk-mq 的性能优势依赖于正确的 CPU 亲和性配置:软件队列绑定到特定 CPU,硬件队列绑定到特定 CPU 或 NUMA 节点。当 I/O 提交、中断处理和请求完成都在同一 CPU 上时,缓存命中率最高,延迟最低。

CPU 亲和性映射:软件队列 → 硬件队列 → 中断 CPU 0 NUMA 0 CPU 1 NUMA 0 CPU 2 NUMA 1 CPU 3 NUMA 1 软件队列 0 软件队列 1 软件队列 2 软件队列 3 硬件队列 0 硬件队列 1 硬件队列 2 硬件队列 3 NUMA 节点内映射:CPU 0/1 → 软件队列 0/1 → 硬件队列 0/1(同 NUMA 0)

4.3.1 映射策略

  • 每 CPU 映射: 每个 CPU 拥有独立的软件队列和硬件队列,适用于高并发、低延迟场景。
  • 每 NUMA 节点映射: 同一 NUMA 节点内的 CPU 共享一组硬件队列,减少跨 NUMA 内存访问。
  • 共享映射: 多个软件队列映射到同一个硬件队列,适用于硬件队列数量少于 CPU 数量的场景。

4.3.2 中断亲和性配置

硬件队列的中断处理函数必须绑定到与提交 CPU 相同的 NUMA 节点上,否则会导致跨 NUMA 中断处理,增加延迟。使用 irqbalance 或手动设置 /proc/irq/<IRQ>/smp_affinity 来绑定中断。

# 查看 NVMe 设备中断号
cat /proc/interrupts | grep nvme

# 将中断 48 绑定到 CPU 0-3(位掩码 0x0f)
echo 0f > /proc/irq/48/smp_affinity

# 使用 irqbalance 自动绑定(推荐)
systemctl enable irqbalance
systemctl start irqbalance

4.4 性能调优实践

4.4.1 队列深度调整

硬件队列深度(nr_hw_queues)和软件队列深度(queue_depth)直接影响吞吐量和延迟。过深的队列增加延迟,过浅的队列降低吞吐量。

参数位置推荐值说明
nr_hw_queues模块参数等于 CPU 核数硬件队列数量,最大不超过设备支持数
queue_depthsysfs64~256每个硬件队列的请求槽位数
rq_affinitysysfs2请求完成亲和性:2 表示强制同 CPU 完成

4.4.2 查看当前映射状态

# 查看 NVMe 设备的队列映射
cat /sys/block/nvme0n1/mq/0/cpu_list
# 输出示例:0-3

# 查看每个硬件队列的 CPU 亲和性
cat /sys/block/nvme0n1/mq/0/hctx*/cpu_list
# 输出示例:0,1  2,3
调优黄金法则: 确保 I/O 提交、硬件队列中断、请求完成处理都在同一 NUMA 节点内。使用 numactl 绑定应用程序和中断,避免跨 NUMA 访问。

4.4.3 常见问题与排查

  • 中断不均衡: 使用 mpstat -I SUM 观察中断分布,如果集中在少数 CPU,调整 smp_affinity。
  • 队列深度不足: 使用 iostat -x 1 观察 avgqu-sz 和 await,如果队列深度长期接近上限,增大 queue_depth。
  • 跨 NUMA 访问: 使用 numastat -p <进程PID> 检查内存分配是否跨节点。

4.5 本章小结

blk-mq 通过多队列架构解决了单队列模型的锁竞争和扩展性问题。软件队列提供每 CPU 无锁提交,硬件分发队列实现流控和批量派发,硬件队列直接映射设备提交端口。正确的 CPU 亲和性配置是发挥 blk-mq 性能的关键,必须确保 I/O 路径各环节在同一 NUMA 节点内完成。