目录
[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 # 实际生效的亲和性
设置路径有两条:
- 内核自动分配(managed interrupts):NVMe 驱动在初始化时通过
pci_alloc_irq_vectors_affinity()请求内核自动做 NUMA 感知的中断分配,此时smp_affinity由内核管理,用户写入会被忽略。 - 用户手动设置:如果驱动未使用 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 中断优化的核心思路就三条:
- 中断要分散到多个 CPU,避免单核瓶颈。
- 中断要落在 NVMe 设备所属 NUMA 节点的 CPU 上,避免跨节点访问。
- 应用线程的 CPU 亲和性要和中断亲和性对齐,让提交 IO 和完成 IO 在同一个核心上处理。
现代内核的 managed interrupts 机制已经自动处理了前两条。多数性能问题出在第三条------应用线程跨 NUMA 访问 NVMe 设备。排查时从 /proc/interrupts 入手,结合 NUMA 拓扑分析,逐步定位即可。