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

【SPDK 用户态存储】vhost / virtio 边界:对 VMM 提供块后端的位置

文章导航

分类入口
storagelinux
标签入口
#spdk#vhost#virtio#vhost-user#qemu#bdev#v26.01

目录

第 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。

机制含义只有三句:

  1. 数据面不经过主机内核块层(除非你故意用 aio 等走内核的 bdev 模块)。
  2. 延迟与失败落在 SPDK reactor / bdev / 下层驱动,不落在「内核 vhost 模块调优」那一套手册上。
  3. 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

官方约束要原样记住:

选型不是口味:要运行时换盘、对齐 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=mem

share=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 通知税」谁更贵——没有跨硬件通用排名。更细的开放问题见下文与 第 1416 篇。

实践上还有两类「看起来像 virtio、其实是环境」的坑:

  1. 大页总量被 QEMU 与 SPDK 分食:文档示例先 HUGEMEM=... setup.sh 再起 vhost,再给 QEMU memory-backend-file;若先起多个大内存 VM,再起 SPDK,常表现为 EAL 或 mempool 分配失败,而不是 chardev 报错。
  2. 权限与生命周期: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 的失败语义是否可被上层重试吸收

开放问题

  1. live migration / 多进程 vhost 会话在故障切换时,socket 与内存后端的一致性如何被观测?官方已知限制节之外,生产是否需要外挂编排探针?
  2. 同一 SPDK 进程混部 vhost 与 nvmf 时,延迟归因是否要强制分 controller/子系统的 iostat,而不是只看进程级 CPU?
  3. 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)

源码(A)

边界外链

上一篇:Initiator 与互通 · 系列目录 · 下一篇:JSON-RPC 与生命周期

读完这篇,下一步读什么

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


By .