块设备驱动性能调优:设备驱动工程实践IO优化 - 第4节
4.1 从单队列到多队列:为什么需要演进?
传统块设备层使用单请求队列(single request queue)模型,所有 I/O 请求在提交到设备驱动前,都经过一个全局自旋锁保护的队列。随着 NVMe 固态硬盘和多核 CPU 的普及,单队列模型成为性能瓶颈:高并发场景下,全局锁争用导致 CPU 利用率不升反降,IOPS 无法随核数线性扩展。
多队列块层(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+ |
4.2 blk-mq 核心架构:软件队列与硬件队列
blk-mq 将 I/O 路径分为三层:软件队列(Software Queue)、硬件分发队列(Hardware Dispatch Queue) 和 硬件队列(Hardware Queue)。软件队列负责接收来自文件系统的 I/O 请求,按块编号排序合并;硬件分发队列负责将请求批量派发给设备驱动;硬件队列对应物理设备的提交端口。
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 SSD | 1~64K 队列 | 每 CPU 或每 NUMA 节点映射 |
| virtio-blk | 1~256 队列 | 每 vCPU 映射 |
| SATA/SAS | 1 队列 | 单队列回退模式 |
4.3 CPU 亲和性与队列映射
blk-mq 的性能优势依赖于正确的 CPU 亲和性配置:软件队列绑定到特定 CPU,硬件队列绑定到特定 CPU 或 NUMA 节点。当 I/O 提交、中断处理和请求完成都在同一 CPU 上时,缓存命中率最高,延迟最低。
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_depth | sysfs | 64~256 | 每个硬件队列的请求槽位数 |
| rq_affinity | sysfs | 2 | 请求完成亲和性: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
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 节点内完成。