土法炼钢 · 系统与基础设施

【SPDK 用户态存储】bdev 核心:统一块接口、I/O 生命周期与排队完成

文章导航

分类入口
storagelinux
标签入口
#spdk#bdev#block-layer#io-lifecycle#queueing#v26.01#userspace

目录

DMA 与 mempool 解决「缓冲能否被设备 DMA」之后,上层仍需要一套与具体后端解耦的块语义:读、写、unmap、flush,以及失败时的排队与 reset。SPDK Block Device User Guide 把这一层叫作 bdev:官方措辞是「等价于传统内核存储栈里紧贴驱动之上的操作系统块存储层」。本文只钉三件事:统一了什么、一次 I/O 怎么走完、相对 Linux block 层砍掉了什么。

本文是「SPDK / 用户态存储栈」系列第 6 篇(共 16 篇)。→ 系列目录

篇目 核心内容
第 5 篇 · DMA / mempool 可 DMA 内存与缓冲池
第 6 篇 · bdev 核心 统一接口、生命周期、排队完成
第 7 篇 · bdev 模块 nvme / aio / raid / lvol 边界

上一篇DMA 与 mempool · 下一篇bdev 模块边界

版本锚定:SPDK v26.01(LTS);Block Device User Guide;公开头 include/spdk/bdev.h、实现树 lib/bdev/ @ tag v26.01。本篇无压测数字、无伪造 RPC 输出。

本篇不展开:各模块 RPC 菜谱与叠层代价(第 7 篇)、把 bdev 挂到 NVMe-oF namespace(第 8 篇)、iostat / trace 读法(第 13 篇)。


一、问题:用户态里「块设备」到底是谁

内核路径上,文件系统或 O_DIRECT 应用面对的是 struct block_device / bio / request_queue 这一整套调度与合并语义。SPDK 进程独占盘之后,应用(或 nvmf_tgt、vhost)不再经过内核块层,但仍需要:

  1. 同一套提交 API 覆盖本机 NVMe、远端 NVMe-oF、ramdisk、内核 AIO 文件等后端;
  2. 可叠加的虚拟块设备(logical volume、RAID、分区),而不把策略写进每个驱动;
  3. 多队列、尽量无锁的提交路径,与 reactor / poller 的线程模型对齐。

官方 Block Device User Guide 把 bdev 库的职责写成六条:可插拔模块 API;多种驱动模块;应用侧枚举 / 认领与读写 unmap;叠层设施(含 lvol、GPT);JSON-RPC 配置;以及 request queueing、timeout、reset,外加 multiple, lockless queues。后两条是排障时最容易被忽略的:队列不是无限的,超时与 reset 是显式状态机,不是「内核会帮你排队到天荒地老」。

flowchart TB
  app["App / nvmf / vhost"] --> api["bdev application API"]
  api --> q["per-channel lockless queues"]
  q --> core["lib/bdev core"]
  core --> mod["bdev / vbdev modules"]
  mod --> nvme["nvme"]
  mod --> aio["aio / uring"]
  mod --> mem["malloc / null"]
  mod --> virt["raid / lvol / gpt"]

直觉:模块负责「接到哪种介质」;bdev 核心负责「队列、完成回调、超时与 reset」。 上层只认 bdev 名与通道(channel),不认 PCIe BDF 或 RDMA 地址——那些落在模块 attach 阶段。


二、统一了什么:块语义与通道模型

2.1 应用 API 的边界

对应用而言,公开入口集中在 include/spdk/bdev.h(tag v26.01):按名打开 bdev、取 I/O 通道、提交 read / write / unmap / flush / reset 等,并注册完成回调。枚举与认领(claim)保证同一物理后端不会被互斥消费者重复打开到冲突状态——具体冲突规则由模块实现,核心只提供认领框架。

与内核对照时,有用的对应关系是概念级的,不是 API 一一映射:

内核块层(概念) SPDK bdev(概念) 差异要点
gendisk / bdev 名 bdev 名 / UUID / alias 进程内命名空间;热拔插靠 RPC 与模块回调
bio / request spdk_bdev_io 无 elevator 合并策略;合并若存在由模块自行决定
blk-mq 硬件队列 bdev I/O channel + 多无锁队列 通道绑定线程;跨线程靠 message,不靠共享队列锁
request_fn / mq_ops.queue_rq 模块 fn_table 提交入口 轮询完成,而非默认中断软中断路径
blkdev_issue_flush bdev flush / unmap 等 能力位由模块声明;不支持则提交失败或降级

统一的是:块大小、容量、读写 unmap flush、队列深度与错误码对上层可见的形状。 不统一的是:电梯算法、I/O 优先级类、cgroup blkio、内核 bio 合并与 plugging——这些在用户态轮询栈里要么不存在,要么被刻意砍掉以换取可预测延迟。

2.2 通道与无锁队列

Block Device User Guide 明确写了 multiple, lockless queues for sending I/O。实践含义与 第 2 篇第 4 篇 一致:

因此「队列满」首先是 该 channel 相对该 bdev 的队列资源耗尽,不是全局唯一大锁队列堵死。排障时要问:是哪个 reactor、哪个 channel、下层模块队列(例如 NVMe qpair)是否也满。


三、一次 I/O 的生命周期

下面按机制顺序描述,不绑定某一模块的私有状态机细节(模块差异见第 7 篇)。

3.1 提交

  1. 上层(应用、nvmf poll group、vhost)在持有 channel 的线程上调用 spdk_bdev_* 提交接口,填入缓冲、LBA、长度、完成回调与 cb_arg
  2. bdev 核心分配 / 填充 spdk_bdev_io,做能力与对齐检查(块大小、是否支持 unmap 等)。
  3. 若当前可下发,调用模块 fn_table 中的提交函数;若暂时不能(例如内部队列或下游背压),进入 queueing:请求挂在 bdev 侧队列,等待下游空间或超时逻辑。

官方把 Request queueing, timeout, and reset handling 列为库能力,说明排队不是「无限缓冲」:超时路径与 reset 会清空或失败在途 I/O,语义由核心与模块协作完成。本篇不伪造某次 bdev_get_iostat 数字;观测字段见第 13 篇。

3.2 完成

  1. 下层模块在 poller 中发现硬件或内核 AIO 完成,调用 bdev 完成路径。
  2. 核心把结果写回 spdk_bdev_io,调用上层注册的完成回调。
  3. 回调必须遵守线程约束:通常在提交所用 channel 对应的线程上下文中执行;若上层需要跨核汇总,应自行 message,而不是假设回调可重入任意线程。
sequenceDiagram
  participant App as Upper layer
  participant Bdev as lib/bdev
  participant Mod as bdev module
  participant Dev as Device / backend
  App->>Bdev: submit on channel
  alt queue not full
    Bdev->>Mod: fn_table submit
    Mod->>Dev: doorbell / aio / memcpy
  else queueing
    Bdev-->>Bdev: enqueue pending
  end
  Dev-->>Mod: completion polled
  Mod->>Bdev: complete bdev_io
  Bdev->>App: callback

3.3 失败轴

至少分清三条轴,避免都叫「I/O 失败」:

典型含义 上层该做什么
提交时返回错误 参数非法、无能力、资源立刻不可得 纠正参数或降并发;不要空转重试同一非法请求
排队后超时 / reset 下游卡住或显式 reset 查模块与设备健康;reset 期间新 I/O 可能失败或再排队
完成回调带错误状态 介质 / 传输 / 远端拒绝 按 NVMe / 模块错误语义重试或 failover(第 10、14 篇)

与内核对照:Linux 可以把 bio 在 elevator / scheduler 里合并、延迟、再下发;bdev 默认假设 调用方已经按延迟敏感路径组织好并发,核心提供排队与 timeout/reset,而不是 CFS 式的公平调度。

3.4 Reset 与超时在契约里的位置

把 reset 理解成「再试一次 ioctl」会误事。在 bdev 契约里,reset 更接近:

  1. 上层或超时逻辑决定「这条后端不再可信地前进」;
  2. 核心与模块协作,使在途 spdk_bdev_io 以错误完成或被清除,并尝试把模块/设备拉回可提交状态;
  3. 在此窗口内,同一 channel 上的新提交可能失败、再入队,或看到短暂的能力抖动。

因此排障时应把「偶发完成错误」「提交即失败」「reset 风暴」分开计数。v26.01 changelog 还扩展了统计重置模式(例如只清错误计数的 SPDK_BDEV_RESET_STAT_ERROR)以及 bdev_get_iostat 一次取多 bdev 名——说明官方把 错误与流量观测 当成契约的一部分,而不是模块私货。具体字段读法留给第 13 篇;本篇只需记住:没有观测就谈不上调队列深度。

缓冲约束与第 5 篇衔接:提交时传入的 payload 必须满足模块声明的 DMA / 对齐要求。bdev 核心做能力与块对齐检查,但 不会 默默替你把任意用户缓冲拷成可 DMA 内存;该拷的时候通常发生在特定 vbdev(例如官方 crypto 写路径的 scratch),代价由那一层承担。


四、相对 Linux block 层:统一与砍掉

4.1 谱系

经典内核块层叙事(bio → request → 驱动)服务的是「多文件系统、多调度器、多中断源共享一台机器」。用户态轮询栈的分叉点,是把 设备独占 + 每核队列 + 完成轮询 当成一等公民:SPDK 设计陈述与 Block Device User Guide 把 bdev 明确写成「内核块层在用户态的对应物」,但目标函数从「共享公平」换成「单租户数据面延迟可预测」。站内 storage/79 仍走内核提交;本系列走的是另一格。

4.2 争论:还要不要「块层」

本系列立场:把 bdev 当 契约层——统一语义与排队;叠层是否划算由第 7 篇按边界判断,而不是在本篇用未测数字宣称「一层固定加多少微秒」。

4.3 砍掉后的运维后果

砍掉内核调度与 cgroup 之后:

4.4 一张对照表:别再问「和内核哪个快」

问题 内核块层答案形态 bdev 答案形态
谁决定合并 elevator / 调度策略 通常不合并,或模块自定
谁保证多租公平 cgroup / 权重 / 限速 进程独占与人工核隔离
完成如何送达 中断 → softirq → 完成 poller 主动取完成
队列满了怎样 阻塞、拥堵控制、再排队 提交失败或 bdev 内 queueing + timeout
热扩展块服务 DM/MD/设备映射器生态 vbdev 模块 + JSON-RPC

「哪个快」只有在固定 workload、固定核与固定后端上才有意义;本系列禁止用未跑过的数字填表。机制上可以确定的是:两边优化的目标函数不同——一个优化共享机上的可管理性,一个优化专用数据面的可预测轮询路径。

若应用已经在内核用 io_uring 把提交开销压下去,却仍觉得「块层太重」,先区分瓶颈在 调度/合并 还是在 驱动/中断。前者未必需要 SPDK;后者才更接近本系列的迁移动机(第 15–16 篇收束)。


五、开放问题

  1. ZNS / KV 等命令集如何在通用 bdev 语义上暴露? 通用 read/write/unmap 无法表达 zone append 的全部约束;模块能力位与专用 API 的边界仍在演进。入口:NVMe ZNS 相关规范与 SPDK zoned 模块文档;站内协议背景见 storage/03
  2. 排队深度与超时默认值如何随后端类型自适应? 本机 NVMe 与跨机 TCP NVMe-oF 的合理 timeout 不是同一数量级;目前更多依赖部署经验与 RPC 调参,而非单一自动策略。
  3. 与 io_uring 内核路径的「混合完成模型」:同机部分盘走 uring bdev、部分盘走用户态 NVMe 时,完成轮询与中断等待如何共置在同一 reactor 上仍缺统一最佳实践(第 15 篇对照)。

六、参考资料

规范 / 官方文档(A)

源码(A)

站内

实验台账


七、小结

  1. bdev 是用户态块栈契约:统一提交 API、通道、排队 / 超时 / reset;模块负责后端差异。
  2. I/O 生命周期是「channel 上提交 →(可排队)→ 模块下发 → poll 完成 → 回调」;跨线程靠 message。
  3. 相对 Linux block 层:统一了块语义与多队列形状,砍掉了共享机公平调度与大量内核合并 / cgroup 语义。
  4. 选哪个模块、能否叠层——见 第 7 篇

系列目录 · 上一篇:DMA 与 mempool · 下一篇:bdev 模块边界

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .