第 10 篇把远端 initiator 互通钉住之后,本机还有一条常见出口:虚拟机里的 virtio 块设备,后端不在内核 vhost,而在 SPDK 用户态进程。很多人把这条路径当成「再学一遍 virtio 规范」;生产排障真正缺的是坐标——谁拥有队列、谁轮询、内存如何共享、热插拔在 scsi/blk 上差什么。
本文是「SPDK / 用户态存储栈」系列第 11 篇:只钉 SPDK 作为 VMM 块后端 的机制位置。不写完整 virtio 设备模型、不写 guest 驱动实现、不粘贴未实测的 IOPS。
本文是「SPDK / 用户态存储栈」系列第 11 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 10 篇 · Initiator 与互通 主机侧与内核 initiator 第 11 篇 · vhost / virtio VMM 块后端位置与边界 第 12 篇 · JSON-RPC 生命周期 配置面与重启状态
版本锚定:机制与 RPC 名对齐 SPDK v26.01(LTS)官方 vhost Target 文档与
lib/vhost/。QEMU 侧:上游 vhost-user-scsi 自 2.10、vhost-user-blk 自 2.12。
一、问题:后端在谁的地址空间里
经典路径是:guest virtio → 主机内核 vhost → 主机块层 / NVMe 驱动。SPDK 路径把「主机侧 vhost 目标」搬进用户态:guest 仍说 virtio,主机侧由 SPDK 进程通过 vhost-user(Unix 域套接字 + 共享内存)消费队列,再落到本系列已写的 bdev。
flowchart LR
guest["Guest virtio<br/>driver"] --> qemu["QEMU / VMM"]
qemu -->|"vhost-user socket<br/>+ shared mem"| spdk["SPDK vhost target"]
spdk --> bdev["bdev"]
bdev --> nvme["NVMe / other modules"]
官方 vhost Target 的定位句很短:vhost target 在本机以进程形式提供本地存储服务,可把虚拟化块设备暴露给 QEMU 或其他进程。加速来自与 SPDK 其余组件相同的用户态轮询;因为 SPDK 在轮询提交队列,它可以通知 guest 跳过提交侧通知,从而在重 I/O 时减少 VMEXIT。
机制含义只有三句:
- 数据面不经过主机内核块层(除非你故意用 aio 等走内核的 bdev 模块)。
- 延迟与失败落在 SPDK reactor / bdev / 下层驱动,不落在「内核 vhost 模块调优」那一套手册上。
- guest CPU 与主机 SPDK CPU 都要记账:少 VMEXIT 不等于两边总核数变少。
谱系上,这是「用户态轮询存储栈」对 virtio 后端 的工程化,不是对 virtio 规范本身的重写。规范细节留给 QEMU/virtio 文档;本系列只要求读者能指出:队列在哪一侧被 poll、DMA 可见内存在哪一侧申请。
二、vhost-scsi 与 vhost-blk:同一插座,不同语义
| 维度 | vhost-scsi | vhost-blk |
|---|---|---|
| QEMU 设备 | vhost-user-scsi-pci |
vhost-user-blk-pci |
| 创建 RPC | vhost_create_scsi_controller |
vhost_create_blk_controller |
| bdev 暴露形态 | SCSI LUN(经 target ID 挂到 controller) | 直接块设备,与 SCSI 无关 |
| 热挂/热摘 bdev | 支持
vhost_scsi_controller_add/remove_target |
无对等 hot-attach RPC |
| 只读 | 文档侧重 scsi 路径的 attach/detach | 创建时可 --readonly / -r |
官方约束要原样记住:
- Vhost-SCSI:每个 SCSI target 当前只支持一个 LUN;多个盘用不同 target ID。
- 物理拔掉 NVMe 时:挂在 scsi controller 上会触发 vhost-scsi 的 hot-detach;挂在 vhost-blk 上则该设备上 I/O 会被中止,可能灌满 guest 日志——这是文档写明的差异,不是「配置手误」。
选型不是口味:要运行时换盘、对齐 SCSI 管理习惯 → scsi;要更薄的块语义、接受「换盘 = 重建 controller」→ blk。两者都先依赖已创建的 bdev(nvme / malloc / aio 等,见 第 7 篇)。
命令语义(需已启动 vhost
应用;输出以本机为准,此处不伪造):
# 创建示例 bdev 后:
scripts/rpc.py vhost_create_scsi_controller --cpumask 0x1 vhost.0
scripts/rpc.py vhost_scsi_controller_add_target vhost.0 0 Malloc0
scripts/rpc.py vhost_create_blk_controller --cpumask 0x1 vhost.1 Malloc1-S 指定 Unix socket
目录(默认当前工作目录);示例常用
/var/tmp。QEMU 侧用
-chardev socket,path=... 接到同一路径。
三、共享内存与 NUMA:后端能跑起来的硬前提
vhost-user 要求 guest 内存对 SPDK 进程可见。官方 QEMU 片段的关键不是「多开几个 queue」,而是:
-object memory-backend-file,id=mem,size=1G,mem-path=/dev/hugepages,share=on
-numa node,memdev=memshare=on + hugepage
路径是共享前提;缺了它,socket
能连上,数据面也会在映射阶段失败。主机侧仍要先按 第 3 篇 准备
hugepage(文档示例用
HUGEMEM=... scripts/setup.sh),再启动:
build/bin/vhost -S /var/tmp -m 0x3-m 给出的核会被 I/O
轮询占满。单个 vhost 设备可用
--cpumask 限制到子集;NUMA 上应选与
关联 VM 同 socket
的核——这是官方性能提示,不是口头禅。
guest 侧:Linux
多队列块层(scsi_mod.use_blk_mq=1 等)与 QEMU
num_queues
要一起看。文档警告:队列开太多时,多设备场景下每个设备都要额外轮询队列,反而拖垮
SPDK vhost;部分发行版在 num_queues 大于 vCPU
数时会 panic——以 guest 内核与镜像能力为准。
四、失败落点:别在 virtio 规范里空转
| 症状方向 | 先落哪一格 | 典型核对 |
|---|---|---|
| QEMU 起不来 / chardev 连不上 | socket 路径、权限、SPDK 是否在听 | vhost_get_controllers;-S 与
path 一致 |
| guest 看不见盘 | scsi vs blk 配置、target/LUN、bootindex | 官方示例用 bootindex=0
保证从系统盘启动 |
| 延迟差但盘在 | cpumask / NUMA、队列数、下层 bdev | 对照 reactor 占用与 第 2 篇 |
| 热换盘异常 | scsi 有 hot-attach;blk 无 | 文档 Hot-attach/hot-detach |
| Windows guest 异常 | 旧 viostor 与非 512 扇区 | 已知限制:0.1.130-1 前对非 512 有 bug |
| 间歇性 I/O 错误灌屏 | 下层 bdev 被删或 NVMe 热拔 | blk 路径无优雅 hot-detach;先看 SPDK 日志再怪 guest |
| VM 与 SPDK 跨 NUMA | 内存后端节点与
-m/--cpumask |
官方明确要求同 socket;跨节点会表现为「能用但尾延迟差」 |
排障顺序建议:先证明 socket 与 controller 存在 → 再证明 guest 看见的盘名/大小与 bdev 一致 → 再谈队列与性能。中间不要插入「重装 virtio 驱动」除非 Windows 已知扇区问题已对上版本。
学术争论在本篇收成一句:用户态 vhost 用轮询换提交侧 VMEXIT,是否值得,取决于「主机专用核税」与「guest 通知税」谁更贵——没有跨硬件通用排名。更细的开放问题见下文与 第 14–16 篇。
实践上还有两类「看起来像 virtio、其实是环境」的坑:
- 大页总量被 QEMU 与 SPDK
分食:文档示例先
HUGEMEM=... setup.sh再起 vhost,再给 QEMUmemory-backend-file;若先起多个大内存 VM,再起 SPDK,常表现为 EAL 或 mempool 分配失败,而不是 chardev 报错。 - 权限与生命周期:socket 文件落在
/var/tmp时,进程崩溃残留 sock、或 QEMU 与 SPDK 启动用户不一致,会造成「昨天还能连」的假回归。配置库存应记录-S、socket 名与 systemd 单元依赖顺序,而不是只记 QEMU 命令行截图。
对超融合节点,还要问:vhost 后端的 bdev 是本地 NVMe,还是再叠一层网络 bdev?每多一层,第 7 篇的「何时不该叠层」就要重读一遍——虚拟化出口不会自动抹掉叠层代价。
五、和 NVMe-oF、内核 vhost 的位置差
三条「把块设备送给别人用」的路径容易混称,机制上并不互换:
| 路径 | 消费方 | 主机侧实现落点 | 本系列位置 |
|---|---|---|---|
| 内核 vhost + virtio | 本机 VM | 内核 vhost 模块 + 块层 | 对照对象,非本系列正文 |
| SPDK vhost-user | 本机 VM / 连同一 socket 的进程 | 用户态 poll + bdev | 本篇 |
| SPDK NVMe-oF | 远端 initiator | 传输 + nvmf + bdev | 第 8–10 篇 |
NVMe-oF 解决的是跨主机块语义;vhost-user
解决的是同主机地址空间共享下的 virtio
后端。把「已经会配 nvmf」直接当成会配 vhost,会在
share=on 内存后端和 guest
队列通知上栽跟头——远端路径根本没有这层共享内存契约。
反过来,有人用 SPDK vhost 只为了「虚拟机里看到一块快盘」,却把网关流量也塞进同一批 reactor:合法,但排障时必须声明 vhost controller 的 cpumask 与 nvmf poll group 是否争核。第 14 篇五轴里,这类问题同时落在 CPU 轴与环境轴,而不是「virtio 规范没读熟」。
工程间隙还有一条:官方 vhost Target 示例输出里会出现 DPDK EAL 初始化日志——说明 vhost 应用仍坐在 SPDK/DPDK 环境层上。运维若只按「QEMU 设备参数」排障而忽略 hugepage 与 EAL 失败,会在错误层空转。本系列 第 3 篇 的环境清单对 vhost 同样适用,并不因为前端是虚拟机就豁免。
若产品形态是「SPDK 在宿主机、业务在 Kata/QEMU 虚拟机」,还要区分:块设备后端(本篇)与 virtio-net + DPDK(本系列明确不写)。两者都可能出现 vhost-user 字样,但失败模式、观测字段与绑核策略不同;混用同一套「vhost 调优笔记」会制造假相关。
六、谱系、争论与本篇开放问题
谱系(简):virtio 把设备模型标准化到 guest/hypervisor 边界 → 内核 vhost 把后端加速留在主机内核 → vhost-user 把后端进程化,使用户态存储栈(SPDK)能直接 poll 队列。SPDK 文档强调的跳过提交通知,是这条谱系上「用轮询换 VMEXIT」的工程句,不是对 virtio 环格式的重新发明。
争论:专用核轮询是否总优于内核 vhost + 中断微调?一方看重 guest 提交路径少陷入;一方看重主机核可与其他任务共享、运维方言不换。两边都缺「与你同代硬件、同队列深度」的可复现对比时,争论会退化成口号。合法做法是:固定 guest 镜像与块大小,只改后端实现,按 第 13 篇 口径自测——本篇不提供未跑数字。
另一处工程争论是 vhost-scsi 与 vhost-blk 的运维友好性:scsi 有 hot-attach/detach,更像传统存储阵列换 LUN;blk 更薄,但 NVMe 热拔时文档已警告 guest 可能被错误日志淹没。选「更像 SAN」还是「更少抽象」没有统一答案;要写进容量模型的是:换盘窗口内 guest I/O 的失败语义是否可被上层重试吸收。
开放问题:
- live migration / 多进程 vhost 会话在故障切换时,socket 与内存后端的一致性如何被观测?官方已知限制节之外,生产是否需要外挂编排探针?
- 同一 SPDK 进程混部 vhost 与 nvmf 时,延迟归因是否要强制分 controller/子系统的 iostat,而不是只看进程级 CPU?
- guest
num_queues与主机 poller 数量的匹配,有没有可机器检查的不变量(例如「queues ≤ vCPU 且 ≤ 每设备 poll 预算」),还是只能靠压测经验表?
七、本篇边界与下一篇
写了:SPDK 在 VMM 路径上的位置、scsi/blk 语义差、共享内存与 cpumask、与 NVMe-oF/内核 vhost 的对照、文档级失败落点与开放问题。
没写:virtio 环布局逐字段、live migration 全手册、未实测性能表、Windows 驱动排错全书。
控制面如何创建这些对象、进程重启后什么还在,见 第 12 篇 JSON-RPC。把 vhost 只当成「多几个 RPC」会低估配置面纪律——下一篇专门钉住这件事。
排障时把 guest dmesg、QEMU 日志与 SPDK
进程日志放在同一时间轴上对齐,是比单独抠 virtio
规范更有效的方法。三者时间源可能不一致,所以应用业务超时时间戳做粗对齐,再在秒级窗口内查
socket accept、controller 创建与 bdev 删除事件。若只有 guest
侧报 I/O error 而 SPDK 侧无对应 bdev
事件,优先怀疑路径连错实例或内存共享失败,而不是介质本身。反过来,SPDK
已打印 hot-detach 而 guest 仍重试旧
LUN,属于预期的短暂窗口,应用层重试策略要能消化,而不是把它记成「SPDK
随机掉盘」。
容量规划上,为每个 vhost controller
预留的不只是一块盘,还包括:独占或共享的 poller 核、与 VM 同
NUMA 的大页预算、socket 目录的生命周期、以及 guest
队列数上限。缺任何一项,都会在「功能演示通过、生产一扩容就抖」的模式里复发。把这些项写进与虚拟机模板绑定的清单,比事后调
num_queues 更接近根因治理。
八、参考资料
规范 / 官方文档(A)
- SPDK vhost
Target(
https://spdk.io/doc/vhost.html),v26.01 文档树。 - SPDK An Overview of SPDK
Applications:
vhost应用与公共框架参数(-S、-m、-r等)。 - SPDK
JSON-RPC:
vhost_create_*、vhost_scsi_controller_*、vhost_get_controllers。
源码(A)
lib/vhost/、include/spdk/vhost.h@ tag v26.01。
边界外链
- 第 6–7 篇 bdev、第 3 篇环境与绑盘。
- 协议侧 virtio/NVMe 背景见站内 storage/03(不替代本文机制坐标)。
→ 上一篇:Initiator 与互通 · 系列目录 · 下一篇:JSON-RPC 与生命周期
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【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 Target:subsystem、listener 与 namespace→bdev
拆解 SPDK NVMe-oF target:lib/nvmf 中 tgt/subsystem/listener/namespace 与 bdev 的映射;说明 poll group 如何驱动 I/O,以及 app/nvmf_tgt 相对库的角色。