Linux NVMe 中断排查与性能优化:CPU 亲和性

目录

一、背景知识

二、原理

[2.1 NVMe 多队列模型](#2.1 NVMe 多队列模型)

[2.2 中断亲和性机制](#2.2 中断亲和性机制)

[2.3 NUMA 拓扑的影响](#2.3 NUMA 拓扑的影响)

三、排查流程

[3.1 确认中断分布现状](#3.1 确认中断分布现状)

[3.2 检查当前亲和性配置](#3.2 检查当前亲和性配置)

[3.3 判断是否为 managed interrupts](#3.3 判断是否为 managed interrupts)

[3.4 识别问题模式](#3.4 识别问题模式)

[3.5 实时监控工具](#3.5 实时监控工具)

四、优化方法

[4.1 方案一:让内核自动管理(推荐)](#4.1 方案一:让内核自动管理(推荐))

[4.2 方案二:手动绑定亲和性](#4.2 方案二:手动绑定亲和性)

[4.3 方案三:调整 irqbalance](#4.3 方案三:调整 irqbalance)

[4.4 方案四:应用层配合](#4.4 方案四:应用层配合)

[4.5 方案五:减少队列数量](#4.5 方案五:减少队列数量)

五、端到端排查与优化流程图

六、关键命令速查

七、小结


一、背景知识

NVMe(Non-Volatile Memory Express)设备通过 PCIe 总线直连 CPU,天然支持多队列并行 IO。每个 IO 队列对应一个 MSI-X 中断向量,中断会被投递到某个 CPU 核心上处理。当中断分布不合理时,会出现:

  • 单核 softirq 打满,其余核心空闲
  • IO 延迟抖动(P99 突增)
  • 跨 NUMA 访问带来额外开销

CPU 亲和性(CPU Affinity)就是控制"哪个中断由哪个核心处理"的核心手段。


二、原理

2.1 NVMe 多队列模型

复制代码
                    ┌─────────────────────────────────┐
                    │          NVMe Controller         │
                    ├────────┬────────┬───────┬────────┤
                    │  SQ/CQ │  SQ/CQ │ SQ/CQ │  ...  │
                    │  pair 0│  pair 1│ pair 2│       │
                    └───┬────┴───┬────┴──┬────┴───────┘
                        │        │       │
                   IRQ  0   IRQ  1  IRQ  2    ...
                        │        │       │
                    ┌───▼──┐ ┌──▼───┐ ┌─▼────┐
                    │ CPU 0│ │ CPU 1│ │ CPU 2│  ...
                    └──────┘ └──────┘ └──────┘

关键概念:

  • Submission Queue(SQ)/ Completion Queue(CQ):每对队列独立处理 IO 请求和完成通知。
  • MSI-X 中断:每个 CQ 绑定一个 MSI-X 中断向量,硬件直接将中断投递到目标 CPU。
  • 队列数量:内核默认为每个在线 CPU 创建一个 IO 队列(外加一个 Admin Queue),即 num_queues = min(online_cpus, device_max_queues)。

2.2 中断亲和性机制

Linux 内核中,每个中断向量有一个 smp_affinity 掩码,决定该中断可以被哪些 CPU 处理。

复制代码
/proc/irq/<irq_number>/smp_affinity        # 十六进制掩码
/proc/irq/<irq_number>/smp_affinity_list    # CPU 列表格式(如 0-3,8)
/proc/irq/<irq_number>/effective_affinity   # 实际生效的亲和性

设置路径有两条:

  1. 内核自动分配(managed interrupts):NVMe 驱动在初始化时通过 pci_alloc_irq_vectors_affinity() 请求内核自动做 NUMA 感知的中断分配,此时 smp_affinity 由内核管理,用户写入会被忽略。
  2. 用户手动设置:如果驱动未使用 managed interrupts,可通过 irqbalance 守护进程或直接写 /proc/irq/ 来控制。

2.3 NUMA 拓扑的影响

复制代码
 ┌──────────── NUMA Node 0 ────────────┐   ┌──────────── NUMA Node 1 ────────────┐
 │  CPU 0  CPU 1  CPU 2  CPU 3         │   │  CPU 4  CPU 5  CPU 6  CPU 7         │
 │          Local Memory               │   │          Local Memory               │
 │                                     │   │                                     │
 │  ┌─────────┐                        │   │                                     │
 │  │ NVMe SSD│  (PCIe 挂在 Node 0)    │   │                                     │
 │  └─────────┘                        │   │                                     │
 └─────────────────────────────────────┘   └─────────────────────────────────────┘

NVMe 设备物理上挂在某个 NUMA Node 的 PCIe 总线上。如果中断被投递到远端 NUMA 节点的 CPU,完成回调中的 DMA 数据访问需要跨越 QPI/UPI 总线,延迟增加 40-100ns。

查看设备所属 NUMA 节点:

复制代码
cat /sys/block/nvme0n1/device/device/numa_node
# 或
lspci -s <bdf> -vvv | grep "NUMA node"

三、排查流程

3.1 确认中断分布现状

第一步,找到 NVMe 设备对应的所有中断号:

复制代码
# 方法一:通过 /proc/interrupts 过滤
cat /proc/interrupts | grep nvme

# 方法二:精确查看
ls /sys/class/nvme/nvme0/device/msi_irqs/

输出示例:

复制代码
 35:    1204521          0          0          0   PCI-MSI  nvme0q0   # Admin Queue
 36:          0    2038472          0          0   PCI-MSI  nvme0q1   # IO Queue 1
 37:          0          0    1893201          0   PCI-MSI  nvme0q2   # IO Queue 2
 38:          0          0          0    1756890   PCI-MSI  nvme0q3   # IO Queue 3

关键看各列数字。如果某一列远大于其他列,说明中断集中在单个 CPU 上。

3.2 检查当前亲和性配置

复制代码
# 批量查看所有 NVMe 中断的亲和性
for irq in $(grep nvme /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    echo "IRQ $irq -> affinity: $(cat /proc/irq/$irq/smp_affinity_list) | effective: $(cat /proc/irq/$irq/effective_affinity_list 2>/dev/null || echo N/A)"
done

输出示例:

复制代码
IRQ 35 -> affinity: 0     | effective: 0
IRQ 36 -> affinity: 1     | effective: 1
IRQ 37 -> affinity: 2     | effective: 2
IRQ 38 -> affinity: 3     | effective: 3

3.3 判断是否为 managed interrupts

复制代码
# 检查某个中断号
cat /proc/irq/36/effective_affinity_list

# 尝试写入,如果是 managed interrupt,写入不会生效
echo 4 > /proc/irq/36/smp_affinity_list
cat /proc/irq/36/effective_affinity_list   # 仍然是原来的值

内核 4.15+ 的 NVMe 驱动默认使用 managed interrupts,此时手动修改 smp_affinity 无效。可通过 dmesg 确认:

复制代码
dmesg | grep -i "managed"
# 或查看 irq flags
cat /proc/irq/36/actions  # 如果存在

3.4 识别问题模式

问题一:中断全部落在同一个 CPU

复制代码
# /proc/interrupts 中只有 CPU0 列有计数
 36:   12038472          0          0          0   PCI-MSI  nvme0q1
 37:    9893201          0          0          0   PCI-MSI  nvme0q2

常见原因:

  • irqbalance 未运行或配置了 IRQBALANCE_BANNED_CPUS
  • 手动设置了错误的亲和性掩码
  • 内核启动参数 irqaffinity=0 把所有中断限制到了 CPU 0

问题二:中断落在远端 NUMA 节点

复制代码
# NVMe 挂在 NUMA 0,但中断跑到了 NUMA 1 的 CPU 上
cat /sys/block/nvme0n1/device/device/numa_node   # 输出 0
cat /proc/irq/36/effective_affinity_list          # 输出 4(属于 NUMA 1)

问题三:单队列压力不均

某些 IO 队列处理量远超其他队列。这通常是应用层线程分布不均导致的------NVMe 驱动根据提交 IO 的 CPU 选择对应的队列。

3.5 实时监控工具

复制代码
# 每秒采样中断增量
watch -n 1 -d 'grep nvme /proc/interrupts'

# 使用 perf 观察中断处理开销
perf top -e irq:irq_handler_entry -s irq,cpu

# 使用 mpstat 查看各核 softirq 开销
mpstat -P ALL 1 | grep -E "CPU|all"

# bcc/BPF 工具
hardirqs -d 10     # 统计硬中断耗时分布
softirqs -d 10     # 统计软中断耗时分布

四、优化方法

4.1 方案一:让内核自动管理(推荐)

现代内核(4.15+)的 NVMe 驱动会自动:

  • 为每个在线 CPU 创建一个 IO 队列
  • 使用 managed interrupts 将各队列中断绑定到本地 NUMA 节点的 CPU

大多数场景下这已经是最优方案。确认方式:

复制代码
# 确认队列数等于 CPU 数
cat /sys/block/nvme0n1/device/queue_count

# 确认每个中断绑在不同的 CPU 上
for irq in $(grep nvme0q /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    echo "IRQ $irq -> $(cat /proc/irq/$irq/effective_affinity_list)"
done

4.2 方案二:手动绑定亲和性

适用于旧内核或非 managed interrupts 场景:

复制代码
#!/bin/bash
# 将 NVMe 中断绑定到指定 NUMA 节点的 CPU
NVME_DEV="nvme0"
NUMA_NODE=$(cat /sys/class/nvme/$NVME_DEV/device/numa_node)
NUMA_CPUS=$(numactl --hardware | grep "node $NUMA_NODE cpus:" | cut -d: -f2)

i=0
CPUS_ARRAY=($NUMA_CPUS)
NUM_CPUS=${#CPUS_ARRAY[@]}

for irq in $(grep "${NVME_DEV}q[1-9]" /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    target_cpu=${CPUS_ARRAY[$((i % NUM_CPUS))]}
    echo "$target_cpu" > /proc/irq/$irq/smp_affinity_list
    echo "IRQ $irq -> CPU $target_cpu"
    ((i++))
done

4.3 方案三:调整 irqbalance

如果需要 irqbalance 运行但想控制 NVMe 中断策略:

复制代码
# /etc/sysconfig/irqbalance 或 /etc/default/irqbalance

# 方式一:排除 NVMe 中断,不让 irqbalance 管
IRQBALANCE_BANNED_INTERRUPTS="36 37 38 39"

# 方式二:提示 irqbalance NVMe 中断应靠近指定 NUMA 节点
# 使用 IRQBALANCE_ARGS 中的 --hintpolicy=exact
IRQBALANCE_ARGS="--hintpolicy=exact"

重启服务:

复制代码
systemctl restart irqbalance

4.4 方案四:应用层配合

NVMe 驱动按提交 IO 的 CPU 编号选择队列。因此,让应用线程的 CPU 亲和性与 NVMe 中断亲和性对齐,可以最小化跨核开销:

复制代码
# 将 fio 测试绑定到 NVMe 所在 NUMA 节点
numactl --cpunodebind=0 --membind=0 fio --name=test \
    --filename=/dev/nvme0n1 --ioengine=io_uring --direct=1 \
    --bs=4k --iodepth=64 --rw=randread --numjobs=4

在应用代码中:

复制代码
#define _GNU_SOURCE
#include <sched.h>

// 将当前线程绑定到指定 CPU
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(target_cpu, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);

4.5 方案五:减少队列数量

队列数过多时,每个队列分到的 IO 深度较浅,反而可能降低合并效率。可以通过内核参数限制:

复制代码
# 临时:重新加载 nvme 模块(会断开所有 NVMe 设备)
modprobe -r nvme
modprobe nvme write_queues=0 poll_queues=0

# 或通过内核启动参数
# nvme.write_queues=0 nvme.poll_queues=0

查看效果:

复制代码
cat /sys/block/nvme0n1/device/queue_count

五、端到端排查与优化流程图

复制代码
开始排查
  │
  ▼
查看 /proc/interrupts 中 NVMe 中断分布
  │
  ├── 中断集中在少数 CPU ──────────────────┐
  │                                         ▼
  │                              检查 irqbalance 状态
  │                              检查内核启动参数 irqaffinity=
  │                              检查 smp_affinity 设置
  │                                         │
  │                                         ▼
  │                              修复亲和性配置 ──► 重新验证
  │
  ├── 中断跨 NUMA 节点 ───────────────────┐
  │                                        ▼
  │                            确认设备 numa_node
  │                            对比 effective_affinity
  │                                        │
  │                                        ▼
  │                            绑定中断到本地 NUMA CPU
  │                            绑定应用线程到同一 NUMA
  │                                        │
  │                                        ▼
  │                                   重新验证
  │
  ├── 分布均匀但延迟仍高 ─────────────────┐
  │                                        ▼
  │                            mpstat 检查 softirq 开销
  │                            perf 分析中断处理路径
  │                            考虑开启 io_uring/polling 模式
  │
  ▼
验证优化效果
  │
  ├── fio 基准测试(IOPS、延迟 P99)
  ├── /proc/interrupts 增量采样
  └── mpstat 确认 CPU 负载均衡

六、关键命令速查

目的 命令
查看 NVMe 中断分布 grep nvme /proc/interrupts
查看中断亲和性 cat /proc/irq/<N>/smp_affinity_list
查看实际生效亲和性 cat /proc/irq/<N>/effective_affinity_list
查看设备 NUMA 节点 cat /sys/block/nvme0n1/device/device/numa_node
查看队列数 cat /sys/block/nvme0n1/device/queue_count
手动设置亲和性 echo <cpu_list> > /proc/irq/<N>/smp_affinity_list
实时监控中断变化 watch -n1 -d 'grep nvme /proc/interrupts'
查看各核 softirq mpstat -P ALL 1
NUMA 拓扑信息 numactl --hardware
MSI-X 向量列表 ls /sys/class/nvme/nvme0/device/msi_irqs/

七、小结

NVMe 中断优化的核心思路就三条:

  1. 中断要分散到多个 CPU,避免单核瓶颈。
  2. 中断要落在 NVMe 设备所属 NUMA 节点的 CPU 上,避免跨节点访问。
  3. 应用线程的 CPU 亲和性要和中断亲和性对齐,让提交 IO 和完成 IO 在同一个核心上处理。

现代内核的 managed interrupts 机制已经自动处理了前两条。多数性能问题出在第三条------应用线程跨 NUMA 访问 NVMe 设备。排查时从 /proc/interrupts 入手,结合 NUMA 拓扑分析,逐步定位即可。

相关推荐
wuminyu1 小时前
ForkJoinPool内部WorkQueue的Lock-Free数组操作以及并发任务窃取原理剖析
java·linux·c语言·jvm·c++
快乐の番薯1 小时前
卷王问卷考试系统自动化功能测试
运维·功能测试·自动化
她说彩礼65万1 小时前
C语言 堆区和栈区
java·linux·c语言
一号弯2 小时前
装完LINUX,请先新建日常用户
linux·运维·服务器
lpfasd1232 小时前
WinSW在Win7上失败真相-实测与修复
windows·nginx
驭渊的小故事2 小时前
linux 基础命令 + git 仓库创建和配置命令
linux·git
ShineWinsu2 小时前
对于Redis:主从复制的解析
linux·数据库·c++·redis·缓存·面试·主从复制
Lsetea2 小时前
OpenSSL verify报unable to get issuer certificate:error 2与partial_chain排查
linux·https·ssl证书·openssl·证书链
许彰午3 小时前
03-Linux环境准备依赖包内核参数与用户组
linux·运维·服务器·数据库