3. Linux 内核 NVMe 驱动:版本、多队列与 I/O 调度

说起 NVMe 性能,大多数人第一反应是硬盘的极限 IOPS。但我得说,跑分之前先看看驱动,这步很多人容易跳过去。内核版本不同,NVMe 驱动的行为差异远比你想象的大。

这一节,我们就聊聊 Linux 内核自带的 NVMe 驱动——它怎么演变过来的,多队列到底怎么工作的,以及 I/O 调度器在这背后扮演什么角色。

3.1 驱动版本演进:从实验到默认

NVMe 驱动最早作为实验性模块进入内核是 3.3 版本(2012 年)。那时候硬件很少,代码也就几百行。我早年做嵌入式存储时,内核还是 3.10 左右,nvme 模块加载后经常莫名掉盘。那会儿驱动还是单队列,CPU 核一多就瓶颈。

真正的转折点在 4.0 以后——blk-mq 框架成熟,NVMe 驱动顺势重写,支持了多队列和硬件中断亲和性。到现在,5.x/6.x 已经非常稳定,大部分厂商标配内核都自带。

NVMe 驱动演进时间线 3.3 – 实验性驱动 4.0 – blk-mq 适配 5.0 – 多队列稳定 6.x – 本地化优化 4.x 之前内核如果要用 NVMe 必须手动模块编译,且 IOPS 受限于单队列。 5.15+ 以后默认开启 nvme 内置驱动,生产环境基本不再需要额外打补丁。 单队列 手动加载 blk-mq 实验 多队列雏形 默认多队列 中断亲和 io_uring 优化 poll 模式

工作中查驱动版本是最常规的操作。下面这段命令能直接告诉你当前系统用的是哪个版本、带没带多队列特性:

# 查看 NVMe 驱动模块详情
modinfo nvme | grep -E 'version|description'

# 或者直接查 /proc 里的内核参数
cat /proc/version  # 内核版本
dmesg | grep nvme  # 驱动加载日志
经验提示: 我建议上线前用 dmesg | grep -i "nvme.*queues" 看一眼实际分配的队列数。有时候 BIOS 或者虚拟化环境会截断队列数,跑分前确认一下。

3.2 多队列支持:blk-mq 和硬件队列映射

你想想看,一个高速 NVMe SSD 能跑到几百万 IOPS,如果所有 CPU 核去抢一个提交队列(Submission Queue),光锁就能把吞吐拖垮。

解决方案就是多队列:每个 CPU 核拥有独立的 SQ/CQ 对,硬件直接根据 CPU 编号分发请求,互不干扰。

NVMe 驱动在 blk-mq 框架下实现了三层队列映射:

  • 软件队列(per-CPU)——每个 CPU 核一个,做请求合并和调度。
  • 硬件队列(per-NVMe SQ)——一个或多个,直接提交到 PCIe 寄存器。
  • 中断绑定(MSI-X)——每个队列独立中断,irqbalance 可以把硬中断固定到指定 CPU 核。
内核版本默认队列数可调参数备注
4.0 – 4.4nr_cpu_ids无
4.5 – 4.19min(nr_cpu_ids, 4)nvme.write_queues默认 4 个写队列,防止过度竞争
5.0+nr_cpu_idsnvme.poll_queues, nvme.write_queues支持读写队列分开,poll 队列独立
6.x自动检测nvme_core.multipath支持原生多路径 + 多队列

我记得有一次帮客户调优,一台 128 核的机器只分配了 4 个写队列。一查是内核版本太老(4.4),写队列默认上限就 4 个。升级内核后 IOPS 直接翻了 4 倍。

# 查看当前 NVMe 设备的队列信息
cat /sys/block/nvme0n1/mq/0/cpu_list   # CPU 亲和
ls /sys/block/nvme0n1/mq/              # 所有队列

# 调整写队列数(5.0+ 内核)
echo 16 > /sys/module/nvme/parameters/write_queues
谨慎调整: 我曾经在内核 5.4 上把 write_queues 调到 64,结果因为中断向量不够,系统直接 panic。调整前一定要看 PCIe MSI-X 中断向量上限(cat /proc/interrupts | grep nvme)。

3.3 I/O 调度器:没有调度器的调度器

NVMe 底层有硬件队列和 SQ Doorbell 机制,天生就支持无锁提交。传统块层的 I/O 调度(比如 CFQ、Deadline)反而成了累赘,增加延迟。

所以 NVMe 的推荐配置是「无调度器」——也就是 "none"(或旧称 "noop")。让请求直接通过 blk-mq 送入硬件队列,不经过任何软件合并。

不过现代内核做了层优化:mq-deadline 调度器在 NVMe 上也能做到几乎零开销,同时提供读写分离的延迟保障。我个人习惯是:

  • 普通场景:用 none,追求极限 IOPS。
  • 混合读写,有延迟抖动要求:用 mq-deadline,因为它会优先调度读请求,对 OLTP 友好。
  • 全面IO + 多路复用(如 NVMe over Fabrics):可以考虑 bfq,但很少人用,因为会大幅降低吞吐。

3.4 快速检查实战

测试前我建议按以下步骤走一遍,能在 10 秒内确定当前环境是否处于最佳状态:

  1. 查内核版本:uname -r(至少 5.0+)
  2. 查驱动版本:modinfo nvme | grep ^filename(确认是内核自带还是自己编译)
  3. 查队列数:ls /sys/block/nvme0n1/mq/ | wc -l(应该等于 CPU 核数,或者至少 8 以上)
  4. 查 I/O 调度器:cat /sys/block/nvme0n1/queue/scheduler(显示括号里是当前正在用的,推荐 none 或 [mq-deadline])
  5. 确认中断亲和:cat /proc/interrupts | grep "nvme0q" | head -5(看看中断是否分散在多个 CPU 上)

避坑点(来自真实失败案例)

我曾经在一台 40 核服务器上跑 fio,IOPS 只有预期的一半。查了半个月,最后发现是 /sys/block/nvme0n1/queue/nr_requests 被固件限制成了 64,导致队列深度太低。解决方法:echo 1024 > /sys/block/nvme0n1/queue/nr_requests,性能直接恢复。

另一个高频问题是:虚拟化环境中 Guest 内核不支持原生多队列映射,导致所有 IO 走一个队列。那就只能升级 Guest 内核,或者换成 virtio-blk。

3.5 小结:驱动是地基,多队列是电梯,调度器是门禁

内核驱动版本决定了你能用多少队列、中断怎么绑、调度器能不能 bypass。跑 NVMe 基准测试前,建议先跑一轮 dmesg | grep -i nvme 并核实队列数和调度器。这个检查比任何参数调整都管用。

记住一句话:NVMe 的潜力在内核驱动里就定了七成。