本机 bdev 就绪之后,要把块设备送给远端主机,SPDK 走的是
NVMe over Fabrics target:规范里的
subsystem / namespace / 传输,落到库 lib/nvmf
与示例应用 app/nvmf_tgt。协议背景见站内 storage/03
的 NVMe-oF 节;本文只钉 SPDK 对象如何咬合
bdev,不重写规范全书。
本文是「SPDK / 用户态存储栈」系列第 8 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 7 篇 · bdev 模块 叶子与叠层 第 8 篇 · NVMe-oF target subsystem / listener / NS→bdev 第 9 篇 · 传输层 RDMA / TCP 与中断模式
版本锚定:SPDK v26.01;NVMe over Fabrics Target、NVMe-oF Target Programming Guide;源码
lib/nvmf/、app/nvmf_tgt/@ v26.01。官方写明与 Linux 内核 NVMe-oF target/host 做过互通测试。无伪造 discover/connect 输出。
本篇不展开:RDMA vs TCP 选型与 interrupt mode(第 9 篇)、initiator 与多路径(第 10 篇)、TLS/DH-HMAC-CHAP 完整安全菜谱(仅作边界提及)。
一、Target 在栈上的位置
官方 NVMe-oF Target Getting Started:SPDK NVMe-oF target 是用户态应用,把块设备经 Ethernet / InfiniBand / Fibre Channel 等 fabric 导出;当前文档主线支持 RDMA 与 TCP(FC 依赖厂商 LLD,另议)。
术语对齐(官方刻意贴近规范、并借用 iSCSI 习惯语):
| 用语 | 含义 |
|---|---|
| target | 导出 subsystem 的一侧(SPDK 文档用语;规范侧重 subsystem 等对象) |
| host | 连接过来的客户端(规范用语) |
| initiator | 与 host 同义的常见叫法(iSCSI 遗留) |
flowchart LR
host["Host / initiator"] --> xport["Transport RDMA/TCP"]
xport --> lis["listener"]
lis --> sub["subsystem"]
sub --> ns["namespace"]
ns --> bdev["bdev"]
bdev --> leaf["nvme/aio/malloc/..."]
关键不变量:namespace 就是 bdev(Programming Guide 原文:Namespaces are bdevs)。映射做错时,远端看到的是「空 subsystem」或错误容量,而不是「传输配错」——后者是 listener / transport 问题。
二、lib/nvmf
原始对象
NVMe-oF Target Programming Guide
列出库级原语(开发嵌入 lib/nvmf
时必须认全):
| 对象 | 规范/SPDK | 职责 |
|---|---|---|
spdk_nvmf_tgt |
SPDK 引入 | 一组 subsystem + 传输与连接的集合 |
spdk_nvmf_subsystem |
规范 subsystem | 含 namespace 与 controller,做访问控制 |
spdk_nvmf_ns |
规范 namespace | 绑定一个 bdev |
spdk_nvmf_transport |
规范 fabric 抽象 | RDMA/TCP/… 插件 |
spdk_nvmf_listener |
监听地址 | 接受新连接 |
spdk_nvmf_qpair |
规范 queue pair | 与网络连接 1:1 |
spdk_nvmf_poll_group |
SPDK 引入 | 可一起轮询的一组连接 |
spdk_nvmf_host |
host NQN | 访问控制条目 |
库与示例应用的分工:
lib/nvmf:实现创建 target、subsystem、把 I/O 落到 bdev 所需的全部逻辑;可被独立链进自有进程。app/nvmf_tgt:用 JSON-RPC 与 app framework 把上述对象暴露成可运维进程;入门文档里的scripts/rpc.py示例默认针对它。
嵌入式产品可以只链库、自建控制面;运维排障时仍建议先用
nvmf_tgt + RPC
建立心智模型,再映射到自有封装。
三、配置顺序:transport → subsystem → NS → listener
官方 Getting Started 用 RPC 给出最小闭环(语义级,不粘贴本机输出):
- 启动
nvmf_tgt(注意信号:文档要求尽量用 SIGTERM 释放共享内存;SIGKILL 可能导致手工清理)。 nvmf_create_transport:按传输初始化(RDMA/TCP 参数如 I/O unit、max I/O、in-capsule data 等)。- 准备 bdev(例如
bdev_malloc_create或bdev_nvme_attach_controller)。 nvmf_create_subsystem:NQN、序列号、是否 allow-any-host 等。nvmf_subsystem_add_ns:把 bdev 名挂进 subsystem。nvmf_subsystem_add_listener:传输类型、地址、服务端口(示例常用 4420)。
Programming Guide 对库 API
的状态机更严:subsystem 以 inactive 创建,经
spdk_nvmf_subsystem_start()
激活;运行中修改通常要 pause → 改 NS/host/listener →
resume。RPC 封装会隐藏部分步骤,但 「先有 bdev,再
add_ns;先有 transport,再 add_listener」
这一依赖不变。
NQN 校验:官方按规范形式化 EBNF,并强调最大长度等约束;UUID 形式 NQN 按字节比较、不分大小写折叠——文档举例大小写不同的 UUID NQN 在 target 侧不相等。配置生成器应固定小写十六进制。
Discovery subsystem 会随 target 自动创建;discover 流量与普通 I/O subsystem 分流,但都走同一套 poll group 接纳模型。
配置完成后应用「读回核对」:用 RPC 列出
transport、subsystem、NS、listener,确认 NQN、bdev
名、地址端口与设计文档一致,而不是只看进程还在。NQN
字节级比较与大小写陷阱(官方对 UUID NQN
的警告)特别适合在这一步抓出来。多租户场景再核对 host
允许列表是否最小权限——allow-any-host
能省事,也能把发现面扩到不可接受的范围。
四、数据面:poll group 与无锁 I/O
Programming Guide 对路径的描述可以直接当排障坐标系:
- 新连接被 listener 接纳后,自动分配到某个 poll group。
- poll group 在创建它的那条线程上轮询;也只应在该线程访问。
- 普通 NVM 命令(READ/WRITE 等)从收包到提交给 backing bdev,在同一线程走完;完成亦由该线程轮询 bdev/后端后回包。热路径不加跨线程锁。
- Admin / 管理类命令可能改 subsystem 全局状态:通过向各线程发 message,让 subsystem 短暂 pause,期间新 I/O 排队,变更后再 resume。管理命令稀少,因此宁可用 pause 协调,也不给每条 I/O 加锁。
flowchart TB
lis["listener accept"] --> assign["assign qpair to poll group"]
assign --> pg["poll group on thread N"]
pg --> nvm["NVM READ/WRITE"]
nvm --> bdev["spdk_bdev submit"]
bdev --> cpl["poll completion"]
cpl --> rsp["fabric response"]
pg --> adm["Admin / mgmt"]
adm --> pause["pause subsystem via messages"]
pause --> change["state change"]
change --> resume["resume + drain queued I/O"]
零拷贝:官方写明 RDMA 传输下数据在 NIC 与主机内存、主机内存与 SSD 之间移动,不在主机内存内再拷一次;其他传输未来(或实现)可能需要拷贝。TCP 路径的缓冲策略见第 9 篇,不在此用未测数字对比 RDMA。
CPU 与 NUMA:Getting Started 建议 NIC 与 NVMe 同
NUMA;-m core mask 决定 reactor 可用核。target
性能首先是 poll group
线程是否被同核杂务拖死,其次才是单条 RPC 参数。
4.1 和「把盘 export 出去」相关的失败分层
把一次远端 READ 拆开,失败可能落在完全不同的层:
| 层 | 症状直觉 | 先查什么 |
|---|---|---|
| 传输 / listener | 连不上、握手失败 | IP、端口、nvmf_create_transport、防火墙、RDMA
模块 |
| 访问控制 | 连上被拒 / 看不见 NS | host NQN、allow-any-host、listener 是否挂到该 subsystem |
| namespace 映射 | 容量为 0 或 I/O 全错 | add_ns 的 bdev 名是否存在、bdev
是否被其他消费者 claim |
| bdev / 叶子 | 部分 I/O 失败、reset | 第 6–7 篇的队列、后端健康、介质错误 |
官方还提醒:用信号杀进程时优先 SIGTERM,让应用释放共享内存;SIGKILL 可能导致资源残留需手工清理。这与「target 挂了再拉起来」的运维剧本直接相关——第 12 篇会谈状态保留边界,此处先把信号选择当成数据面纪律。
嵌入自有控制面时,Programming Guide 的状态机比 RPC 示例更值得当成规范:inactive / paused / running 的切换决定何时允许改 NS 与 host 表。把「线上直接改」理解成「任意时刻 mutate」,会在 I/O 仍涌入时撞上 pause 排队与管理命令稀有但昂贵的现实。产品若提供热加卷,应把 pause 窗口暴露成可观测事件,而不是藏进 RPC 成功率。
五、访问控制与安全边界(机制级)
默认:subsystem 不接受任意 host,也不在任意已建立
listen 地址上自动放行——需
nvmf_subsystem_add_host /
add_listener(库 API
同名语义)。allow-any-host
是显式开关,与安全加固选项可能互斥(例如官方 TLS
节:--secure-channel 与
--allow-any-host 互斥)。
v26.01 起,changelog 写明 target 支持 NVMe 2.0 所需特性集合,并增强 Identify CNS、可选命令掩码等;安全相关还有 TCP TLS(文档标 experimental)、DH-HMAC-CHAP 带内认证等。本篇只强调:认证/加密挂在 host 与 listener 配置上,不改变 NS→bdev 映射本身。
NSSR(NVM Subsystem Reset)与固件更新在 Getting Started 后半有单独章节:NSSR 默认关闭,开启后会影响整个 subsystem 乃至共享同一底层 NVMe 的其他导出;固件 download/commit 后设备不会自动从系统消失,复位策略要运维显式设计。这些不是日常 I/O 热路径,但属于「target 角色」的管理面边界——把它们误当成「只影响单个 namespace」会在共享叶子 bdev 时放大爆炸半径。
子系统创建时的序列号、型号字符串与最大命名空间数等字段,决定了 initiator 侧 Identify 看到的身份信息。它们不改变 I/O 热路径,却影响自动化发现与监控标签是否稳定。生产上应把这些字符串当成 API 契约:随意改 SN 会导致监控系统把同一导出认成新设备。v26.01 对 NVMe 2.0 Identify 相关 CNS 的补齐,也意味着旧脚本若只解析部分 Identify 页面,升级后应回归一次发现与容量核对,而不是假设「能 connect 就够了」。
把 bdev 名写进 namespace 之后,容量、块大小与 unmap 能力都随该 bdev 走。换底层叶子(例如从 malloc 换成 nvme)时,若仍复用同一 bdev 名,initiator 可能只看到「同一设备变慢或变大」;若改名,则表现为新设备。命名策略应在控制面设计阶段定死,避免把调试期的临时名带进生产导出表。
六、谱系、争论与开放问题
谱系:NVMe-oF 把 NVMe 队列模型绑到多种
fabric(见 storage/03);SPDK
用用户态 poll group + bdev 实现 target,与内核
nvmet 形成两条实现线。官方明确做过与内核
host/target 的互通测试。用户态路线的卖点不是「多一个 export
工具」,而是与 reactor、bdev、可选 vhost
组成同一进程内的数据面——这是本系列 08–11 连读的原因。
争论:控制面用 JSON-RPC 热改 subsystem,还是像装置固件一样静态配置。RPC 灵活,但 pause 窗口与「配置与数据面谁先谁后」是事故高发区;静态配置可复现,却不利于多租户热加卷。另一条争论是「每个租户一个 subsystem」还是「大 subsystem 多 namespace」:前者隔离清晰、listener/NQN 管理成本高;后者省对象、blast radius 更大。SPDK 原语两种都允许,产品必须二选一写进规范。
开放问题:
- Admin pause 与长尾:大规模连接下 subsystem pause 的扇出延迟如何度量与限流,仍缺少与业务 SLO 绑定的标准做法。
- NSSR / 固件升级与多 subsystem 共享同一 bdev_nvme:官方 NSSR 文档警告跨 subsystem 副作用——生产编排是否允许共享叶子 bdev,值得写成显式策略。
- Discovery 过滤与多租户暴露面:v26.01
增加
spdk_nvmf_set_custom_discovery_filter();策略引擎与审计模型仍属产品层空白。 - poll group 与 bdev channel 的亲和策略:连接哈希到 poll group 之后,如何保证其提交的 bdev channel 仍落在同核、同 NUMA,缺少一键式官方配方,常靠部署约定。
七、参考资料
规范 / 官方文档(A)
- SPDK v26.01,NVMe over Fabrics Target(Getting Started、RPC 配置序、与内核互通声明、信号与共享内存)。
- SPDK v26.01,NVMe-oF Target Programming Guide(原语、poll group、无锁 I/O、pause 协调、RDMA 零拷贝)。
- SPDK v26.01,Changelog(nvmf:NVMe 2.0 必需特性、Identify CNS 07h、discovery filter API 等)。
源码(A)
spdk/spdktag v26.01:lib/nvmf/、app/nvmf_tgt/、include/spdk/nvmf.h。
站内
实验台账
- 本篇无本机
nvme discover/ RPC 输出;命令仅作官方语义复述。
以上机制足以支撑「把本地 bdev 正确导出」;剩下的延迟与 CPU 形态,取决于传输与轮询策略,而不是再改一次 NQN 字符串。
八、小结
lib/nvmf实现 target;nvmf_tgt是 RPC 化示例进程。- namespace = bdev;listener 管「从哪进」,subsystem 管「谁能看哪些 NS」。
- 普通 I/O 在单 poll group 线程无锁走完;管理命令用 subsystem pause 协调。
- 传输怎么选、低负载如何少空转——见 第 9 篇。
附:和内核
nvmet 对照时该问的三句话
读内核文档或发行版「NVMe-oF target」howto 时,用三句话对齐 SPDK,避免概念错位:
- 配置面在哪? 内核多在
configfs/
nvmetcli;SPDK 在进程内 JSON-RPC 与lib/nvmf状态机。 - 数据面线程模型? 内核走内核线程与中断/软中断习惯;SPDK 默认 reactor 轮询,可选 interrupt mode(第 9 篇)。
- 块设备从哪来? 内核 target 挂内核块设备;SPDK namespace 必须是 bdev——这是本系列 06–08 的接缝。
互通测试证明「能连」,不证明「运维模型相同」。把内核
runbook 原样贴到 nvmf_tgt
上,是最常见的跨实现误迁移。
→ 系列目录 · 上一篇:bdev 模块边界 · 下一篇:传输层
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【SPDK 用户态存储】用户态存储全景:从 Reactor 到 NVMe-oF 的坐标系
定位 SPDK 相对内核块层、O_DIRECT+io_uring 与 NVMe 协议百科的生态位;钉住 I/O 路径、线程模型、设备独占、块抽象、远端传输五条坐标系,并给出 16 篇阅读路线。
【SPDK 用户态存储】bdev 核心:统一块接口、I/O 生命周期与排队完成
把 SPDK bdev 钉成用户态块栈:相对 Linux block 层统一了什么、砍掉了什么;一次 I/O 从提交、多无锁队列排队到完成回调的落点;队列满与 reset 的失败语义。
【SPDK 用户态存储】bdev 模块边界:nvme / aio / malloc 与何时不该叠层
按后端与虚拟层两类划清 SPDK bdev 模块边界:nvme、aio、malloc、null、raid、lvol 各自保证什么;说明叠层何时伤害延迟与排障,并拒绝做成模块百科。
【SPDK 用户态存储】NVMe-oF 传输层:RDMA vs TCP,轮询与 v26.01 中断模式
对比 SPDK NVMe-oF 的 RDMA 与 TCP 传输约束;说明默认轮询模型,并依据 v26.01 changelog 与官方 Interrupt Mode 文档钉住低负载 RDMA/TCP 中断驱动模式的适用边界。