云计算百科
云计算领域专业知识百科平台

Linux NVMe 中断排查与性能优化:NUMA 实战

目录

引言

一、先建立正确的排查思路

二、准备工具与安全注意事项

安全注意事项

三、识别 NVMe 控制器和 PCIe 地址

1. 查看 NVMe 设备

2. 获取 PCIe BDF 地址

3. 确认上层设备是否经过其他块设备

四、检查 PCIe 链路是否正常

五、确认 MSI-X 是否启用

1. 查看 MSI-X 能力

2. 查看设备实际分配的 IRQ

六、查看 NVMe 中断分布

1. 查看 /proc/interrupts

2. 只查看目标设备的 IRQ

3. 动态观察中断计数

七、检查 NVMe 队列数量和 blk-mq 映射

1. 查看块层硬件队列

2. 查看每个 blk-mq 队列对应的 CPU

3. 查看控制器队列信息

八、队列深度与中断合并调优

1. 查看当前队列深度

2. 使用 nvme set-feature 调整 NVMe 队列深度

3. 队列深度过大导致尾延迟升高的原理

4. 不同负载类型下建议的队列深度范围

5. 中断合并与轮询参数的查看与设置

九、总结与快速检查清单

1. 按排查顺序排列的检查项表格

2. 总结:推荐的优化顺序

3. 最常见的 NVMe 性能瓶颈误判场景及避免方法

十、互动与讨论


引言

NVMe SSD 通过 PCIe、DMA、多队列和 MSI-X 中断实现高并发、低延迟 I/O。但在实际系统中,即使 SSD 本身性能很高,也可能因为以下问题无法发挥应有性能:

  • MSI-X 没有正常启用;
  • I/O 队列或中断向量数量不足;
  • NVMe 中断集中在少数 CPU;
  • IRQ、应用线程和设备位于不同 NUMA 节点;
  • irqbalance 与手工中断绑定策略冲突;
  • 队列深度过大,造成尾延迟升高;
  • 中断频率过高,消耗大量 CPU;
  • PCIe 链路降速或降宽;
  • 控制器进入低功耗状态后产生唤醒延迟;
  • 虚拟化环境中的 vCPU 调度或中断注入延迟。

本文从 Linux 实战角度介绍如何观察 NVMe 中断、分析 IRQ 与 CPU 的映射关系,并结合 NUMA、队列深度、中断合并和轮询模式进行优化。


一、先建立正确的排查思路

NVMe 性能问题不能只看 SSD 的带宽或 IOPS。一个完整的 I/O 路径包括:

应用线程
↓
文件系统或裸块设备
↓
Linux 块层 blk-mq
↓
NVMe Submission Queue
↓
PCIe DMA
↓
NVMe 控制器
↓
Completion Queue
↓
MSI-X 中断或轮询
↓
CPU 完成请求

性能优化的目标是让以下资源合理对应:

应用线程
↓
CPU
↓
blk-mq 硬件队列
↓
NVMe Submission/Completion Queue
↓
MSI-X 向量
↓
IRQ 处理 CPU
↓
NUMA 节点

排查时建议遵循以下顺序:

  • 确认测试对象和实际物理设备;
  • 检查驱动、PCIe 链路和设备错误;
  • 确认 MSI-X 是否启用;
  • 检查实际分配的 IRQ 和 NVMe 队列;
  • 在真实负载下观察中断分布;
  • 检查 IRQ、应用线程、内存和设备的 NUMA 关系;
  • 调整 CPU 亲和性或工作负载布局;
  • 最后再考虑队列深度、中断合并、轮询和电源策略。
  • 不要一开始就关闭 IOMMU、停止 irqbalance 或修改大量内核参数。先找到瓶颈,再进行单变量调整。


    二、准备工具与安全注意事项

    常用工具包括:

    sudo apt install pciutils nvme-cli numactl sysstat fio

    不同发行版的软件包管理命令可能不同。

    本文示例使用以下设备名称:

    CTRL=nvme0
    NS=nvme0n1

    其中:

    • /dev/nvme0 是 NVMe 控制器字符设备;
    • /dev/nvme0n1 是 Namespace 块设备;
    • /dev/nvme0n1p1 是对应分区。

    安全注意事项

  • 不要在生产盘上执行随机写测试。
  • 对裸设备执行 fio –rw=randwrite 会破坏数据。
  • 修改 IRQ 亲和性可能影响线上延迟。
  • 重新加载 NVMe 驱动会使设备短暂离线。
  • 原始设备的只读测试虽然不会写入数据,但仍然会占用带宽、队列和控制器资源。
  • 每次只修改一个变量,并保留修改前的基线数据。

  • 三、识别 NVMe 控制器和 PCIe 地址

    1. 查看 NVMe 设备

    nvme list

    示例输出可能包括:

    Node SN Model Namespace
    /dev/nvme0n1 XXXXXXXX Example NVMe SSD 1

    查看控制器和 Namespace 关系:

    nvme list-subsys

    如果系统使用了 NVMe Multipath,一个 Namespace 可能通过多个控制器路径访问。此时必须确认 I/O 实际经过哪条路径。


    2. 获取 PCIe BDF 地址

    PCIe 设备通常使用以下格式标识:

    Domain:Bus:Device.Function

    例如:

    0000:5e:00.0

    可以通过 sysfs 获取控制器对应的 PCIe 地址:

    CTRL=nvme0
    BDF=$(basename "$(readlink -f /sys/class/nvme/$CTRL/device)")
    echo "$BDF"

    然后查看设备和驱动:

    lspci -s "$BDF" -nnk

    正常情况下应看到:

    Kernel driver in use: nvme

    如果设备没有绑定 nvme 驱动,需要先检查:

    • 驱动是否加载;
    • 设备是否被 VFIO 接管;
    • 是否位于虚拟机中;
    • 内核日志中是否存在初始化错误。

    3. 确认上层设备是否经过其他块设备

    实际业务可能访问的是:

    • LVM 逻辑卷;
    • device-mapper;
    • dm-crypt;
    • 软件 RAID;
    • NVMe Multipath;
    • 容器中的映射设备;
    • 虚拟机中的虚拟磁盘。

    可以查看块设备关系:

    lsblk -o NAME,TYPE,PKNAME,MAJ:MIN,SIZE,FSTYPE,MOUNTPOINTS

    如果业务访问的是 /dev/mapper/…,不能简单假设所有 I/O 都落到某个固定 NVMe 控制器上。


    四、检查 PCIe 链路是否正常

    中断调优之前,应先确认 PCIe 链路没有降速、降宽或持续报错。

    sudo lspci -s "$BDF" -vv

    重点查看:

    LnkCap:
    LnkSta:
    DevSta:
    MSI-X:

    也可以筛选:

    sudo lspci -s "$BDF" -vv |
    grep -E 'LnkCap:|LnkSta:|DevSta:|MSI-X:'

    例如,设备能力可能是:

    LnkCap: Speed 16GT/s, Width x4

    实际链路状态可能是:

    LnkSta: Speed 16GT/s, Width x4

    如果 LnkSta 明显低于 LnkCap,例如:

    LnkCap: Speed 16GT/s, Width x4
    LnkSta: Speed 8GT/s, Width x2

    说明链路可能出现降速或降宽。常见原因包括:

    • 主板插槽只提供部分 PCIe 通道;
    • 转接卡或背板限制;
    • BIOS 配置问题;
    • PCIe 信号质量问题;
    • 设备安装位置不正确;
    • PCIe AER 错误后链路降级;
    • 虚拟化平台限制。

    PCIe 链路问题不是 IRQ 绑定能够解决的。


    五、确认 MSI-X 是否启用

    1. 查看 MSI-X 能力

    执行:

    sudo lspci -s "$BDF" -vv | grep -A3 'MSI-X'

    典型输出:

    MSI-X: Enable+ Count=65 Masked-
    Vector table: BAR=0 offset=…
    PBA: BAR=0 offset=…

    其中:

    • Enable+ 表示 MSI-X 已启用;
    • Enable- 表示设备具备 MSI-X 能力,但当前未启用;
    • Count=65 表示 MSI-X Table 支持的最大表项数量;
    • Masked- 表示没有全局屏蔽全部 MSI-X 向量。

    需要注意:

    Count=65 表示设备最多支持的 MSI-X 表项数量,不表示 Linux 当前实际分配了 65 个 IRQ。

    实际分配数量应通过 sysfs 或 /proc/interrupts 查看。


    2. 查看设备实际分配的 IRQ

    PCI_PATH=/sys/bus/pci/devices/$BDF

    ls -1 "$PCI_PATH/msi_irqs"

    输出可能是:

    144
    145
    146
    147
    148

    这些数字是 Linux IRQ 编号。

    统计数量:

    find "$PCI_PATH/msi_irqs" -mindepth 1 -maxdepth 1 | wc -l

    如果 msi_irqs 目录不存在,应检查:

    • MSI/MSI-X 是否实际启用;
    • 设备是否由其他驱动管理;
    • 是否运行在虚拟机中;
    • 内核是否使用了禁用 MSI 的启动参数;
    • 驱动初始化是否失败。

    检查启动参数:

    cat /proc/cmdline

    如果存在以下参数,可能影响 MSI:

    pci=nomsi

    除非为了定位特定硬件问题,否则不建议禁用 MSI/MSI-X。


    六、查看 NVMe 中断分布

    1. 查看 /proc/interrupts

    grep -E 'CPU|nvme' /proc/interrupts

    示例:

    CPU0 CPU1 CPU2 CPU3
    144: 5 0 0 0 PCI-MSI nvme0q0
    145: 0 10234 0 0 PCI-MSI nvme0q1
    146: 0 0 11567 0 PCI-MSI nvme0q2
    147: 0 0 0 10891 PCI-MSI nvme0q3

    通常:

    • nvme0q0 常用于 Admin Queue;
    • nvme0q1、nvme0q2 等通常对应 I/O 队列;
    • 每一列表示该 IRQ 在对应逻辑 CPU 上累计处理的次数。

    不同内核版本和驱动实现的命名可能不同,不能仅根据队列名称断定底层映射关系。


    2. 只查看目标设备的 IRQ

    为了避免系统中存在多个 NVMe 设备时产生混淆,可以根据 PCIe 设备的 msi_irqs 目录逐个查看:

    for path in /sys/bus/pci/devices/$BDF/msi_irqs/*; do
    irq=${path##*/}
    awk -v n="$irq:" '$1 == n {print}' /proc/interrupts
    done

    这样可以把 IRQ 与具体 PCIe 控制器对应起来。


    3. 动态观察中断计数

    watch -n 1 'grep -E "CPU|nvme0q" /proc/interrupts'

    在执行 I/O 负载时,观察以下现象:

    • 哪些数据队列 IRQ 正在增长;
    • IRQ 是否只集中到一个 CPU;
    • 不同队列是否均匀增长;
    • Admin Queue 中断是否很少;
    • 中断数是否与负载变化大致一致。

    不要期待每个 I/O 都产生一次中断。原因包括:

    • 一次中断可以批量处理多个 Completion Queue 条目;
    • 控制器可能使用中断合并;
    • 驱动可能在一次中断中回收多个完成请求;
    • 部分队列可能使用轮询;
    • 工作负载可能命中页缓存,没有产生真实块 I/O。

    七、检查 NVMe 队列数量和 blk-mq 映射

    1. 查看块层硬件队列

    find /sys/block/$NS/mq \\
    -mindepth 1 -maxdepth 1 -type d

    统计数量:

    find /sys/block/$NS/mq \\
    -mindepth 1 -maxdepth 1 -type d | wc -l

    这些目录表示 Linux blk-mq 为该块设备暴露的硬件上下文。它们与 NVMe I/O Queue 关系密切,但不能简单认为:

    blk-mq 目录数量 = MSI-X 数量 = CPU 数量

    三者可能不同。


    2. 查看每个 blk-mq 队列对应的 CPU

    for q in /sys/block/$NS/mq/*; do
    printf '%s: ' "$(basename "$q")"
    cat "$q/cpu_list"
    done

    示例:

    0: 0,4
    1: 1,5
    2: 2,6
    3: 3,7

    这表示不同 CPU 提交的块请求会被映射到对应的硬件队列。

    如果一个应用只运行在 CPU 2,那么它可能主要使用 CPU 2 所映射的某个硬件队列。因此,单线程测试只激活一个 NVMe 队列通常是正常现象,不能直接判断为队列配置错误。


    3. 查看控制器队列信息

    部分内核会提供:

    test -r /sys/class/nvme/$CTRL/queue_count &


    八、队列深度与中断合并调优

    1. 查看当前队列深度

    块层为每个请求队列维护一个 nr_requests 参数,表示该队列最多可以容纳的待处理请求数量。查看当前值:

    cat /sys/block/nvme0n1/queue/nr_requests

    典型输出:

    256

    这个值表示块层允许排队等待下发的请求上限。它并不直接等于 NVMe 控制器内部的队列深度,但会影响请求在块层的堆积程度。

    2. 使用 nvme set-feature 调整 NVMe 队列深度

    NVMe 规范通过 Number of Queues 特性(Feature ID 0x07)配置 I/O 队列数量,而队列深度通常在控制器初始化时由驱动协商确定。对于支持动态调整的设备,可以使用 nvme set-feature 查看或修改相关特性:

    sudo nvme get-feature /dev/nvme0 -f 0x07 -H

    查看当前队列配置:

    sudo nvme set-feature /dev/nvme0 -f 0x07 -v 32

    上面的命令尝试将 I/O 队列数量设置为 32。需要注意:

    • 实际生效的队列数量由驱动、控制器能力和内核参数共同决定;
    • 修改后建议重新加载驱动或重启系统,使配置完整生效;
    • 生产环境修改前务必记录基线数据,并确认业务窗口允许短暂离线。

    块层请求队列深度也可以通过 sysfs 调整:

    echo 512 | sudo tee /sys/block/nvme0n1/queue/nr_requests

    该修改立即生效,但重启后会恢复默认值。如果需要持久化,可以写入 udev 规则或系统启动脚本。

    3. 队列深度过大导致尾延迟升高的原理

    队列深度越大,控制器越容易保持忙碌,理论上可以提高吞吐量。但过大的队列深度会带来以下问题:

    • 请求在队列中等待的时间变长,单个请求的完成时间被拉长;
    • 多个请求同时竞争控制器资源,造成排队抖动;
    • 中断处理批量变大,CPU 处理完成事件的时间点更集中,延迟分布变宽;
    • 在高负载下,队头阻塞会放大尾延迟,使 P99 和 P99.9 明显恶化。

    因此,队列深度并不是越大越好。对于延迟敏感型负载,较小的队列深度配合足够的队列数量,往往能获得更稳定的延迟表现。

    4. 不同负载类型下建议的队列深度范围

    负载类型典型场景建议队列深度说明
    高 IOPS 随机读写、数据库事务 256 – 1024 保持较高深度以充分利用多队列并行能力
    高带宽 顺序读写、日志写入 128 – 512 深度过高收益有限,反而增加延迟
    低延迟 在线交易、实时查询 32 – 128 控制排队长度,优先保证延迟稳定

    以上范围是经验参考值,实际最优值需要通过压测对比确定。建议使用 fio 分别测试不同深度下的吞吐和延迟分布,再结合业务指标选择。

    5. 中断合并与轮询参数的查看与设置

    NVMe 驱动在部分内核版本中提供中断合并和轮询相关参数,通常位于控制器 sysfs 目录下。查看当前配置:

    ls /sys/class/nvme/nvme0/queue/

    查看是否支持轮询模式:

    cat /sys/class/nvme/nvme0/queue/io_poll

    输出 0 表示轮询未启用,1 表示已启用。启用轮询:

    echo 1 | sudo tee /sys/class/nvme/nvme0/queue/io_poll

    部分驱动还支持中断合并相关参数,例如:

    cat /sys/module/nvme/parameters/io_timeout
    cat /sys/module/nvme/parameters/poll_queues

    其中:

    • io_timeout 控制 I/O 超时时间,间接影响请求重试和延迟表现;
    • poll_queues 指定使用轮询模式的队列数量,适合延迟极敏感且 CPU 资源充足的场景。

    修改内核模块参数需要重新加载模块或写入启动参数:

    sudo modprobe -r nvme
    sudo modprobe nvme poll_queues=4

    中断合并和轮询的取舍原则:

    • 中断合并适合高吞吐、可容忍一定延迟的场景,可以减少 CPU 中断开销;
    • 轮询模式适合低延迟、高 CPU 占用可接受的场景,可以避免中断延迟抖动;
    • 两者都需要结合真实负载验证,不能仅凭理论判断。

    九、总结与快速检查清单

    1. 按排查顺序排列的检查项表格

    下表汇总了本文涉及的完整排查流程,按建议的执行顺序排列。每项都给出检查命令、预期结果和常见问题,便于在遇到 NVMe 性能问题时快速对照。

    排查顺序检查项检查命令预期结果常见问题
    1 识别 NVMe 控制器和 PCIe 地址 nvme list、readlink -f /sys/class/nvme/nvme0/device 能确认设备型号、Namespace 和 BDF 地址 设备未绑定 nvme 驱动,或被 VFIO 接管
    2 确认上层设备是否经过其他块设备 lsblk -o NAME,TYPE,PKNAME,MAJ:MIN 能看清 LVM、dm-crypt、Multipath 等映射关系 业务访问的是 /dev/mapper/…,误以为 I/O 直接落到固定控制器
    3 检查 PCIe 链路状态 sudo lspci -s "$BDF" -vv | grep -E 'LnkCap:|LnkSta:' LnkSta 的速度和宽度不低于 LnkCap 链路降速或降宽,IRQ 绑定无法解决
    4 确认 MSI-X 是否启用 sudo lspci -s "$BDF" -vv | grep -A3 'MSI-X' 显示 Enable+,且 msi_irqs 目录存在 MSI-X 未启用,或内核启动参数包含 pci=nomsi
    5 查看设备实际分配的 IRQ ls -1 /sys/bus/pci/devices/$BDF/msi_irqs 能看到多个 IRQ 编号,数量与队列数匹配 msi_irqs 目录不存在,或 IRQ 数量明显偏少
    6 观察中断分布 watch -n 1 'grep -E "CPU|nvme0q" /proc/interrupts' 负载下各 I/O 队列 IRQ 分散到多个 CPU 中断集中在单个 CPU,或队列间计数严重不均
    7 检查 blk-mq 队列与 CPU 映射 for q in /sys/block/$NS/mq/*; do cat "$q/cpu_list"; done 每个硬件队列对应合理的 CPU 集合 单线程测试只激活一个队列,误判为配置错误
    8 确认 NUMA 关系 cat /sys/bus/pci/devices/$BDF/numa_node、numactl –hardware 设备、IRQ 处理 CPU 和应用线程位于同一 NUMA 节点 跨 NUMA 访问导致延迟升高,IRQ 绑定方向错误
    9 调整 IRQ 亲和性 echo "$cpu" | sudo tee /proc/irq/$irq/smp_affinity_list 各队列 IRQ 均匀绑定到设备所在 NUMA 节点的 CPU irqbalance 与手工绑定冲突,调整后被覆盖
    10 调整队列深度与中断合并 cat /sys/block/nvme0n1/queue/nr_requests、cat /sys/class/nvme/nvme0/queue/io_poll 队列深度和中断合并参数与负载类型匹配 队列深度过大导致尾延迟升高,或轮询模式占用过多 CPU

    2. 总结:推荐的优化顺序

    NVMe 性能优化必须遵循从底层到上层的排查顺序,避免在错误的方向上浪费精力。推荐的顺序是:先确认 PCIe 链路和 MSI-X 启用状态,再分析中断分布和 NUMA 关系,最后才调整队列深度和中断合并。

    具体来说,PCIe 链路降速或降宽属于硬件层面的问题,任何 IRQ 绑定或队列参数调整都无法弥补,因此必须最先排除。MSI-X 未启用则意味着设备可能退回到传统中断模式,中断向量数量不足会直接限制多队列并行能力,这也是后续所有分析的前提。只有确认链路正常、MSI-X 已启用,中断分布和 NUMA 关系的分析才有意义。

    在中断分布和 NUMA 关系层面,重点是把 IRQ 处理 CPU、应用线程和设备放在同一个 NUMA 节点,并让各队列的中断均匀分散到该节点的多个 CPU 上。这一步通常能解决大部分中断集中和跨 NUMA 访问导致的延迟问题。

    最后才考虑队列深度和中断合并。队列深度并非越大越好,过大会放大尾延迟;中断合并和轮询模式各有适用场景,需要结合真实负载验证。按照这个顺序排查,可以避免在硬件或中断配置存在根本问题时,盲目调整上层参数而收效甚微。

    3. 最常见的 NVMe 性能瓶颈误判场景及避免方法

    • 误判一:中断集中在单个 CPU 就认为是 IRQ 亲和性问题。单线程测试只激活一个 NVMe 队列是正常现象,因为应用只运行在某个 CPU 上,对应的请求只会映射到该 CPU 关联的硬件队列。避免方法:先确认负载是否多线程、多队列,再观察中断分布;如果单线程负载下中断集中,不能直接判定为配置错误。
    • 误判二:IOPS 不达标就立刻调整队列深度。队列深度只是影响性能的因素之一,PCIe 链路降速、MSI-X 未启用或跨 NUMA 访问都可能造成同样的现象。避免方法:先按检查清单逐项排除硬件和中断配置问题,再考虑队列深度调整,避免在错误方向上反复试错。
    • 误判三:看到 P99 延迟升高就认为是中断合并导致的。尾延迟升高可能来自队列深度过大、队头阻塞、控制器低功耗唤醒或应用线程跨 NUMA 访问。避免方法:先观察中断分布和 NUMA 关系,确认没有更底层的问题后,再评估中断合并参数的影响。
    • 误判四:业务访问 /dev/mapper/… 就假设 I/O 直接落到某个 NVMe 控制器。LVM、dm-crypt、Multipath 等映射设备可能把 I/O 分散到多个底层设备,或经过额外的软件层。避免方法:先用 lsblk 确认块设备关系,再针对实际物理设备进行排查。
    • 误判五:修改 IRQ 亲和性后立即测试,发现没有效果就认为方法无效。irqbalance 可能在后台覆盖手工绑定,或者应用线程仍然运行在错误的 NUMA 节点上。避免方法:先停止并禁用 irqbalance,同时把应用线程绑定到设备所在 NUMA 节点,再重新压测验证。

    十、互动与讨论

    读完本文,相信你已经对 Linux NVMe 中断排查与 NUMA 优化有了系统的认识。为了帮助你把知识真正落地,这里留几个互动问题,欢迎在评论区一起交流:

    • 你遇到过中断集中在单个 CPU 的情况吗?当时是如何定位和解决的?是 IRQ 亲和性问题,还是单线程负载的正常现象?
    • 你的生产环境队列深度设置是多少?是否针对高 IOPS、高带宽或低延迟负载做过针对性调优?调优前后的 P99 延迟变化如何?
    • 你更倾向中断合并还是轮询模式?在什么业务场景下你会选择切换,切换后 CPU 占用和延迟表现有什么变化?
    • 有没有踩过 NUMA 相关的坑?比如应用线程、IRQ 和设备跨节点访问导致性能骤降,你是如何发现并解决的?

    如果你在实战中遇到了本文没有覆盖到的 NVMe 性能问题,或者对某个命令的输出有疑问,欢迎在评论区留言。我会根据大家的反馈,继续补充更多实战案例和排查技巧。

    觉得本文有帮助的话,不妨点个赞、收藏并分享给同样在做存储性能优化的朋友,让更多人少走弯路。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux NVMe 中断排查与性能优化:NUMA 实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!