第 6 篇 把 bdev 核心钉成统一提交与排队契约。真正决定延迟形状与失败模式的,是挂在契约之下的 模块:有的直接碰硬件或内核,有的只在已有 bdev 上再虚拟一层。官方 Block Device User Guide 列出的模块远多于生产常用集合;本文只划 边界与选型轴,不做成 RPC 百科。
本文是「SPDK / 用户态存储栈」系列第 7 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 6 篇 · bdev 核心 统一接口与生命周期 第 7 篇 · bdev 模块 后端 / 虚拟层边界与叠层代价 第 8 篇 · NVMe-oF target namespace 映射到 bdev 上一篇:bdev 核心 · 下一篇:NVMe-oF target
版本锚定:SPDK v26.01;Block Device User Guide 中 nvme / aio / malloc / null / raid / lvol 等章节;源码
module/bdev/@ v26.01。无伪造基准测试。
本篇不展开:crypto / OCF / RBD / DAOS 等可选模块的完整配置;blobstore 内部实现;把结果导出成内核 NBD 的运维菜谱。
一、两类模块:后端 vs 虚拟层
官方区分 bdev module(对接某类存储)与 vbdev module(在已有 bdev 上再创建块设备)。排障时先问:出错的名字是 叶子后端,还是中间的虚拟层?
flowchart TB
subgraph leaf ["Leaf backends"]
nvme["bdev_nvme"]
aio["bdev_aio"]
uring["bdev_uring"]
malloc["bdev_malloc"]
null["bdev_null"]
end
subgraph virt ["Virtual layers"]
raid["bdev_raid"]
lvol["bdev_lvol"]
gpt["bdev_gpt"]
pass["passthru / delay / ..."]
end
front["nvmf / vhost / app"] --> virt
virt --> leaf
front --> leaf
| 角色 | 代表模块 | 一句话边界 |
|---|---|---|
| 用户态 NVMe 叶子 | bdev_nvme |
PCIe 或 NVMe-oF 控制器 attach 后导出 namespace bdev |
| 内核旁路叶子 | bdev_aio / bdev_uring |
经 Linux AIO 或 io_uring 访问内核块设备 /
文件(O_DIRECT) |
| 测试叶子 | malloc / null |
大页 ramdisk;或丢弃写、读未定义数据的空靶 |
| 虚拟聚合 | raid |
多成员拼 RAID0 / 1 / 5F / concat |
| 虚拟空间管理 | lvol |
在 lvstore(基于 blobstore)上切逻辑卷 |
| 自动分区 | gpt |
识别 SPDK GPT 分区类型并导出分区 bdev |
前端(尤其 nvmf_subsystem_add_ns)只认
最终要导出的那个 bdev
名。叶子与虚拟层可以叠,但每多一跳就多一处队列、完成回调与错误翻译。
二、叶子模块:保证什么、不保证什么
2.1 bdev_nvme
- 保证:把 用户态 NVMe
驱动 的 namespace 暴露成
bdev;
bdev_nvme_attach_controller同时覆盖本机PCIe与远端RDMA/TCP等 transport(官方 User Guide 示例)。 - 不保证:替你解决多路径策略细节——驱动侧有 multipath 能力,但 ANA / 策略 RPC 与故障切换观测要单独设计(第 10、14 篇)。
- 典型失败:绑盘/VFIO 未就绪、远端 listener 不可达、qpair 资源耗尽。表现在 bdev 层是 attach 失败或 I/O 完成错误,根因往往在驱动或传输。
2.2 bdev_aio 与
bdev_uring
官方对 AIO 模块的定位很明确:通过 Linux AIO
访问内核块设备或文件,且使用
O_DIRECT,绕过页缓存;并写明这是「在不用用户态驱动的前提下,用户态
target 最接近典型内核 target 的模式之一」。
- 适用:盘仍归内核驱动、只想让 SPDK 前端(nvmf/vhost)复用;或过渡迁移、同机共置约束下无法 VFIO 独占。
- 代价:完成路径回到内核提交/完成模型,与纯轮询用户态 NVMe 不是同一延迟分布;CPU 账本里会同时出现 SPDK reactor 与内核块/中断层。
- uring
变体:
configure --with-uring后经 io_uring 下发;与站内 io_uring 系列、storage/79 对照时,记住:这里 uring 是 bdev 叶子,不是让数据库进程自己提 io_uring。
v26.01 changelog 对 bdev_aio
注明:默认不再使用 RWF_NOWAIT,需显式
--no-wait。版本相关行为以 changelog
为准,避免从旧博客抄默认值。
2.3 malloc 与
null
- malloc:从 SPDK 大页内存切出的易失
ramdisk;适合功能验证、nvmf 连通性、CI。官方示例
bdev_malloc_create。 - null:丢弃写、读内容未定义;官方用途是「用最小块设备开销压测其余 bdev I/O 栈」以及构造 malloc 不易模拟的配置。不要把 null 上的「很高 IOPS」写成介质性能结论——它测的是栈与轮询开销上界,不是盘。
叶子选型还有一条常被忽略的轴:失败是否可归因到硬件。
bdev_nvme 的完成错误往往能对照 NVMe
status;aio/uring 则混有内核 errno 与块层语义;malloc/null
几乎不教你「盘坏了怎么办」。若目标是练排障剧本,应用真实
nvme 叶子(或至少 aio 指向真实设备),而不是长期停留在
malloc。
三、虚拟层:raid 与 lvol
3.1 RAID
User Guide:RAID vbdev 可把任意 SPDK bdev
组合成一个 RAID bdev;当前支持
RAID0、Concat、RAID1、RAID5F(RAID5F 需
--with-raid5f)。带冗余级别支持降级与
rebuild;可选把 metadata
落在成员盘上,避免进程重启后手工重建。
边界:
- RAID0 / concat:条带或拼接,无冗余;成员丢失即数据不可用。
- RAID1 / 5F:有重建与降级语义;重建吃带宽与 CPU,和「用户态低延迟」目标可能冲突——这是产品策略问题,不是模块 bug。
- 允许成员尚未存在就创建 RAID 名,待成员注册后 claim——运维上要防「名字在、数据面未齐」的窗口。
3.2 lvol
逻辑卷建立在 lvol store 之上,而 lvol
store 又建在某个 base bdev 上,底层是 Blobstore(官方指向
Logical volume / Blobstore 文档)。RPC
顺序是:bdev_lvol_create_lvstore →
bdev_lvol_create。
边界:
- 得到的是可变大小的虚拟 bdev,适合多租户切分;
- 元数据与 cluster 分配引入额外故障面(lvstore 损坏、空间耗尽);
- 对延迟敏感的单租户「整盘直出」路径,常常 不需要 lvol。
GPT 模块则更「被动」:识别特定 partition type GUID 后自动导出分区 bdev;改分区表常需经 NBD 暴露给内核工具再重新 attach——细节见官方 GPT 节,本篇不展开菜谱。
对「要不要上
lvol」可以用更硬的产品问题代替性能测试口号:是否需要在同一块物理介质上切出多个可独立扩缩的卷、是否需要快照类能力(若走
blobstore/lvol 生态)、以及元数据损坏时的恢复演练是否已写进
runbook。若三个答案都是「暂时不需要」,整盘
bdev_nvme 直出通常更干净。RAID
同理:先回答冗余与条带是否在
本进程的用户态栈
解决,还是应在上层集群/客户端解决;把集群级复制再叠一层
RAID1,常常只是重复交税。
四、何时不该叠层
叠层伤害来自三类,而不是「虚拟层原罪」:
- 额外跳数:每次 vbdev 完成回调多一跳;crypto 类写路径还可能引入临时缓冲(官方 crypto 节说明写用 scratch,避免污染源缓冲)。无证据时不要编造微秒数,但要承认跳数与拷贝是真实成本来源。
- 错误语义变长:叶子返回的介质错误,经 RAID/lvol 翻译后,上层看到的是另一套状态;排障必须从导出 bdev 名 向下展开依赖树。
- 资源与重建策略冲突:RAID rebuild、lvol 元数据写、远端 NVMe-oF 重传同时抢同一组核与网卡时,轮询栈没有内核 elevator 帮你「公平」。
实用排除:
| 目标 | 更稳的形状 | 慎用 |
|---|---|---|
| 最低延迟本机盘 | bdev_nvme 直出 |
null/malloc 当「假盘」却当生产结论 |
| 快速验证 nvmf | malloc → subsystem NS | 未测完就上 RAID5F+lvol+crypto |
| 盘仍在内核 | aio/uring 叶子 | 再叠多层 vbdev「顺便」做完整存储产品 |
| 多租户切分 | 单层 lvol 或单层 RAID | lvol 上再套 RAID 再套 crypto 却无观测 |
争论:存储产品倾向「功能完备的虚拟块栈」;数据面倾向「能少一层是一层」。SPDK 同时提供两边工具,默认并不替你做产品裁剪。本系列立场:前端需要的能力用 最少虚拟层 表达;把「将来可能要的功能」提前叠进热路径,是常见过度设计。
五、配置与依赖:先画树,再写 RPC
模块边界一旦清楚,配置顺序就变成依赖图问题,而不是「RPC 清单抄写」:
- 先创建叶子(attach NVMe、create
malloc/aio),确认
bdev_get_bdevs一类枚举里名字存在; - 再挂需要全员到齐的虚拟层(RAID 可延迟 claim,但导出前必须成员齐;lvol 必须先有 lvstore);
- 最后把顶层 bdev
名交给前端(
nvmf_subsystem_add_ns、vhost 等)。
反例:先 add_ns 再创建底层、或把 crypto
建在即将被 RAID
替换的临时名上,会导致前端持有陈旧名字、热重启后映射漂移。第
12 篇谈 JSON-RPC
生命周期时会回到「配置与数据面谁先谁后」;本篇只要求:每一条
RPC 都对应依赖树上的一条边。
passthru / delay 这类模块适合当教学与注入延迟的靶,不适合出现在生产热路径叙事里。官方写明 delay 不是高保真盘模拟,而是「看额外延迟如何冲击应用」的粗粒度工具;null 则是「测栈开销上界」。把它们留在实验台账,避免写进架构图冒充介质层。
可选模块(RBD、OCF、DAOS、xNVMe、virtio 发起端等)各自引入外部运行时或许可边界:例如 RBD 要权衡「每 bdev 一个 cluster 连接」与「共享 cluster 对象」的线程模型(官方 User Guide 明确点出 librados 线程与上下文切换张力)。它们证明 bdev 插件面足够宽,但 宽不等于你的产品默认该全开。本系列主干仍是 nvme 叶子 ± 必要时一层虚拟层 → nvmf/vhost。
5.1 一张「允许的形状」备忘
| 形状 | 何时合理 | 主要代价 |
|---|---|---|
nvme → nvmf NS |
本机用户态盘导出 | 设备独占、绑盘运维 |
aio/uring → nvmf NS |
盘留在内核,只要用户态 target | 完成模型混杂、延迟谱不同 |
malloc → nvmf NS |
联调、CI | 无持久性 |
nvme → lvol → nvmf |
多租户切分 | 元数据与空间耗尽 |
nvme×N → raid → nvmf |
进程内需要冗余/条带 | 重建与降级吃资源 |
nvme → raid →
lvol → crypto → nvmf |
极少作为默认;需强产品理由 | 跳数、错误翻译、观测盲区叠加 |
表的最后一行不是「禁止」,而是 默认拒绝,除非依赖图、观测与故障演练齐备。第 8 篇开始的 nvmf 配置,应默认从上表前几行长出来。
六、开放问题
- ZNS / 分区命名空间在通用 bdev + RAID/lvol 上的组合语义仍不完整:条带与 zone 约束如何共存,缺少与经典块 RAID 同级的社区共识实现。
- 内核旁路叶子(aio/uring)与用户态 NVMe 叶子混部时,统一 QoS 与超时模型缺失——第 15 篇会对照,但生产策略仍靠部署约定。
- vbdev 可观测性:第 13 篇的 iostat 若只看顶层 bdev,会吞掉下层重建与元数据写;如何默认导出依赖图仍是工程缺口。
- RAID 元数据落盘默认关闭(官方为兼容性):运维自动化若假设「进程重启后阵列自愈」,会与默认行为冲突——产品是否强制开启 metadata,应写成显式策略而非口头约定。
七、参考资料
规范 / 官方文档(A)
- SPDK v26.01,Block Device User Guide(Introduction;Linux AIO;Malloc;Null;NVMe;Logical volumes;RAID)。
- SPDK
v26.01,Changelog(
bdev_aio与RWF_NOWAIT默认变更等)。
源码(A)
spdk/spdktag v26.01:module/bdev/nvme/、aio/、malloc/、null/、raid/、lvol/等。
站内
实验台账
- 本篇无命令输出、无模块间延迟对比数字。
八、小结
- 先分叶子与虚拟层;导出给 nvmf/vhost 的是最终 bdev 名。
- nvme / aio·uring / malloc·null 覆盖「用户态盘、内核旁路、测试靶」三条叶子轴;raid / lvol 解决聚合与切分,不是默认必叠。
- 叠层伤害来自跳数、错误翻译与资源策略冲突;用最少层表达产品能力。
- 把选好的 bdev 挂进 NVMe-oF——见 第 8 篇。
→ 系列目录 · 上一篇:bdev 核心 · 下一篇:NVMe-oF target
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【SPDK 用户态存储】用户态存储全景:从 Reactor 到 NVMe-oF 的坐标系
定位 SPDK 相对内核块层、O_DIRECT+io_uring 与 NVMe 协议百科的生态位;钉住 I/O 路径、线程模型、设备独占、块抽象、远端传输五条坐标系,并给出 16 篇阅读路线。
【SPDK 用户态存储】用户态 NVMe 驱动:Controller、Qpair 与 Poll Group
拆解 SPDK 被动式 NVMe 库:probe/attach、admin 与 I/O qpair、完成轮询与 poll group;说明单线程独占 qpair、零拷贝提交路径及相对内核驱动的边界。
【SPDK 用户态存储】bdev 核心:统一块接口、I/O 生命周期与排队完成
把 SPDK bdev 钉成用户态块栈:相对 Linux block 层统一了什么、砍掉了什么;一次 I/O 从提交、多无锁队列排队到完成回调的落点;队列满与 reset 的失败语义。
【SPDK 用户态存储】NVMe-oF Target:subsystem、listener 与 namespace→bdev
拆解 SPDK NVMe-oF target:lib/nvmf 中 tgt/subsystem/listener/namespace 与 bdev 的映射;说明 poll group 如何驱动 I/O,以及 app/nvmf_tgt 相对库的角色。