在 Debian 12 上部署 SPDK:编译、验证与 NVMe-oF 实测
这篇记录了我在 Debian 12 上从源码编译 SPDK v24.09、把它跑起来、再用 fio 压一遍的过程。内容从 I/O 路径和 Reactor 模型讲起,然后是部署、验证、监控和上线前的注意事项。
malloc bdev、NBD、NVMe-oF/TCP 这几条路径都实际跑过,4.5 节有测试环境和原始数字。NVMe 直连需要把盘绑到 VFIO,我的测试环境没有可直通的盘,这部分只给步骤;生产调优和监控接入也按“建议”标注,上生产前要在自己的环境验证。
目录
1. SPDK 是什么
SPDK(Storage Performance Development Kit)是一个用户态存储开发套件,最早由 Intel 发起。它把 NVMe 驱动、块设备抽象(bdev)、NVMe-oF/iSCSI/vhost-user 这些协议和内存管理都放在用户空间,核心做法是轮询代替中断、零拷贝、无锁队列。这样做的直接结果是把 4K 随机读的软件开销压到微秒级,代价是要独占 CPU 核心。
核心能力概览:
| 能力 | 说明 |
|---|---|
| 用户态 NVMe 驱动 | 绕过内核 nvme 驱动,直接操作 PCIe BAR 空间完成 I/O 提交与完成 |
| bdev 抽象层 | 统一块设备接口,支持 malloc / NVMe / AIO / Ceph RBD / RAID / Crypto / QoS 等多种后端,可动态组合与堆叠 |
| 多协议 Target | NVMe-oF(TCP/RDMA)、iSCSI、vhost-user、NBD 等,同一进程可同时暴露多种协议 |
| JSON-RPC 控制面 | 所有运行时配置通过 Unix Domain Socket 上的 JSON-RPC 完成,适合自动化编排 |
| Reactor 线程模型 | 基于 DPDK EAL 的事件驱动框架,每个 Reactor 绑定一个 CPU 核心,通过 run-to-completion 模型避免线程切换开销 |
1.1 SPDK 整体架构
graph TB
subgraph Applications
A1["spdk_tgt"]
A2["nvmf_tgt"]
A3["vhost"]
A4["iscsi_tgt"]
A5["Custom App"]
end
subgraph Control Plane
B1["JSON-RPC Server
Unix Domain Socket"]
B2["rpc.py / curl"]
end
subgraph SPDK Framework
C1["Reactor / Thread Model"]
C2["Event Framework"]
C3["Poller Subsystem"]
end
subgraph Block Device Layer - bdev
D1["malloc"]
D2["NVMe bdev"]
D3["AIO / uring"]
D4["Ceph RBD"]
D5["RAID"]
D6["Crypto"]
D7["QoS / Rate Limit"]
end
subgraph Transport Protocols
E1["NVMe-oF/TCP"]
E2["NVMe-oF/RDMA"]
E3["iSCSI"]
E4["vhost-user"]
E5["NBD"]
end
subgraph Low-Level Drivers
F1["SPDK NVMe Driver
User-Space PCIe"]
F2["DPDK EAL"]
F3["VFIO / UIO"]
end
subgraph Hardware
G1["NVMe SSD"]
G2["NIC - RDMA / TCP"]
end
B2 --> B1
B1 --> A1
B1 --> A2
B1 --> A3
B1 --> A4
B1 --> A5
A1 --> C1
A2 --> C1
A3 --> C1
A4 --> C1
A5 --> C1
C1 --> C2
C2 --> C3
C3 --> D1
C3 --> D2
C3 --> D3
C3 --> D4
C3 --> D5
C3 --> D6
C3 --> D7
D5 --> D2
D6 --> D2
D7 --> D2
D2 --> F1
F1 --> F2
F2 --> F3
F3 --> G1
A1 --> E5
A2 --> E1
A2 --> E2
A4 --> E3
A3 --> E4
E1 --> D1
E1 --> D2
E2 --> D2
E3 --> D2
E4 --> D2
E5 --> D1
E1 --> G2
E2 --> G2
E3 --> G2
看图时可以从下往上:硬件层是 NVMe SSD 和网卡;SPDK 的用户态驱动通过 DPDK 和 VFIO/UIO 接管设备;bdev 层把 malloc、NVMe、AIO、RBD、RAID、Crypto、QoS 这些后端统一成块设备;Reactor 负责事件循环和轮询;最上面是 spdk_tgt、nvmf_tgt、vhost、iscsi_tgt 这些应用,JSON-RPC 负责运行时配置。
RAID、Crypto、QoS 是堆叠在其它 bdev 之上的,所以图中画成指向 NVMe bdev 的箭头。
1.2 传统内核 I/O 路径 vs SPDK I/O 路径
SPDK 的性能来自两条 I/O 路径的差异:
graph TD
subgraph Kernel I/O Path
KA["Application
User Space"] -->|"syscall
context switch"| KB["VFS / Page Cache"]
KB --> KC["Block Layer
I/O Scheduler / mq"]
KC --> KD["NVMe Driver
Kernel Space"]
KD -->|"MMIO doorbell"| KE["NVMe Hardware"]
KE -.->|"completion IRQ"| KD
end
subgraph SPDK I/O Path
SA["Application
User Space"] -->|"function call
no syscall"| SB["SPDK bdev API"]
SB --> SC["SPDK NVMe Driver
User Space / Polling"]
SC -->|"MMIO doorbell"| SE["NVMe Hardware"]
SE -.->|"poll completion queue"| SC
end
关键差异点:
| 维度 | 内核路径 | SPDK 路径 |
|---|---|---|
| 系统调用 | 每次 I/O 至少 1 次 syscall(read/write/io_submit) | 无 syscall,纯函数调用 |
| 中断处理 | 完成后触发硬件中断 -> softirq -> 唤醒进程 | 轮询(polling),Reactor 线程持续检查完成队列 |
| 数据拷贝 | 通常经过 Page Cache 或 bounce buffer | 零拷贝,DMA 直接写入/读取用户态内存(hugepage) |
| 上下文切换 | 用户态 <-> 内核态频繁切换 | 同一用户态线程完成全链路,无切换 |
| 锁竞争 | 内核 block layer 存在多级锁(blk-mq 有所改善) | 无锁设计,每个 Reactor 独占自己的队列与资源 |
| CPU 占用模式 | 空闲时释放 CPU(中断唤醒) | 轮询线程持续占用 CPU 核心(即使无 I/O) |
需要注意的权衡:SPDK 的轮询模式意味着即使没有 I/O 请求,绑定的 CPU 核心也会被 100% 占用。这在 I/O 密集型场景是合理的(用 CPU 换延迟),但在混合负载或低 I/O 场景下需要仔细评估 CPU 预算。
1.3 Reactor 线程模型
调优之前先要理解 Reactor 模型:
graph LR
subgraph CPU Core 0
R0["Reactor 0
Main Reactor"]
R0 --> P0A["Poller: NVMe I/O"]
R0 --> P0B["Poller: JSON-RPC"]
R0 --> P0C["Poller: NVMe-oF Acceptor"]
end
subgraph CPU Core 1
R1["Reactor 1"]
R1 --> P1A["Poller: NVMe-oF
Poll Group 1"]
R1 --> P1B["Poller: bdev I/O"]
end
subgraph CPU Core 2
R2["Reactor 2"]
R2 --> P2A["Poller: NVMe-oF
Poll Group 2"]
R2 --> P2B["Poller: bdev I/O"]
end
核心概念:
- Reactor:绑定到单个 CPU 核心的事件循环线程,永不阻塞,以
run-to-completion模式执行所有注册的 Poller。 - Poller:注册在 Reactor 上的回调函数,分为 active poller(每轮都执行,如 NVMe completion queue 检查)和 timed poller(定时执行,如统计采集)。
- SPDK Thread:逻辑线程,可运行在任意 Reactor 上。一个 Reactor 可以承载多个 SPDK Thread。
- Message Passing:Reactor 之间通过无锁环形队列(ring)传递消息,避免共享数据结构的锁竞争。
这套模型的直接推论:
- 给 SPDK 分配几个核心,它就能使用几个 Reactor 并行处理 I/O;
- 不同 Reactor 之间的 I/O 路径完全隔离,不存在锁争用;
- 核心数不是越多越好,需要匹配实际 I/O 并发度和 NVMe 队列数。
2. 为什么要用 SPDK
2.1 性能层面
企业级 NVMe 的 4K 随机读延迟已经到 5µs 左右。请求经过内核块层、中断和上下文切换后,软件开销往往比介质本身还大。下面是一组公开资料里的典型量级:
| 指标 | 硬件能力 | 内核 NVMe 驱动(fio ioengine=libaio) | SPDK(fio ioengine=spdk) |
|---|---|---|---|
| 4K 随机读延迟 | ~5 µs(介质延迟) | ~12-15 µs | ~6-8 µs |
| 4K 随机读 IOPS(单核) | ~800K+ | ~200-350K | ~600-800K |
| 4K 随机读 IOPS(多核饱和) | ~1.6M+ | ~800K-1M(受限于中断/锁) | ~1.5M+(接近线速) |
| CPU 每 IOP 开销 | - | ~4000-6000 cycles | ~1000-1500 cycles |
以上数据来自公开资料中的典型量级参考(基于 Intel P5800X / Samsung PM9A3 等企业级 NVMe SSD),不是本文环境实测。实际数值因硬件型号、固件版本、内核版本、fio 参数等因素而异,部署后应以自己的实测数据为准。
2.2 实际应用场景
SPDK 本身不是文件系统,也不是开箱即用的存储产品。它更像一套数据面积木:bdev 怎么组、协议怎么暴露、上层怎么接,都要自己决定。下面这些是社区和商业产品里比较常见的用法。
| 场景 | 典型做法 | 要注意的地方 |
|---|---|---|
| NVMe-oF 存储解耦 | 存储节点用 nvmf_tgt 把本地 NVMe 通过 TCP/RDMA 暴露出去,计算节点用内核 nvme connect 接进来,当本地盘用 |
网络决定上限。1Gb 只能做功能验证,4.5 的 24K IOPS 就是例子;生产至少 25Gb,host 要做 allowlist 或 TLS |
| 虚拟化块存储后端 | SPDK 的 vhost 程序通过 vhost-user 把 virtio-blk/scsi 队列交给 QEMU,虚拟机 I/O 不走宿主机内核块层 |
要配 hugepage 和 CPU 绑核;vhost-user socket 的生命周期要和 QEMU 对齐 |
| 分布式存储引擎的本地盘管理 | DAOS 这类引擎用 SPDK 直接管 NVMe,把盘交给用户态 I/O 栈;SPDK 自带的 blobstore/blobfs 可以做上层 | 要改存储引擎的 I/O 层,不是挂个盘就能用;副本和 HA 由上层负责 |
| Kubernetes 持久卷 | spdk-csi 把 SPDK 管理的卷通过 NVMe-oF 暴露给节点,CSI 负责 attach 和 mount |
节点要装 nvme-tcp;网络和 multipath 要提前规划;适合高性能有状态负载 |
| 自研数据面 | 直接基于 bdev API 写 SPDK 应用,再叠加 QoS、RAID、crypto 等 bdev | 开发和运维成本最高,适合存储厂商或大规模自研团队 |
实际选型时,先确定上层要什么接口:要块设备给主机或虚拟机,选 NVMe-oF;要给 QEMU 挂盘,选 vhost-user;要接 Kubernetes,选 CSI;要改存储引擎内部,才需要直接写 bdev 应用。
几个可以继续看的项目:
- Ceph NVMe-oF Gateway:用 SPDK 把 Ceph RBD 镜像通过 NVMe-oF/TCP 暴露给客户端。
- DAOS:HPC/AI 存储引擎,底层用 SPDK 访问 NVMe。
- SPDK CSI:把 SPDK 卷接进 Kubernetes。
- SPDK vhost:QEMU/KVM 的 virtio 后端。
下面这些情况不适合用 SPDK:
- 应用要的是 POSIX 文件系统,直接 open/read/write。SPDK 不提供这些语义。
- I/O 很稀疏,CPU 却很紧张。轮询线程会一直占着核心。
- 只有 SATA/SAS HDD。硬件本身就是瓶颈,换 SPDK 收益有限。
- 团队没有精力维护用户态进程、hugepage、VFIO 和绑核。这种场景先上 io_uring 更划算。
判断要不要上,可以问三个问题:现在的瓶颈是不是 I/O?能不能接受几个 CPU 核心被轮询线程占满?团队有没有精力维护用户态存储栈?前两个是“是”、第三个也有底,再考虑 SPDK。
2.3 与 io_uring 的对比
io_uring 作为 Linux 5.x 引入的新异步 I/O 接口,同样致力于降低 I/O 路径开销,但定位不同:
| 维度 | SPDK | io_uring |
|---|---|---|
| 运行空间 | 用户态,完全绕过内核 | 用户态提交,内核执行 |
| 设备绑定 | 需要将设备从内核驱动解绑(VFIO/UIO) | 无需解绑,内核驱动照常工作 |
| 延迟极限 | 更低(无 syscall、无中断) | 略高于 SPDK,但远低于 libaio |
| 生态兼容 | 独立编程模型,与 POSIX I/O 不兼容 | 兼容现有内核 block 设备与文件系统 |
| 运维复杂度 | 高(hugepage、VFIO、独占 CPU 核心) | 低(内核原生支持,无额外配置) |
| 典型用户 | 存储厂商、SDS 引擎、NVMe-oF Target | 通用高性能应用、数据库(如 io_uring 版 RocksDB) |
选型上可以这么分:已有应用跑在内核文件系统或块设备上,想少改代码提升性能,先用 io_uring;要做 NVMe-oF target、vhost-user 后端,或者自己控制整个数据面,再看 SPDK。两者不是替代关系,很多存储系统会同时用到。
3. Debian 12 部署 SPDK
本节以 Debian 12(bookworm, x86_64)为例,给出可复现的从零部署步骤。默认使用 bash 与 sudo。
说明:本节和第 4 节的 malloc bdev、NBD、NVMe-oF/TCP 回环路径可以在无 NVMe 硬件的 Debian 12 VM 里复现;VFIO 绑定和 NVMe 直连需要真实设备,不在这个范围内。
3.1 环境与前置检查
硬件要求:
| 项目 | 最低要求 | 生产建议 |
|---|---|---|
| CPU 架构 | x86_64(支持 SSE4.2) | 支持 NUMA 的多核 Xeon / EPYC |
| 内存 | 4 GB(含 2 GB hugepage) | 根据 bdev 数量与连接数规划,至少 16 GB |
| 磁盘 | 任意(用 malloc bdev 验证) | NVMe SSD(支持 VFIO) |
| IOMMU | 可选 | 生产环境建议开启(Intel VT-d / AMD-Vi) |
| 网络 | 可选 | NVMe-oF/TCP 需千兆以上网卡;RDMA 需 RoCE/IB 网卡 |
# 检查发行版与内核版本
cat /etc/os-release | grep -E 'PRETTY_NAME|VERSION_ID'
uname -r
# 检查 CPU 是否支持 SSE4.2(DPDK 必需)
grep -o 'sse4_2' /proc/cpuinfo | head -1
# 检查 IOMMU 是否已启用(可选)
dmesg | grep -i -E 'DMAR|IOMMU' | head -5
# 检查现有 NVMe 设备(可选)
lspci -nn | grep -i 'Non-Volatile memory'
nvme list 2>/dev/null || echo "无 NVMe 设备或 nvme-cli 未安装"
3.2 安装构建依赖
两种方式二选一,按环境选:
方式一:手动安装最小依赖集(适合离线准备或严格管控包源的环境)
sudo apt update
sudo apt install -y \
git build-essential meson ninja-build pkg-config \
python3 python3-venv python3-pip python3-pyelftools \
libnuma-dev libssl-dev libaio-dev libfuse3-dev \
uuid-dev libncurses-dev libcunit1-dev \
autoconf automake libtool \
jq curl fio nvme-cli \
nasm
各依赖用途说明:
| 包 | 用途 |
|---|---|
meson / ninja-build |
构建 DPDK 子模块;SPDK 顶层仍用 ./configure + make |
libnuma-dev |
NUMA 感知内存分配 |
libssl-dev |
TLS 支持(NVMe-oF/TCP with TLS) |
libaio-dev |
AIO bdev 后端 |
libfuse3-dev |
NVMe CUSE 字符设备和 blobfs FUSE 挂载 |
python3-pyelftools |
DPDK 构建依赖 |
autoconf / automake / libtool |
构建自带的 ISA-L、ISA-L-crypto 子模块时,autogen.sh 需要 autoreconf |
libcunit1-dev |
编译自带的单元测试 ut 时需要 CUnit 头文件 |
nasm |
ISA-L(存储加速库)汇编编译 |
fio / nvme-cli |
性能测试与 NVMe 管理工具 |
uuid-dev / libncurses-dev |
UUID 生成 / spdk_top TUI 界面 |
scripts/pkgdep.sh会把这些依赖一次装齐;手动装时,libfuse3-dev、autoconf/automake/libtool、meson/ninja-build、libcunit1-dev这几个最容易漏,缺任何一个都会在 configure 或 make 阶段报错。
方式二:使用 SPDK 自带脚本自动安装(推荐联网环境)
# 先克隆源码(见 3.3),然后执行
sudo scripts/pkgdep.sh
该脚本会根据 /etc/os-release 自动判断发行版并安装所有已知依赖,包括 RDMA、iSCSI、FUSE 等可选组件。
如果后续要用 RDMA(RoCE v2),额外确保安装:
sudo apt install -y rdma-core libibverbs-dev librdmacm-dev
3.3 获取并编译 SPDK(固定标签,保证可复现)
固定标签是为了让每次构建拿到同一份代码,避免 HEAD 漂移。
cd /opt
sudo git clone https://github.com/spdk/spdk.git
sudo chown -R $(whoami):$(whoami) /opt/spdk
cd /opt/spdk
# 固定到稳定发布版本
git checkout v24.09
git submodule update --init --recursive
生产环境建议将源码与构建产物放在
/opt/spdk而非$HOME,便于多用户访问与权限管理。
# SPDK 顶层用 configure + make 构建
./configure --with-vhost --with-virtio --with-nvme-cuse
make -j$(nproc)
SPDK v24.09 顶层仍然用
./configure+make,没有meson.build。meson/ninja是 DPDK 子模块的构建工具,make会自动调用,所以依赖里要装;不要按某些文档去执行meson setup build,这里没有对应的构建定义。
# 验证关键二进制已编译成功
ls -la build/bin/spdk_tgt build/bin/nvmf_tgt build/examples/hello_world
离线编译方案:
对于无法联网的生产环境,可在联网构建机上完成以下步骤后打包传输:
# 在联网机上
cd /opt/spdk && git checkout v24.09
git submodule update --init --recursive
# 将 /opt/spdk 整个目录打包(含子模块)
tar czf spdk-v24.09-full.tar.gz -C /opt spdk/
# 传输到离线目标机后解压编译即可
3.4 Hugepage 配置
SPDK 通过 DPDK 使用 hugepage 进行 DMA 缓冲区和内部数据结构的内存分配,这是运行的硬性前提。
容量规划:
| 场景 | 建议 hugepage 数量(2MB 页) | 总计 |
|---|---|---|
| 功能验证(malloc bdev) | 1024 页 | 2 GB |
| 单盘 NVMe + NVMe-oF Target | 2048 页 | 4 GB |
| 多盘 NVMe + 高连接数 | 4096+ 页 | 8+ GB |
具体开销取决于 I/O 大小、队列深度、连接数和共享缓冲区配置,不能只按 bdev 数量估算。功能验证给 1-2 GB 就够;连接多、队列深时,-s 要相应加大。上线前用 spdk_top、bdev_get_iostat 和 HugePages_Free 看余量,再调整。
临时生效(测试用):
# 查看当前 hugepage 状态
grep -i huge /proc/meminfo
# 设置 1024 个 2MB hugepage(约 2GB)
sudo sysctl -w vm.nr_hugepages=1024
# 确保 hugetlbfs 已挂载
sudo mkdir -p /dev/hugepages
sudo mount -t hugetlbfs nodev /dev/hugepages 2>/dev/null || true
# 验证
grep HugePages_ /proc/meminfo
vm.nr_hugepages是请求值,内核实际能分配多少取决于可用内存和碎片。实测在一台 3 GB 的 VM 上第一次写 1024 时,HugePages_Total只到 454(908 MB),接着spdk_tgt -s 1024就报EAL: Not enough memory available! Requested: 1024MB, available: 908MB。先sync,再执行echo 3 | sudo tee /proc/sys/vm/drop_caches,然后重设vm.nr_hugepages,才能拿到 1024 页。生产环境按上面的容量规划预留内存。
持久化(生产环境):
# 写入 sysctl 配置
cat <<'EOF' | sudo tee /etc/sysctl.d/60-spdk-hugepages.conf
# SPDK hugepage configuration
vm.nr_hugepages = 2048
EOF
# 写入 fstab 确保 hugetlbfs 开机挂载
grep -q hugetlbfs /etc/fstab || \
echo 'nodev /dev/hugepages hugetlbfs defaults,mode=1770,gid=0 0 0' | sudo tee -a /etc/fstab
sudo sysctl -p /etc/sysctl.d/60-spdk-hugepages.conf
sudo mkdir -p /dev/hugepages
sudo mount -a
# 验证
grep HugePages_ /proc/meminfo
NUMA 感知的 hugepage 分配(多路服务器):
在多 NUMA 节点的服务器上,应将 hugepage 分配到 NVMe 设备所在的 NUMA 节点,避免跨节点内存访问:
# 查看 NVMe 设备所在的 NUMA 节点
cat /sys/bus/pci/devices/0000:5e:00.0/numa_node
# 在指定节点上分配 hugepage(示例:node 0 分配 1024 页,node 1 分配 1024 页)
echo 1024 | sudo tee /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 1024 | sudo tee /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
1G hugepage 通常要在内核启动参数里预留(
hugepagesz=1G default_hugepagesz=1G hugepages=N),不能像 2MB 页那样随时扩容。对 TLB miss 敏感的场景可以用 1G 页,但要提前规划好内存。
3.5 VFIO 设备绑定(可选,NVMe 直连场景)
SPDK 用户态驱动需要通过 VFIO(推荐)或 UIO 机制获取 NVMe 设备的 PCIe 资源访问权限。
纯 malloc bdev 功能验证可跳过本节。
步骤一:启用 IOMMU
# Intel CPU
sudo mkdir -p /etc/default/grub.d
echo 'GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT intel_iommu=on iommu=pt"' \
| sudo tee /etc/default/grub.d/90-iommu.cfg
# AMD CPU(二选一)
# echo 'GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT amd_iommu=on iommu=pt"' \
# | sudo tee /etc/default/grub.d/90-iommu.cfg
sudo update-grub
sudo reboot
重启后验证:
dmesg | grep -i -E 'DMAR|IOMMU' | head -5
# 应看到类似 "DMAR: IOMMU enabled" 的输出
步骤二:绑定 NVMe 设备
cd /opt/spdk
# 查看当前 NVMe 设备及其驱动绑定关系
sudo scripts/setup.sh status
# 执行绑定(将所有 NVMe 设备绑定到 VFIO;注意:会解除内核 nvme 驱动的访问)
sudo HUGEMEM=4096 scripts/setup.sh
# 再次确认绑定状态
sudo scripts/setup.sh status
生产环境警告:
scripts/setup.sh默认会绑定所有 NVMe 设备。如果系统盘是 NVMe,必须通过PCI_BLOCKED环境变量排除:
# 排除系统盘(假设系统盘是 NVMe,PCI 地址为 0000:01:00.0;用 lspci 确认)
sudo PCI_BLOCKED="0000:01:00.0" HUGEMEM=4096 scripts/setup.sh
恢复设备到内核驱动:
sudo scripts/setup.sh reset
3.6 启动 spdk_tgt 与 JSON-RPC 管理
spdk_tgt 是 SPDK 的通用目标进程,启动后通过 JSON-RPC 动态配置 bdev、transport、subsystem 等。
基础启动:
cd /opt/spdk
# 前台启动(调试用,日志同时写到文件;Ctrl+C 停止)
sudo build/bin/spdk_tgt -m 0x3 -s 2048 -r /var/tmp/spdk.sock 2>&1 | tee /tmp/spdk_tgt.log
# 参数说明:
# -m 0x3 : CPU 核心掩码,0x3 = 核心 0 和 1(二进制 11)
# -s 2048 : SPDK 内存大小(由 hugepage 提供),单位 MB
# -r <path> : JSON-RPC Unix socket 路径
验证进程就绪(另开一个终端):
# 检查进程
pgrep -a spdk_tgt
# 查询已加载的子系统
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock framework_get_subsystems | jq '.[].subsystem'
# 查询 Reactor 列表(确认核心绑定)
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock framework_get_reactors | jq '.reactors[] | {core: .lcore, busy: .busy, idle: .idle}'
3.7 systemd 服务化(生产环境)
生产环境不应使用 & 后台运行进程,应通过 systemd 管理:
sudo cat > /etc/systemd/system/spdk-tgt.service <<'EOF'
[Unit]
Description=SPDK Target Daemon
After=network.target
Wants=network.target
[Service]
Type=simple
# 核心掩码根据实际 NUMA 拓扑调整
ExecStartPre=/sbin/modprobe vfio-pci
ExecStart=/opt/spdk/build/bin/spdk_tgt \
-m 0x3 \
-s 4096 \
-r /var/tmp/spdk.sock \
--wait-for-rpc
ExecStartPost=/bin/sleep 1
ExecStartPost=/opt/spdk/scripts/rpc.py -s /var/tmp/spdk.sock framework_start_init
ExecStartPost=/opt/spdk/conf/init-bdevs.sh
# 优雅停止
ExecStop=/opt/spdk/scripts/rpc.py -s /var/tmp/spdk.sock spdk_kill_instance SIGTERM
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
# 安全加固
LimitMEMLOCK=infinity
LimitNOFILE=65536
LimitCORE=infinity
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now spdk-tgt
sudo systemctl status spdk-tgt
--wait-for-rpc让进程先停在初始化前,等framework_start_init被调用后再启动子系统。bdev 创建要放在framework_start_init之后,因为 bdev RPC 依赖 bdev 子系统已经初始化;上面的ExecStartPost就是按这个顺序排的。
配套的初始化脚本(由上面的 ExecStartPost 调用):
#!/usr/bin/env bash
# /opt/spdk/conf/init-bdevs.sh
# 启动后通过 JSON-RPC 创建所需的 bdev 与 subsystem
set -euo pipefail
RPC="/opt/spdk/scripts/rpc.py -s /var/tmp/spdk.sock"
# 示例:创建 malloc bdev 用于测试(1024 MB,块大小 4096)
$RPC bdev_malloc_create -b Malloc0 1024 4096
# 示例:创建 NVMe-oF/TCP transport
$RPC nvmf_create_transport -t TCP -u 8192 -m 8 -c 4096
echo "SPDK bdev initialization complete."
记得
sudo chmod +x /opt/spdk/conf/init-bdevs.sh,否则 systemd 的ExecStartPost会报 permission denied。这个示例脚本不是幂等的:进程重启后 bdev 名字会冲突,需要先清理或改成按需创建;生产环境按实际拓扑写初始化脚本。
4. 部署后验证
4.1 到 4.4 是四种从低到高的验证路径,覆盖不同硬件条件;4.5 和 4.6 给出一组在 Homelab 双 VM 上的实测数据,以及从 malloc bdev 换成真实 NVMe 的配置。
4.1 最简验证:malloc bdev + NBD + fio
这是无需任何特殊硬件即可完成的端到端验证,通过 SPDK 创建内存块设备,经 NBD(Network Block Device)内核模块映射为标准块设备后使用 fio 测试。
下面的命令假设 3.6 的
spdk_tgt已经在跑。只是做 malloc/NBD 验证时,启动spdk_tgt可以加-u(--no-pci)跳过 PCI 探测;要接 NVMe 设备时不要加。
graph LR
A["fio"] -->|"/dev/nbd0"| B["NBD
Kernel Module"]
B -->|"NBD socket"| C["SPDK bdev
Malloc0"]
C --> D["Hugepage Memory
User Space"]
cd /opt/spdk
# 1) 创建 1 GiB malloc bdev(block_size = 4096)
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_create -b Malloc0 1024 4096
# 2) 加载 NBD 内核模块(允许最多 16 个 nbd 设备)
sudo modprobe nbd nbds_max=16
# 3) 将 bdev 映射到 /dev/nbd0
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nbd_start_disk Malloc0 /dev/nbd0
# 4) 确认设备就绪
lsblk | grep nbd0
# 5) fio 基准测试:4K 随机读写混合,队列深度 32
sudo fio --name=spdk-malloc-verify \
--filename=/dev/nbd0 \
--ioengine=libaio \
--direct=1 \
--rw=randrw \
--rwmixread=70 \
--bs=4k \
--iodepth=32 \
--numjobs=1 \
--time_based \
--runtime=15 \
--group_reporting
# 6) 查看 SPDK 侧 IO 统计
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_get_iostat -b Malloc0 | jq
# 7) 清理
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nbd_stop_disk /dev/nbd0
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_delete Malloc0
nbd_stop_disk前务必确保没有进程仍在使用/dev/nbd0(如 fio 已退出、无文件系统挂载)。可用sudo lsof /dev/nbd0检查。
NBD 路径要经过内核块层,这里的 fio 数字只能确认 SPDK 进程、bdev 和映射都正常,不代表 SPDK 的数据面性能。我在测试环境照上面的命令跑 15 秒,read 38.4K IOPS、write 16.5K IOPS,平均 clat 约 574µs;更多实测见 4.5。
4.2 NVMe 直连验证(需完成 3.5 VFIO 绑定)
以下需要真实 NVMe 设备和 VFIO 绑定。本文的测试环境没有可直通的 NVMe,这部分只做步骤参考。
hello_world 示例程序直接通过 SPDK 用户态 NVMe 驱动与设备交互,不依赖 spdk_tgt 进程:
cd /opt/spdk
# 向 NVMe 设备写入一段字符串再读回
sudo build/examples/hello_world
预期输出应包含:
- SPDK 探测到的 NVMe 控制器和 namespace 信息
- “Hello world!\n” 写入与读回一致的确认消息
要看设备枚举和 bdev 名称,用 spdk_tgt 接管控制器:
sudo build/bin/spdk_tgt -m 0x3 -s 2048 -r /var/tmp/spdk.sock &
sleep 2
RPC="sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock"
$RPC bdev_nvme_attach_controller -b Nvme0 -t pcie -a 0000:5e:00.0
$RPC bdev_get_bdevs | jq -r '.[].name'
压测可以用 SPDK 的 fio 插件或者 bdevperf。fio 插件默认不编译,需要先准备 fio 源码,再用 --with-fio=/path/to/fio 重新 configure 和 make,插件会出现在 build/fio/ 下;bdevperf 是 SPDK 自带的 bdev 压测程序,用法见 examples/bdev/bdevperf/。
v24.09 的
examples/nvme/里没有perf和identify这两个示例,build/examples/perf、build/examples/identify都不存在。旧文档里的这两条命令不要直接照抄。
4.3 NVMe-oF/TCP 回环验证
在同一主机上完成 NVMe-oF target 暴露与 initiator 连接的端到端验证:
graph LR
subgraph Host - Same Machine
A["nvme connect
Kernel Initiator"] -->|"TCP 127.0.0.1:4420"| B["nvmf_tgt
SPDK NVMe-oF Target"]
B --> C["bdev: Malloc0
1 GiB"]
A -.->|"/dev/nvmeXn1"| D["fio"]
end
cd /opt/spdk
# 1) 停止 spdk_tgt(如在运行),启动 nvmf_tgt
sudo pkill spdk_tgt 2>/dev/null; sleep 1
sudo build/bin/nvmf_tgt -m 0x3 -s 2048 -r /var/tmp/spdk.sock &
sleep 2
# 2) 创建 TCP transport
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nvmf_create_transport \
-t TCP \
-u 8192 \
-m 8 \
-c 4096
# 3) 创建 malloc bdev
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_create -b Malloc0 1024 4096
# 4) 创建 NVMe-oF subsystem(NQN 需全局唯一)
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nvmf_create_subsystem \
nqn.2025-10.io.spdk:cnode1 \
-a -s SPDK00000001
# 5) 将 bdev 作为 namespace 挂载到 subsystem
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nvmf_subsystem_add_ns \
nqn.2025-10.io.spdk:cnode1 Malloc0
# 6) 添加 TCP listener
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nvmf_subsystem_add_listener \
nqn.2025-10.io.spdk:cnode1 \
-t tcp -a 127.0.0.1 -s 4420
# 7) 内核 initiator 端:发现与连接
sudo modprobe nvme-tcp
sudo nvme discover -t tcp -a 127.0.0.1 -s 4420
sudo nvme connect -t tcp -n nqn.2025-10.io.spdk:cnode1 -a 127.0.0.1 -s 4420
# 8) 确认新设备出现
sleep 1
sudo nvme list
ls -la /dev/nvme*n* 2>/dev/null
# 9) fio 验证
NVME_DEV=$(ls /dev/nvme*n1 2>/dev/null | tail -1)
if [ -n "$NVME_DEV" ]; then
sudo fio --name=nvmeof-tcp-verify \
--filename="$NVME_DEV" \
--ioengine=libaio \
--direct=1 \
--rw=randread \
--bs=4k \
--iodepth=32 \
--numjobs=1 \
--time_based \
--runtime=10 \
--group_reporting
fi
# 10) 清理(顺序很重要)
sudo nvme disconnect -n nqn.2025-10.io.spdk:cnode1
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nvmf_delete_subsystem nqn.2025-10.io.spdk:cnode1
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_delete Malloc0
跨主机测试时,把命令里的
127.0.0.1换成 target 的实际 IP,listener 也要写成这个 IP。v24.09 里 listener 写成0.0.0.0时,另一台机器连过来会被访问控制拒绝,报does not allow host ... to connect at this address;实测踩过这个坑,细节见 4.5。
4.4 多层 bdev 拓扑验证(RAID + QoS)
这一节用
spdk_tgt。如果刚做完 4.3,先停掉nvmf_tgt,再按 3.6 启动spdk_tgt(可以加-u跳过 PCI)。
SPDK bdev 的核心能力之一是可堆叠组合。以下示例演示在两个 malloc bdev 上构建 RAID0,再对 RAID 设备施加 QoS 限速:
graph TB
A["NBD /dev/nbd0
fio 测试入口"] --> B["bdev: QoS Layer
IOPS Limit: 50000"]
B --> C["bdev: Raid0
Strip Size: 64K"]
C --> D1["bdev: Malloc0
512 MiB"]
C --> D2["bdev: Malloc1
512 MiB"]
cd /opt/spdk
# 1) 创建两个 malloc bdev
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_create -b Malloc0 512 4096
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_create -b Malloc1 512 4096
# 2) 创建 RAID0(条带大小 64K)
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_raid_create \
-n Raid0 -z 64 -r 0 -b "Malloc0 Malloc1"
# 3) 对 Raid0 设置 QoS(限制读写合计 IOPS 为 50000)
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_set_qos_limit \
Raid0 --rw-ios-per-sec 50000
# 4) 映射到 NBD 验证
sudo modprobe nbd nbds_max=16
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nbd_start_disk Raid0 /dev/nbd0
# 5) fio 验证 - 观察 IOPS 是否被限制在 50000 左右
sudo fio --name=raid0-qos \
--filename=/dev/nbd0 \
--ioengine=libaio \
--direct=1 \
--rw=randread \
--bs=4k \
--iodepth=64 \
--numjobs=1 \
--time_based \
--runtime=10 \
--group_reporting
# 6) 清理
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nbd_stop_disk /dev/nbd0
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_raid_delete Raid0
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_delete Malloc0
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_delete Malloc1
实测(2 vCPU 的 VM,NBD 入口):限制设成 20000 时,fio 读到 19.9K IOPS,QoS 生效;设成 50000 时只读到 44.6K,因为这台 VM 的 NBD 路径本身到不了 50K,此时瓶颈不在 QoS。调 QoS 前先用不限速的基线确认后端能跑多快。
4.5 Homelab 实测:本地 NBD 与跨主机 NVMe-oF
前面几节是操作步骤,这一节是实际跑出来的数字。测试环境是一套 PVE 9.2 集群,两台 Debian 12 虚拟机分在两个物理节点上,中间走 1Gb 链路:
| 项目 | 目标机(target) | 发起端(initiator) |
|---|---|---|
| PVE 节点 | infra-node1 | infra-node3 |
| vCPU / 内存 | 2 / 3072 MB | 2 / 1536 MB |
| 虚拟机磁盘 | Ceph 上的 virtio 盘 | 同左 |
| 网络 | virtio-net,物理口 1Gb/s | 同左 |
| SPDK | v24.09(DPDK 24.03.0) | 内核 nvme-tcp 客户端 |
| 测试后端 | malloc bdev,512 MiB | — |
两台 VM 都没有 NVMe 直通。malloc bdev 的数据放在 SPDK 的 hugepage 内存里,所以下面测到的是 NBD 路径和 NVMe/TCP 协议栈,不是 NVMe 介质本身的性能。
先量网络上限:iperf3 单流 10 秒,发送端 889 Mbits/s,接收端 886 Mbits/s。
本地 malloc + NBD
spdk_tgt -m 0x1 -s 1024 -u,512 MiB malloc bdev 映射到 /dev/nbd0,fio 跑 4K 随机读,QD128,每轮 30 秒,前 5 秒预热,共 3 轮:
| 轮次 | IOPS | 带宽 | 平均延迟 | p99 |
|---|---|---|---|---|
| 1 | 56,045 | 219 MiB/s | 2.27 ms | 6.32 ms |
| 2 | 63,305 | 247 MiB/s | 2.01 ms | 4.75 ms |
| 3 | 51,567 | 201 MiB/s | 2.47 ms | 8.36 ms |
中位数是 56,045 IOPS、219 MiB/s、平均延迟 2.27 ms、p99 6.32 ms,三轮的 IOPS 范围是 51,567 到 63,305。波动和 VM 共享 CPU、NBD 单队列都有关系。这个量级只能说明 SPDK 进程、bdev、NBD 映射都工作正常,不能拿来评价 SPDK 的 IOPS。要看数据面性能,用 4.2 的 NVMe 直连,或者 fio 的 SPDK ioengine 直接压 bdev。
跨主机 NVMe-oF/TCP
target 启动 nvmf_tgt -m 0x1 -s 1024 -u,把同一个 512 MiB malloc bdev 通过 TCP 暴露出去;initiator 用内核 nvme connect 接上,在 /dev/nvme0n1 上跑同样的 4K 随机读 QD128,也是 3 轮:
| 轮次 | IOPS | 带宽 | 平均延迟 | p99 |
|---|---|---|---|---|
| 1 | 24,634 | 96.2 MiB/s | 5.16 ms | 10.29 ms |
| 2 | 24,726 | 96.6 MiB/s | 5.14 ms | 9.90 ms |
| 3 | 24,187 | 94.5 MiB/s | 5.26 ms | 11.86 ms |
中位数是 24,634 IOPS、96.2 MiB/s,约 807 Mbits/s,相当于 iperf3 接收端带宽的 91%。瓶颈在 1Gb 链路上,剩下约 9% 是 NVMe/TCP 和 TCP/IP 的开销。平均延迟从本地 NBD 的 2 ms 量级涨到 5 ms,主要来自网络往返和队列排队;这组数字不代表 NVMe-oF 在 10Gb/25Gb 网络或 RDMA 下的延迟。
一个访问控制的坑
target 的 listener 如果写成 -a 0.0.0.0,initiator 从另一台机器连过来时,v24.09 的 spdk_nvmf_subsystem_listener_allowed 会拿连接的实际地址去比对 listener 地址,比对失败,target 日志报:
Subsystem 'nqn.2025-10.io.spdk:cnode1' does not allow host 'nqn...' to connect at this address.
这次 subsystem 开了 -a(allow any host),所以不是 host NQN 被拒,而是 listener 地址对不上。把 listener 改成 target 的具体 IP(示例是 10.0.0.10)就能连上。回环测试两边都用 127.0.0.1,不会碰到这个问题。
我在测试机上照上面的回环命令跑 4K 随机读、QD32、10 秒,得到 47.1K IOPS,平均 clat 659µs。回环不经过物理网络,这组数字只用于确认 target 和 initiator 配置正确;跨主机数据见 4.5。
这组测试对应的是存储解耦场景的最小验证。把 malloc bdev 换成真实 NVMe、把 1Gb 换成 25/100Gb,就是下一节要说的落地形态。
4.6 从 malloc 到真实 NVMe:一个存储节点的最小配置
4.5 用 malloc bdev 代替了真实盘。生产里把 bdev 换成 NVMe,拓扑不变:
graph LR
subgraph Compute Node
A["应用 / 文件系统"]
B["内核 nvme-tcp initiator"]
end
subgraph Network
C["25/100Gb TCP"]
end
subgraph Storage Node
D["SPDK nvmf_tgt"]
E["bdev: Nvme0n1"]
F["NVMe SSD"]
end
A --> B
B -->|"NVMe-oF/TCP"| C
C --> D
D --> E
E --> F
这台测试环境没有可直通的 NVMe,下面的 NVMe bdev 部分只核对了 RPC 语法,没有在真实盘上跑过。上生产前先找一块可以丢数据的盘走一遍。
存储节点上,先把 NVMe 绑到 VFIO(见 3.5),再启动 nvmf_tgt:
cd /opt/spdk
sudo build/bin/nvmf_tgt -m 0x3 -s 4096 -r /var/tmp/spdk.sock &
sleep 2
RPC="sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock"
# 1) 创建 TCP transport
$RPC nvmf_create_transport -t TCP -u 8192 -m 8 -c 4096
# 2) 接管 NVMe 控制器(-a 是 PCI 地址,用 lspci 确认)
$RPC bdev_nvme_attach_controller -b Nvme0 -t pcie -a 0000:5e:00.0
# 3) 确认 namespace bdev 出现,通常是 Nvme0n1
$RPC bdev_get_bdevs | jq -r '.[].name'
创建 subsystem 时不要用 -a,把计算节点的 host NQN 加进 allowlist:
# 在计算节点上取 host NQN,把输出的字符串记下来
nvme show-hostnqn
# 输出示例:nqn.2014-08.org.nvmexpress:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# 在存储节点上
RPC="sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock"
HOSTNQN="nqn.2014-08.org.nvmexpress:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
SUB=nqn.2025-10.io.example:storage0
$RPC nvmf_create_subsystem "$SUB" -s SPDK00000000000001
$RPC nvmf_subsystem_add_host "$SUB" "$HOSTNQN"
$RPC nvmf_subsystem_add_ns "$SUB" Nvme0n1
$RPC nvmf_subsystem_add_listener "$SUB" -t tcp -a 10.0.0.10 -s 4420
计算节点接进来:
sudo modprobe nvme-tcp
sudo nvme discover -t tcp -a 10.0.0.10 -s 4420
sudo nvme connect -t tcp -n nqn.2025-10.io.example:storage0 -a 10.0.0.10 -s 4420
nvme list-subsys
lsblk
之后把它当本地块设备用:mkfs、mount,或者直接给数据库和存储引擎。上线前有几件事要确认:
- 网络:1Gb 只能做功能验证,4.5 的 24K IOPS 就是 1Gb 的天花板。要跑满 NVMe,至少 25Gb,最好 100Gb。
- 多路径:内核 NVMe 自带 multipath,可以连同一 target 的多个 listener 或端口。SPDK 只做 target,不做副本;目标机挂了就是挂了,HA 要在上层做。
- 安全:生产用 host allowlist,不要
-a;把 initiator 的/etc/nvme/hostnqn固定下来并纳入配置管理,否则重装后 NQN 变化会连不上。需要加密时给 host 配 PSK,listener 加--secure-channel。 - 监控:把
bdev_get_iostat、nvmf_get_stats、spdk_top接进 Prometheus,至少盯住延迟、队列深度和 hugepage 余量。
5. 监控体系
SPDK 自带 JSON-RPC 统计接口和 spdk_top。DPDK 的 telemetry 在 SPDK 启动时默认关闭(EAL 参数里有 --no-telemetry),所以监控主要走 JSON-RPC。生产环境可以把这些指标接进 Prometheus + Grafana。
5.1 监控架构总览
graph LR
subgraph SPDK Process
A["spdk_tgt / nvmf_tgt"]
B["JSON-RPC
/var/tmp/spdk.sock"]
end
subgraph Collection Layer
D["spdk_exporter.sh
cron / systemd timer"]
E["spdk_top
Interactive TUI"]
end
subgraph Monitoring Stack
F["node_exporter
textfile collector"]
G["Prometheus"]
H["Grafana"]
I["AlertManager"]
end
A --> B
B --> D
B --> E
D -->|"write .prom file"| F
F --> G
G --> H
G --> I
5.2 JSON-RPC 统计查询
# bdev 层 IO 统计(最常用)
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_get_iostat | jq
# 输出结构(关键字段):
# {
# "tick_rate": 2200000000, <- CPU TSC 频率
# "ticks": 123456789, <- 当前 tick
# "bdevs": [{
# "name": "Malloc0",
# "bytes_read": 1048576,
# "bytes_written": 2097152,
# "num_read_ops": 256,
# "num_write_ops": 512,
# "read_latency_ticks": 12345, <- 累计延迟 ticks,除以 num_read_ops 再换算
# "write_latency_ticks": 23456,
# ...
# }]
# }
# NVMe-oF subsystem 连接信息
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nvmf_get_stats | jq
# Reactor 线程负载
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock framework_get_reactors | jq
平均延迟换算:latency_us = read_latency_ticks / num_read_ops / tick_rate * 1000000。read_latency_ticks 是累计值,不是单次 I/O 的延迟。
5.3 Prometheus 集成(textfile collector 方式)
下面的 exporter 是模板,本文环境没有接入真实的 Prometheus/node_exporter;指标名和采集路径请先在测试环境确认。
sudo cat > /usr/local/bin/spdk_exporter.sh <<'SCRIPT'
#!/usr/bin/env bash
# SPDK Prometheus Exporter - textfile collector
# 每次执行生成 .prom 文件供 node_exporter 采集
set -euo pipefail
SOCK="/var/tmp/spdk.sock"
RPC="/opt/spdk/scripts/rpc.py -s $SOCK"
OUT="/var/lib/node_exporter/textfile_collector/spdk.prom"
TMP=$(mktemp)
# 检查 SPDK 进程是否存活
if ! $RPC framework_get_reactors >/dev/null 2>&1; then
echo '# SPDK process not running' > "$TMP"
echo 'spdk_up 0' >> "$TMP"
mv "$TMP" "$OUT"
exit 0
fi
echo 'spdk_up 1' > "$TMP"
# bdev IO 统计
IOSTAT=$($RPC bdev_get_iostat 2>/dev/null || echo '{}')
TICK_RATE=$(echo "$IOSTAT" | jq -r '.tick_rate // 1')
echo "$IOSTAT" | jq -r --argjson tr "$TICK_RATE" '
.bdevs[]? |
"spdk_bdev_bytes_read{bdev=\"\(.name)\"} \(.bytes_read // 0)",
"spdk_bdev_bytes_written{bdev=\"\(.name)\"} \(.bytes_written // 0)",
"spdk_bdev_read_ops_total{bdev=\"\(.name)\"} \(.num_read_ops // 0)",
"spdk_bdev_write_ops_total{bdev=\"\(.name)\"} \(.num_write_ops // 0)",
"spdk_bdev_read_latency_us{bdev=\"\(.name)\"} \((if .num_read_ops > 0 then (.read_latency_ticks / .num_read_ops / $tr * 1000000) else 0 end))",
"spdk_bdev_write_latency_us{bdev=\"\(.name)\"} \((if .num_write_ops > 0 then (.write_latency_ticks / .num_write_ops / $tr * 1000000) else 0 end))"
' >> "$TMP"
# Reactor 负载统计
$RPC framework_get_reactors 2>/dev/null | jq -r '
.reactors[]? |
"spdk_reactor_busy_ticks{core=\"\(.lcore)\"} \(.busy // 0)",
"spdk_reactor_idle_ticks{core=\"\(.lcore)\"} \(.idle // 0)"
' >> "$TMP"
mv "$TMP" "$OUT"
chmod 0644 "$OUT"
SCRIPT
sudo chmod +x /usr/local/bin/spdk_exporter.sh
sudo mkdir -p /var/lib/node_exporter/textfile_collector
使用 systemd timer 替代 cron(更精确、可观测):
# 创建 service 单元
sudo cat > /etc/systemd/system/spdk-exporter.service <<'EOF'
[Unit]
Description=SPDK Prometheus Metrics Exporter
[Service]
Type=oneshot
ExecStart=/usr/local/bin/spdk_exporter.sh
User=root
EOF
# 创建 timer 单元(每 15 秒采集一次)
sudo cat > /etc/systemd/system/spdk-exporter.timer <<'EOF'
[Unit]
Description=Run SPDK exporter every 15 seconds
[Timer]
OnBootSec=30
OnUnitActiveSec=15
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now spdk-exporter.timer
# 验证
sudo systemctl list-timers | grep spdk
cat /var/lib/node_exporter/textfile_collector/spdk.prom
5.4 spdk_top 与 Reactor 观测
spdk_top 是 SPDK 自带的实时 TUI,连到运行中的进程看 Thread、Poller 和核心负载:
cd /opt/spdk
# -r 指定 JSON-RPC socket,默认就是 /var/tmp/spdk.sock
sudo build/bin/spdk_top -r /var/tmp/spdk.sock
界面里按 Tab 切换:
| 视图 | 内容 |
|---|---|
| Threads | 各 SPDK Thread 的 busy/idle 百分比、Poller 数量 |
| Pollers | 各 Poller 的类型(active/timed/paused)、执行次数、周期 |
| Cores | 各 Reactor 核心的负载分布 |
要脚本化的话直接用 JSON-RPC:framework_get_reactors 看每个 Reactor 的 busy/idle,bdev_get_iostat 看每个 bdev 的 IOPS 和延迟。DPDK telemetry 默认关闭,不要指望在 /var/run/dpdk/*/telemetry 找到可用的端点。
5.5 关键告警指标建议
| 指标 | 告警条件 | 说明 |
|---|---|---|
spdk_up |
== 0 | SPDK 进程不可达 |
| Reactor busy 比率 | > 95% 持续 5 分钟 | Reactor 过载,需增加核心或优化连接分布 |
| bdev read/write latency | P99 超过基线(本地 NVMe 可从 100µs 起,NVMe-oF 按网络基线放宽) | 延迟异常,检查 NVMe 健康或 QoS 配置 |
| hugepage free | < 10% 总量 | hugepage 即将耗尽,需扩容或排查泄漏 |
| NVMe-oF 连接数 | 突变 > 20% | 客户端大规模连接/断开,检查网络或客户端状态 |
6. 版本升级与回滚
6.1 升级策略
SPDK 是用户态进程,升级就是编译新版本、停旧进程、起新进程。需要关注两件事:JSON-RPC 接口兼容性,以及停机和恢复要多久。
graph TD
A["阅读 Release Notes
确认 API 变更"] --> B["在测试环境编译新版本"]
B --> C["运行回归测试
malloc + NVMe-oF 验证"]
C --> D{"测试通过?"}
D -->|"Yes"| E["生产环境:编译新版本
保留旧版本二进制"]
D -->|"No"| F["修复脚本/配置
适配新 API"]
F --> C
E --> N["更新 service 指向新目录"]
N --> G["维护窗口:停止旧进程"]
G --> H["启动新版本进程"]
H --> I["执行初始化脚本
JSON-RPC 配置"]
I --> J["端到端验证"]
J --> K{"验证通过?"}
K -->|"Yes"| L["完成升级"]
K -->|"No"| M["回滚:停止新进程
启动旧版本二进制"]
并行编译,不停服准备:
cd /opt/spdk
git fetch --tags
# 查看可用版本
git tag -l 'v25.*' | sort -V
# 以 v25.09 为例,换成你要升级的 tag
# 在不同目录编译新版本(不影响正在运行的旧版本)
sudo cp -a /opt/spdk /opt/spdk-v25.09
cd /opt/spdk-v25.09
git checkout v25.09
git submodule update --init --recursive
./configure --with-vhost --with-virtio --with-nvme-cuse
make -j$(nproc)
# 验证新版本编译成功
build/bin/spdk_tgt --version
# 把 systemd 指向新目录,但先不重启,等维护窗口再切
sudo sed -i 's|/opt/spdk/|/opt/spdk-v25.09/|g' /etc/systemd/system/spdk-tgt.service
sudo systemctl daemon-reload
6.2 API 兼容性检查
SPDK 的 JSON-RPC 接口在大版本间可能有变更(参数重命名、方法废弃等)。升级前务必对比:
# 导出当前版本的 RPC 方法列表
cd /opt/spdk
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock rpc_get_methods | jq -r '.[]' | sort > /tmp/rpc_methods_old.txt
# 编译新版本后启动临时实例,导出其方法列表
cd /opt/spdk-v25.09
sudo build/bin/spdk_tgt -m 0x4 -s 512 -r /var/tmp/spdk_test.sock &
sleep 2
sudo python3 scripts/rpc.py -s /var/tmp/spdk_test.sock rpc_get_methods | jq -r '.[]' | sort > /tmp/rpc_methods_new.txt
sudo pkill -f "spdk_tgt.*spdk_test.sock"
# 对比差异
diff /tmp/rpc_methods_old.txt /tmp/rpc_methods_new.txt
6.3 回滚方案
# 回滚只需切换二进制路径
sudo systemctl stop spdk-tgt
# 把 ExecStart、rpc.py、init-bdevs.sh 的路径一起换回旧版本
sudo sed -i 's|/opt/spdk-v25.09|/opt/spdk|g' /etc/systemd/system/spdk-tgt.service
sudo systemctl daemon-reload
sudo systemctl start spdk-tgt
# 验证
sudo python3 /opt/spdk/scripts/rpc.py -s /var/tmp/spdk.sock framework_get_subsystems | jq '.[0]'
7. 生产环境最佳实践
以下是生产建议,本文环境没有逐条实测;上生产前请先在测试环境验证。
7.1 NUMA 亲和性
在多路服务器上,NVMe 设备、网卡与 CPU/内存之间的 NUMA 拓扑直接影响延迟。核心原则:让 SPDK Reactor 绑定的 CPU 核心、hugepage 内存、NVMe 设备三者在同一 NUMA 节点。
# 1) 确认 NVMe 设备的 NUMA 节点
cat /sys/bus/pci/devices/0000:5e:00.0/numa_node
# 输出示例: 1
# 2) 确认 NUMA 节点对应的 CPU 核心
lscpu | grep -i numa
# NUMA node0 CPU(s): 0-15,32-47
# NUMA node1 CPU(s): 16-31,48-63
# 3) 选择 NUMA node 1 的核心用于 SPDK(如核心 16-19)
# 核心掩码计算: cores 16,17,18,19 -> 0xF0000
sudo build/bin/spdk_tgt -m 0xF0000 -s 4096 -r /var/tmp/spdk.sock
# 4) 确保 hugepage 分配在 node 1
echo 2048 | sudo tee /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
先在 node0 上把
nr_hugepages写 0 释放已分配的页,再往 node1 分配;两边写死的数字加起来不能超过物理内存。
7.2 CPU 核心规划
| 角色 | 建议核心数 | 说明 |
|---|---|---|
| Main Reactor | 1 | 处理 JSON-RPC、管理面、Acceptor |
| I/O Reactor(NVMe-oF) | N | 每个 Reactor 处理一组连接,N 取决于并发连接数与目标吞吐 |
| NVMe I/O 通道 | 与 Reactor 数一致 | 每个 Reactor 自动创建对应的 NVMe I/O Queue Pair |
| 系统预留 | 2-4 | 留给 OS 内核、监控采集、SSH 等 |
单 Reactor 能跑多少 4K IOPS,取决于 bdev 类型、协议和 I/O 大小。malloc bdev 可以到几十万,NVMe-oF/TCP 往往先撞到网卡或 TCP 栈。不要照搬固定数字,用 fio 在目标环境压一遍再定核心数。连接数可以从每 Reactor 32-64 个开始观察,RDMA 下单 Reactor 能承载的吞吐更高,核心数可以少一些。
7.3 NVMe-oF Transport 调优参数
# TCP Transport 关键参数
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock nvmf_create_transport \
-t TCP \
-u 8192 \
-m 8 \
-c 4096 \
-n 4096 \
-q 128
# 参数说明:
# -u (io_unit_size) : 单次 I/O 传输单元大小,建议 8192-131072
# -m (max_io_qpairs_per_ctrlr) : 每个控制器最大 I/O 队列对数
# -c (in_capsule_data_size) : capsule 内嵌数据大小,小 I/O 场景可增大以减少 RTT
# -n (num_shared_buffers) : 共享缓冲区数量,高并发场景需增大
# -q (max-queue-depth) : 每个队列最大未完成 I/O 数
7.4 内核参数调优
cat <<'EOF' | sudo tee /etc/sysctl.d/61-spdk-network.conf
# TCP buffer sizes for NVMe-oF/TCP
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 87380 16777216
# Increase connection backlog
net.core.somaxconn = 4096
net.core.netdev_max_backlog = 8192
# Disable TCP slow start after idle (对 NVMe-oF 重要)
net.ipv4.tcp_slow_start_after_idle = 0
EOF
sudo sysctl -p /etc/sysctl.d/61-spdk-network.conf
7.5 安全加固
# 1) JSON-RPC socket 权限限制(仅 root 和 spdk 组可访问)
sudo groupadd -r spdk 2>/dev/null || true
sudo chgrp spdk /var/tmp/spdk.sock
sudo chmod 0660 /var/tmp/spdk.sock
# 2) 如果要以非 root 用户运行,在 service 里配 User= 和所需 capabilities:
# CapabilityBoundingSet=CAP_SYS_ADMIN CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_RAWIO
# AmbientCapabilities=CAP_SYS_ADMIN CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_RAWIO
# 实际需要的 capability 随版本和驱动变化,先在测试环境确认。
# 3) 启用 NVMe-oF/TLS(PSK)
# PSK 是预先共享的密钥,不需要证书:
# python3 scripts/rpc.py nvmf_subsystem_add_host <subnqn> <hostnqn> --psk /etc/spdk/psk.txt
# python3 scripts/rpc.py nvmf_subsystem_add_listener <subnqn> -t tcp -a <ip> -s 4420 --secure-channel
7.6 优雅停止与资源清理
停止顺序会影响数据安全。下面这个脚本按先断连接、再删 subsystem、最后停进程的顺序来:
#!/usr/bin/env bash
# /opt/spdk/conf/graceful-shutdown.sh
# 适用于本文的 malloc + NBD + NVMe-oF 拓扑。真实环境的 bdev 类型更多,删除步骤要按类型替换。
set -euo pipefail
RPC="/opt/spdk/scripts/rpc.py -s /var/tmp/spdk.sock"
echo "Step 1: 移除所有 NVMe-oF listener..."
$RPC nvmf_get_subsystems | jq -r '
.[] | select(.nqn != "nqn.2014-08.org.nvmexpress.discovery") |
.nqn as $nqn | .listeners[]? |
"\($nqn) \(.trtype) \(.traddr) \(.trsvcid)"
' | while read -r sub trtype traddr trsvcid; do
echo " $sub $trtype $traddr $trsvcid"
$RPC nvmf_subsystem_remove_listener "$sub" -t "$trtype" -a "$traddr" -s "$trsvcid" || true
done
echo "Step 2: 等待 initiator 断开或 I/O 排空..."
sleep 15
echo "Step 3: 删除 subsystem..."
$RPC nvmf_get_subsystems | jq -r '.[].nqn | select(. != "nqn.2014-08.org.nvmexpress.discovery")' |
while read -r sub; do
$RPC nvmf_delete_subsystem "$sub"
done
echo "Step 4: 停止 NBD 映射..."
$RPC nbd_get_disks | jq -r '.[].nbd_device' 2>/dev/null |
while read -r nbd; do
$RPC nbd_stop_disk "$nbd"
done
echo "Step 5: 删除 bdev(按实际类型替换命令)..."
$RPC bdev_malloc_delete Malloc0 2>/dev/null || true
# 真实 NVMe:$RPC bdev_nvme_detach_controller Nvme0
# RAID:$RPC bdev_raid_delete Raid0
echo "Step 6: 终止进程..."
$RPC spdk_kill_instance SIGTERM
8. 故障排查手册
8.1 常见问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
configure 报 --with-nvme-cuse requires libfuse3 |
缺 libfuse3-dev |
dpkg -s libfuse3-dev |
sudo apt install libfuse3-dev |
configure 报 autoreconf: not found |
缺 autotools | command -v autoreconf |
sudo apt install autoconf automake libtool |
make 报 meson: not found |
缺 DPDK 构建工具 | command -v meson ninja |
sudo apt install meson ninja-build |
make 报 CUnit/Basic.h: No such file |
缺 CUnit 头文件 | dpkg -s libcunit1-dev |
sudo apt install libcunit1-dev |
EAL: Not enough memory available |
hugepage 实际分配不足 | grep HugePages_ /proc/meminfo |
先 drop caches 再重设,或增加内存 |
NVMe-oF 连接被拒 does not allow host ... at this address |
listener 写了 0.0.0.0,或 host 不在 allowlist |
target 日志、nvmf_get_subsystems |
listener 写具体 IP,或用 nvmf_subsystem_add_host |
spdk_tgt 启动立即退出 |
hugepage 不足 | grep HugePages_ /proc/meminfo |
增加 vm.nr_hugepages |
spdk_tgt 启动报 EAL: No free hugepages |
hugepage 被其他进程占用 | sudo fuser /dev/hugepages/* |
释放或增加 hugepage |
spdk_tgt 报 Cannot bind to core X |
指定核心不存在或被隔离 | lscpu, cat /sys/devices/system/cpu/online |
调整 -m 核心掩码 |
rpc.py 连接失败 |
socket 路径不对或进程未启动 | ls -la /var/tmp/spdk*.sock |
确认 -r 参数与进程状态 |
| VFIO 绑定失败 | IOMMU 未启用 | dmesg | grep -i iommu |
配置 GRUB 启用 IOMMU(见 3.5) |
| NVMe-oF 连接超时 | 防火墙、IP 不通或 listener 未添加 | ss -tlnp | grep 4420 |
检查 listener 配置与网络 |
fio 报错 No such device |
NBD 设备未就绪 | lsblk | grep nbd |
确认 nbd_start_disk 已完成 |
| 延迟异常高 | 跨 NUMA 访问或 CPU 过载 | numastat, Reactor busy 比率 |
调整 NUMA 亲和(见 7.1) |
| 内存持续增长 | hugepage 泄漏(bdev 未正确删除) | grep HugePages_ /proc/meminfo 对比 |
确保所有 bdev/subsystem 正确清理 |
8.2 日志诊断
# 运行时调整日志级别(无需重启)
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock log_set_flag bdev
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock log_set_level DEBUG
# 查看当前日志标志
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock log_get_flags | jq
# 恢复默认
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock log_set_level NOTICE
sudo python3 scripts/rpc.py -s /var/tmp/spdk.sock log_clear_flag bdev
8.3 核心转储分析
# 确保核心转储已启用
ulimit -c unlimited
echo '/tmp/core.%e.%p' | sudo tee /proc/sys/kernel/core_pattern
# 如果 spdk_tgt 崩溃,使用 gdb 分析
gdb /opt/spdk/build/bin/spdk_tgt /tmp/core.spdk_tgt.<pid>
# (gdb) bt <- 查看调用栈
# (gdb) thread apply all bt <- 所有线程调用栈
9. 扩展阅读
| 资源 | 地址 | 说明 |
|---|---|---|
| SPDK 官方文档 | https://spdk.io/doc/ | API 参考、架构说明、示例代码 |
| SPDK GitHub | https://github.com/spdk/spdk | 源码、Issues、Release Notes |
| SPDK JSON-RPC 参考 | https://spdk.io/doc/jsonrpc.html | 所有 RPC 方法的详细参数说明 |
| NVMe 规范 | https://nvmexpress.org/specifications/ | NVMe Base / NVMe-oF 协议规范 |
| DPDK 文档 | https://doc.dpdk.org/ | EAL、hugepage、VFIO 底层原理 |
| fio 文档 | https://fio.readthedocs.io/ | I/O 基准测试工具完整参考 |
| SPDK NVMe-oF | https://spdk.io/doc/nvmf.html | target/initiator 配置与协议细节 |
| SPDK vhost | https://spdk.io/doc/vhost.html | QEMU/KVM virtio 后端 |
| SPDK blobstore | https://spdk.io/doc/blob.html | blobstore/blobfs 设计与 API |
| Ceph NVMe-oF Gateway | https://github.com/ceph/ceph-nvmeof | 基于 SPDK 的 RBD NVMe-oF 网关 |
| DAOS | https://github.com/daos-stack/daos | 用 SPDK 管理 NVMe 的 HPC/AI 存储 |
| SPDK CSI | https://github.com/spdk/spdk-csi | Kubernetes CSI 驱动 |
nvme-cli |
man nvme |
内核 NVMe 管理工具手册 |
附:最小复现脚本
以下脚本在任意 Debian 12 主机上从零完成 SPDK 编译、启动、malloc bdev 创建与 fio 验证,不需要 NVMe 硬件,不修改内核启动参数:
想在 Homelab 里验证,开一台 Debian 12 VM 就够:4 vCPU、8 GB 内存、32 GB 磁盘。
nbd和nvme-tcp使用 Debian 自带模块;如果modprobe nbd找不到模块,先装linux-modules-extra-$(uname -r)。VFIO/NVMe 直连需要额外的 NVMe 和 IOMMU;虚拟磁盘上的 fio 数字只能验证功能,不能代表 NVMe 性能。
#!/usr/bin/env bash
# spdk-quickstart.sh - Debian 12 SPDK 最小可复现验证
# 用法: sudo bash spdk-quickstart.sh
set -euo pipefail
echo "=== [1/7] Install dependencies ==="
apt update
apt install -y git build-essential meson ninja-build pkg-config \
python3 python3-venv python3-pip \
python3-pyelftools libnuma-dev libssl-dev libaio-dev libfuse3-dev \
uuid-dev libncurses-dev libcunit1-dev autoconf automake libtool \
jq curl fio nasm
echo "=== [2/7] Clone and build SPDK v24.09 ==="
cd /opt
[ -d spdk ] || git clone https://github.com/spdk/spdk.git
cd spdk
git checkout v24.09
git submodule update --init --recursive
./configure
make -j$(nproc)
echo "=== [3/7] Configure hugepages ==="
sysctl -w vm.nr_hugepages=1024
if [ "$(awk '/HugePages_Total/{print $2}' /proc/meminfo)" -lt 1024 ]; then
# 内存碎片或 page cache 占得多时,内核可能只分配一部分
sync
echo 3 > /proc/sys/vm/drop_caches
sysctl -w vm.nr_hugepages=1024
fi
mkdir -p /dev/hugepages
mount -t hugetlbfs nodev /dev/hugepages 2>/dev/null || true
grep HugePages_ /proc/meminfo
if [ "$(awk '/HugePages_Total/{print $2}' /proc/meminfo)" -lt 1024 ]; then
echo "hugepage 分配不足,需要 1024 页(2GB)" >&2
exit 1
fi
echo "=== [4/7] Start spdk_tgt ==="
# -u 跳过 PCI,纯 malloc 验证不需要 VFIO
build/bin/spdk_tgt -m 0x1 -s 1024 -u -r /var/tmp/spdk.sock &
SPDK_PID=$!
sleep 2
echo "spdk_tgt PID: $SPDK_PID"
echo "=== [5/7] Create malloc bdev and NBD mapping ==="
python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_create -b Malloc0 512 4096
modprobe nbd nbds_max=16
python3 scripts/rpc.py -s /var/tmp/spdk.sock nbd_start_disk Malloc0 /dev/nbd0
sleep 1
lsblk | grep nbd0
echo "=== [6/7] Run fio benchmark ==="
fio --name=spdk-verify \
--filename=/dev/nbd0 \
--ioengine=libaio \
--direct=1 \
--rw=randrw \
--rwmixread=70 \
--bs=4k \
--iodepth=32 \
--numjobs=1 \
--time_based \
--runtime=10 \
--group_reporting
echo "=== [7/7] Cleanup ==="
python3 scripts/rpc.py -s /var/tmp/spdk.sock nbd_stop_disk /dev/nbd0
python3 scripts/rpc.py -s /var/tmp/spdk.sock bdev_malloc_delete Malloc0
kill $SPDK_PID 2>/dev/null; wait $SPDK_PID 2>/dev/null || true
echo "=== SPDK quickstart verification PASSED ==="
总结
这篇的内容可以分成三块。原理部分讲 SPDK 为什么快、快在哪里,以及 Reactor 模型怎么影响调优;部署部分给出一套在 Debian 12 上能复现的编译、hugepage、VFIO 和 systemd 步骤;验证部分从 malloc/NBD 一路到跨主机 NVMe-oF,4.5 节有 1Gb 链路上的实测数字。
如果你的应用已经跑在内核文件系统上,只是想让 I/O 快一点,先看 io_uring。要做 NVMe-oF target、vhost-user 后端,或者自己控制数据面,再考虑 SPDK。它带来的性能要用独占 CPU、hugepage、VFIO 和更高的运维复杂度来换,先把这些成本算清楚,再决定要不要上。
如果你觉得这篇文章对你有所帮助,欢迎赞赏~
赞赏