站内 NVMe 协议 已讲清接口与 NVMe-oF 扩展;O_DIRECT 与 io_uring 与 io_uring 系列 覆盖仍走内核的高 IOPS 提交路径。云块 / 新硬件 提到 DPU 与卸载。读者仍缺一角:驱动与块栈搬进用户态之后,一次 I/O 如何经 reactor → NVMe qpair → bdev →(可选)NVMe-oF/vhost,以及相对内核路径各自保证什么、丢掉什么。
本文是「SPDK / 用户态存储栈」系列第 1 篇,只做三件事:
- 画出 SPDK 相对内核 blk / io_uring / 协议百科的生态位与缺口。
- 钉住后续章节共用的五条坐标系,并交代用户态轮询存储栈的设计谱系与开放争论。
- 给出与站内系列的分工,以及 16 篇阅读路线。
本文是「SPDK / 用户态存储栈」系列第 1 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 1 篇 · 用户态存储全景 生态位、主线坐标系、系列路线 第 2 篇 · Reactor event framework、poller、message passing 第 3–5 篇 · 本机路径 绑盘 → NVMe 驱动 → DMA/mempool 第 6–8 篇 · 块与远端 bdev → NVMe-oF target
版本锚定:SPDK v26.01(LTS;官方每年一月发 LTS)。官方 Getting Started / Event Framework / NVMe Driver / DMA From User Space 与源码树 tag v26.01。本篇以定位与路线为主;无本机直通 NVMe 则不粘贴伪造 fio / RPC / setup 输出。非 LTS 特性(如仅出现在 v26.05+)须显式标注。
一、生态位:本系列补哪一角
内核 NVMe、O_DIRECT+io_uring、SPDK
常被塞进同一张「高性能块
I/O」表,但默认把什么当一等公民不同——格子不是互斥,而是故障归因时先看哪一层。
flowchart TB
subgraph paths ["Block I/O paradigms"]
KER["Kernel NVMe + blk<br/>interrupt / softirq"]
URING["O_DIRECT + io_uring<br/>kernel submit path"]
SPDK["SPDK userspace<br/>polled stack"]
DPU["DPU / offload<br/>host-side remnant"]
end
subgraph docs ["Site coverage"]
S03["storage/03<br/>NVMe protocol"]
S79["storage/79<br/>Direct + io_uring"]
THIS["This series<br/>userspace stack"]
end
S03 -.->|"interface map"| KER
S03 -.->|"NVMe-oF wire"| SPDK
S79 --> URING
THIS --> SPDK
KER -.->|"same SSD, different owner"| SPDK
URING -.->|"contrast completion model"| SPDK
SPDK -.->|"when host stack shrinks"| DPU
| 形态 | 一等公民 | 典型代价中心 | 站内入口 |
|---|---|---|---|
| 内核 NVMe + block | 中断 / softirq、通用块层 | 系统调用与共享调度税 | storage/03 协议侧 |
| O_DIRECT + io_uring | 内核异步提交环 | 仍经内核驱动与块层语义 | storage/79、io_uring |
| SPDK 用户态栈 | reactor / poller + 用户态驱动 + bdev | CPU 轮询税、设备独占、运维共置 | 本系列 |
| DPU / 卸载 | 网卡/智能网卡侧 target | 主机侧还剩什么 | storage/73 |
一句话:本系列占住「用户态轮询存储栈」那一格——不重写 NVMe 规范全书,也不复述内核提交路径。
主线引擎取 SPDK v26.01:文档与源码 tag 对齐,适合做「从绑盘到 NVMe-oF」的连续拆解。Ceph OSD、MinIO、云盘产品内核不进正文;仅在选型边界出现。
读者若只带走一句:会跑 hello_world
只是地图入门;能指出延迟与失败落在五条轴的哪一条,才算进入本系列的能力模型。
二、缺口:相对 03 / 79 / io_uring 还缺什么
| 已有内容 | 视角 | 本系列补什么 |
|---|---|---|
| storage/03 | NVMe / NVMe-oF / ZNS 协议与接口演进 | 用户态驱动与 target 实现路径;不重写规范全书 |
| storage/79 | Direct I/O + io_uring 组合 | 对照「绕过内核块层」的另一条路 |
| io_uring、os/57 | 内核异步提交模型 | 对照句:完成通知 vs 用户态轮询 |
| storage/70、73 | 云块、DPU 展望 | 机制前提;不写厂商黑盒 SDK |
| linux/zero-copy | socket ZC 权衡 | 边界外链;本系列焦点在块/NVMe |
结论:读者知道 NVMe 多队列、会配
O_DIRECT+io_uring,但不知道 SPDK
如何绑盘、reactor 上阻塞意味着什么、bdev
队列满如何失败、NVMe-oF RDMA/TCP 与内核 initiator
如何对接。
与 HAProxy 第 1 篇 / Envoy 第 1 篇 对称:那边钉的是代理数据面;这边钉的是存储数据面换宿主之后的失败层。
三、设计谱系:用户态轮询存储从哪分叉
3.1 工作定义
本系列默认的 SPDK 用户态存储栈:
- env:大页、CPU 亲和、VFIO/UIO 绑盘——把 PCI 设备从内核驱动挪到进程可见。
- reactor / poller:每核事件循环;完成靠轮询,而不是默认依赖中断唤醒业务线程。
- 用户态 NVMe 驱动:映射 BAR、提交 SQ、轮询 CQ;库本身不拉线程(官方 NVMe Driver:entirely passive)。
- DMA / mempool:可 DMA、可翻译物理/IOVA 的缓冲;零拷贝的前提条件。
- bdev:统一块设备抽象;上面叠模块,下面接 nvme / aio / malloc 等。
- NVMe-oF / vhost / RPC:远端与虚拟化边界、以及 JSON-RPC 控制面。
这与「内核里开一个
/dev/nvme0n1」共享「块设备」一词,但运行时不变量不同:SPDK
把设备所有权与完成模型做成用户态一等路径。
3.2 学术与工程谱系(本系列回指)
- 用户态驱动 + 轮询:与 DPDK
同宗——把设备从内核驱动解绑,用 hugepage / VFIO 做 DMA
安全内存与设备访问;存储侧把同一范式落到 NVMe。官方 DMA
From User Space 明确:缓冲必须经
spdk_dma_malloc()(或其兄弟)一类路径分配。 - Message passing 取代热路径锁:官方 Message Passing and Concurrency 把目标写成「随硬件近似线性扩展」,并指出传统共享堆 + 锁在核数上升后会把时间耗在争用上;SPDK 倾向把状态钉在单线程,跨核用「函数指针 + 上下文」消息。Erlang / Go 的消息并发是谱系参照,不是 SPDK 运行时依赖。
- 被动库 + 可选 app 框架:NVMe
驱动可单独链接进任意事件循环;
lib/event的 reactor/spdk_app_start()是示例与多数 SPDK 应用的默认宿主,不是唯一宿主(Event Framework:framework is optional)。
谱系收束句:产品层争论「要不要上用户态」;内核层争论「轮询 CPU 税与设备独占是否换得起延迟与确定性」。 后者才是本系列主战场。
3.3 开放争论(先立靶,后各章拆)
| 争论 | A 侧 | B 侧 | 本系列落点 |
|---|---|---|---|
| 轮询 vs 中断 / 内核完成通知 | 尾延迟更可控、少一次调度 | 空闲时 CPU 税高 | 第 2、13、15 篇;26.01 起部分 interrupt mode 边界见第 9 篇 |
| 用户态独占 vs 内核共置 | 绕过块层、进程内零拷贝 | 同机工具链、热升级、容器共享盘变难 | 第 3、12、14–16 篇 |
SPDK vs O_DIRECT+io_uring |
用户态栈端到端 | 内核路径够用、运维面小 | 第 15–16 篇 |
| RDMA vs TCP(NVMe-oF) | 延迟与卸载 | 部署面与兼容性 | 第 9–10 篇 |
| bdev 叠层深度 | 功能组合快 | 每层排队与回调税 | 第 6–7 篇 |
四、五条坐标系(后续章节回指)
后面每一篇都会落到下面某一条轴上。第 1 篇只钉名字,不展开源码。
4.1 I/O 路径轴
app / bdev → (optional) nvmf/vhost
→ NVMe qpair submit → device DMA
→ poll CQ / poll group → completion callback
延迟与失败的归因必须先回答:卡在绑盘、提交、轮询完成、bdev 排队,还是传输层? → 第 2–10、14 篇。
4.2 线程与消息轴
Reactor 每核一条循环;跨核协调走 event /
spdk_thread_send_msg() 一类消息,而不是在 I/O
热路径上抢全局锁。在 reactor 上阻塞等于饿死同核所有
poller。→ 第 2 篇。
4.3 设备独占与 env 轴
scripts/setup.sh 分配大页并把 NVMe
从内核驱动解绑到 VFIO/UIO;setup.sh reset
再绑回。独占换来的是用户态可见性,代价是同机内核路径暂时不可用。→
第 3 篇。
4.4 块抽象轴(bdev)
bdev 统一「块设备」读写与完成回调;模块决定后端是本机 NVMe、aio、内存盘还是 RAID/lvol。相对 Linux block 层,砍掉的是通用调度与大量内核语义,留下的是用户态队列与回调契约。→ 第 6–7 篇。
4.5 远端与控制面轴
NVMe-oF target 把远端命令落到本地 bdev;传输(RDMA/TCP)与 initiator 互通决定「线缆另一端是谁」。JSON-RPC 管生命周期与热插拔,不管热路径 IOPS。→ 第 8–12 篇。
五、与站内系列的分工
| 站内内容 | 负责 | 本系列不重复 |
|---|---|---|
| storage/03 | 协议与接口演进 | 用户态实现路径 |
| storage/79 | Direct + io_uring 组合 | 内核提交细节 |
| io_uring | 内核异步模型 | 对照句即可 |
| storage/73 | DPU / 新硬件展望 | 不写 DOCA 教程 |
| distributed/38 | Ceph / RADOS | 用户态 OSD 栈仅选型边界 |
| linux/zero-copy | socket ZC | 块路径 DMA 约束在第 5 篇 |
读法:只要「本机数据库盘用不用 O_DIRECT+io_uring」→ 79 足够;要「用户态 target 卡在哪、为何 CPU 空转」→ 本系列 01 → 02 → 04 → 06 → 08 → 14。
写给架构师的一句:若组织还没有「设备独占 + 专用轮询核」的运维编制,先读第 3、15、16 篇再决定是否开用户态栈——机制再漂亮,也填不平编制缺口。
本系列后半(观测、排障、选型)会反复回指这五条轴;若第 1 篇只当目录略过,后面读到「卡在独占轴」会对不上号。建议至少把第三节争论表与第四节坐标名留下书签。
六、16 篇地图与阅读建议
| 部分 | 篇 | 一句话 |
|---|---|---|
| 运行时与本地 NVMe | 01–06 | 从坐标系到 bdev 核心 |
| 模块、远端与虚拟化 | 07–12 | 叠层、NVMe-oF、vhost、RPC |
| 观测与收束 | 13–16 | 口径、排障、对照内核、选型 |
最小路径:01 → 02 → 04 → 06 → 08 →
14。
本机用户态盘:再补 03 → 05 → 07。
做路径选型:01 → 15 → 16,并外链 03 / 79 /
io_uring。
本篇不展开 lib/nvme/
函数表,也不给出跨方案延迟排名——那需要声明环境的实测;无直通硬件时,只保留机制与失败模式。
七、一次 I/O 在用户态栈里「停」在哪(预览)
把五条轴压成排障时的第一问,避免一上来就怀疑固件:
| 停顿表象 | 先落哪条轴 | 后续篇 |
|---|---|---|
| 进程起不来 / 看不到盘 | 设备独占与 env | 03 |
| CPU 单核打满但 IOPS 上不去 | 线程与消息(空转 poller、错误绑核) | 02、13 |
| 提交成功、回调迟迟不来 | I/O 路径(未泵 CQ / poller 被堵) | 02、04 |
| 回调报错或队列满 | 块抽象(bdev 排队)或驱动队列深度 | 06–07、14 |
| 远端超时、本地盘却空闲 | 远端与控制面(传输 / listener / mapping) | 08–10、14 |
| 同机其它进程打不开盘 | 独占轴的预期副作用,不是「盘坏了」 | 03、15 |
这张表刻意不含延迟微秒数:它是坐标系,不是 benchmark。读者若只能记住一件事——先点名轴,再下钻模块——第 1 篇就够本。
与内核路径的对照句也适合放在这里一次说清:io_uring 的完成仍由内核驱动与块层语义托底;SPDK 把托底换成「你的 reactor 是否还在跑、你的缓冲是否 DMA 安全、你的设备是否仍在 VFIO 上」。保证集变窄,归因路径变短,但运维面变锋利。
学术谱系上,用户态轮询 I/O 与高频交易/NFV 叙事共享「用 CPU 换尾延迟确定性」的假设;假设不成立的场景(混部严重、昼夜负载差大、核是租来的)会把同一设计打成成本中心。本系列后文用机制拆这些场景,不在第 1 篇宣布胜负。
八、参考资料(本篇)
规范 / 官方文档(A)
- SPDK, Getting Started @
v26.01(
setup.sh/ hugepage 语义入口)。 - SPDK, Event Framework @
v26.01(reactor / event / poller /
spdk_app_start)。 - SPDK, Message Passing and Concurrency @ v26.01。
- SPDK, NVMe Driver @ v26.01。
- SPDK, Direct Memory Access (DMA) From User Space @ v26.01。
- SPDK, Releases / changelog:LTS 线与 v26.01 边界。
源码(A)
spdk/spdktag v26.01:lib/event/、lib/nvme/、lib/bdev/、lib/nvmf/、scripts/setup.sh(后续篇钉具体符号)。
站内对照
实验台账
- 本篇无命令输出;不写 IOPS / 延迟数字。
九、小结
- SPDK 的生态位是用户态轮询存储栈,不是「内核 NVMe 的另一个配置方言」。
- 五条坐标系(I/O 路径、线程消息、设备独占、bdev、远端控制面)是后续排障的共用语言。
- 与 03 / 79 的分工:协议与内核组合路径归百科;本系列回答「搬进用户态之后卡在哪一层」。
- 开放争论先立靶:CPU 税、独占运维、与 io_uring 的边界——各章用机制拆,不提前做未实测排行榜。
- 排障第一问是「停在哪条轴」,不是「先重跑 fio」。
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
SPDK / 用户态存储栈:从 Reactor 到 NVMe-oF
补齐站内 NVMe 协议与 O_DIRECT/io_uring 组合路径之上的用户态轮询存储层:reactor、绑盘、NVMe 驱动、bdev、NVMe-oF 与 vhost,并以排障与选型收束。
【SPDK 用户态存储】排障坐标系:绑盘、空转、队列满、传输超时与多路径
把 SPDK 常见症状映射到绑盘/环境、reactor CPU、bdev 队列、传输超时、多路径五轴;给出可核对清单与观测入口,不虚构延迟数字与 RPC dump。
【SPDK 用户态存储】bdev 核心:统一块接口、I/O 生命周期与排队完成
把 SPDK bdev 钉成用户态块栈:相对 Linux block 层统一了什么、砍掉了什么;一次 I/O 从提交、多无锁队列排队到完成回调的落点;队列满与 reset 的失败语义。
【SPDK 用户态存储】bdev 模块边界:nvme / aio / malloc 与何时不该叠层
按后端与虚拟层两类划清 SPDK bdev 模块边界:nvme、aio、malloc、null、raid、lvol 各自保证什么;说明叠层何时伤害延迟与排障,并拒绝做成模块百科。