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

【SPDK 用户态存储】JSON-RPC 与生命周期:配置面、热插拔与重启边界

文章导航

分类入口
storagelinux
标签入口
#spdk#json-rpc#rpc.py#lifecycle#hotplug#save_config#v26.01

目录

数据面(reactor → bdev → nvmf/vhost)可以在进程内跑得很「热」,但运维真相几乎都经 JSON-RPC 2.0 进出:绑盘、建 subsystem、挂 namespace、建 vhost controller。若把 RPC 当成「随便调的脚本接口」,最容易在两类事故上栽——STARTUP 里改了 RUNTIME 才允许的参数,以及 进程死了却以为配置还在内存之外某处

本文是系列第 12 篇:钉配置面、热插拔与进程生命周期,回答「什么状态能跨重启存活」。不粘贴本机未执行的 RPC dump,不写编排产品教程。

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

篇目 核心内容
第 11 篇 · vhost / virtio VMM 块后端位置
第 12 篇 · JSON-RPC 配置面与状态存活边界
第 13 篇 · 可观测与性能口径 iostat / 官方 report 读法

版本锚定:SPDK v26.01 JSON-RPCAn Overview of SPDK Applications;应用框架源码脉络见 lib/event/app.c(加载 -c、切换 RPC 状态)。


一、配置面在哪:不是第二个数据面

SPDK 目标应用(nvmf_tgtvhost、统一 spdk_tgt 等)共用 app 框架。框架暴露:

机制 作用
Unix RPC socket(默认 /var/tmp/spdk.sock-r 可改) 管理工具连接点
scripts/rpc.py 官方主入口:子命令映射到 JSON-RPC method
-c / --config 启动时加载「RPC 调用序列」JSON,复现配置
save_config 当前可导出配置生成同类 JSON

官方写法:

scripts/rpc.py save_config > config.json
# 新进程:
build/bin/spdk_tgt -c config.json ...

要点:config.json 不是「神秘数据库快照」,而是 一组已排序的 RPC 调用。手写极痛苦,所以用运行中实例导出。批处理可用 scripts/rpc.py < rpc.txt 一次灌多条(文档 JSON-RPC batching)。

远程访问另有 rpc_http_proxy.py 等路径;生产必须当 管理面 隔离,不能和数据网混开。错误码除标准 JSON-RPC 外,文档标明 -1 Invalid state:方法存在但当前运行态不可调用——这是生命周期排障的第一信号。


二、STARTUP 与 RUNTIME:谁先动、谁能动

应用状态从 STARTUPRUNTIMEApp Overview · Deferred initialization)。

stateDiagram-v2
  [*] --> STARTUP: --wait-for-rpc
  STARTUP --> RUNTIME: framework_start_init OK
  [*] --> RUNTIME: normal start
  note right of STARTUP: subset of RPCs\ninit params only
  note right of RUNTIME: full RPC surface\nparams frozen for init

工程含义:把「改 bdev 选项」和「改只能在 STARTUP 生效的全局参数」混在同一条自动化流水线,会得到 -1 Invalid state 或静默以为改成功其实未生效。发布剧本应显式分支:冷启动序列 vs 热 RPC。

-c 文件与状态机的配合:框架会在适当阶段加载配置中的 STARTUP / RUNTIME RPC(app.c 在进入 RUNTIME 后再加载 RUNTIME 段)。--wait-for-rpc 与手写 -c 叠加时,必须按官方状态约束拆步骤,不能假定「一个 JSON 里任意顺序都合法」。


三、热插拔:进程内可变,不等于持久化

RUNTIME 下大量对象可热变更,例如:

这些变更的生命周期是:

状态 进程内 进程退出后
已 RPC 创建的 bdev / nvmf / vhost 对象 有效,直到删除或进程结束 默认丢失
save_config 落盘并在下次 -c 加载 可复现结构 仅当运维保证文件与设备仍存在
进行中的 I/O、连接、guest 会话 随对象与传输状态变化 不存活
bdev_get_iostat 计数 进程内累计;可被 reset 不存活;且 reset 影响所有消费者(见下篇)
VFIO 绑定、大页预留 属主机环境 / 第 3 篇 独立于 SPDK 进程;回退驱动另论

热插拔成功 ≠ 已写入真相源。 若编排系统只记「昨晚 RPC 过」,重启后盘与 subsystem 都不会自己长回来。正确模型是:

  1. 运行态以进程内存 + 当前 RPC 视图为准;
  2. 持久态以你保存的 JSON(或等价编排库存)为准;
  3. 设备独占(vfio-pci)以主机 setup 为准——save_config 不会替你把盘「还给」内核驱动。

多进程模式(--shm-id)改变的是内存与 NVMe 设备共享,不是「自动持久化配置」。主进程退出后 secondary 可继续跑,但也不能挂新进程——这是可用性边界,不是配置数据库。


四、rpc.py 与排障入口

scripts/rpc.py --help
scripts/rpc.py bdev_nvme_attach_controller --help
scripts/rpc.py framework_get_reactors   # 例:运行态查询类;以 --help 与当前 methods 为准

实践清单:

  1. 连错 socket(多实例 -r)时,所有「配置没生效」都是假症状。
  2. rpc_get_methods(current)确认状态,再怀疑业务 RPC。
  3. 变更后若需可重启复现:立刻 save_config 并纳入版本控制 / 配置库存;不要依赖 shell 历史。
  4. 插件扩展(SPDK_RPC_REGISTER + rpc.py plugin)属于二次开发面;生产基线先钉官方 method 集合。

与数据面关系:RPC 走管理线程/消息,不替代 reactor 上禁止阻塞 的纪律。慢 RPC 或错误地在 poller 上下文做重活,会表现为「管理面卡、数据面 CPU 仍 100%」——两轴分开看。


五、重启剧本:什么该进库存,什么不该

把生命周期写成可执行剧本,比再背一批 RPC 名有用。最小闭环是:

  1. 环境层(主机):大页、setup.sh 绑盘、IOMMU;不属于 save_config
  2. 冷启动参数-m-s-r--wait-for-rpc、PCI allow/block;进 systemd/unit 或编排模板。
  3. STARTUP RPC(若使用 deferred init):只能放初始化全局选项;用 rpc_get_methods 校验。
  4. framework_start_init:一次;失败则进程不宜假装「半初始化可服务」。
  5. RUNTIME RPC / -c 中 RUNTIME 段:bdev、nvmf、vhost 对象图。
  6. 运行中热变更:必须回流到库存(再次 save_config 或等价声明式渲染),否则下次重启漂移。

常见事故模式:

模式 机制解释 缓解
只热加盘,不改库存 内存态 ≠ 持久态 变更门禁强制导出或声明式 apply
把 STARTUP 参数塞进 RUNTIME 脚本 -1 Invalid state 或静默无效 脚本分阶段;先查 methods
多实例共用默认 sock 路径 配错进程 每实例独立 -r 与监控标签
依赖 shell 历史当配置 人走政息 Git/配置中心存 JSON
重置 iostat 当「发布成功」信号 计数是观测不是配置 发布看对象存在性与流量,不看清零

与数据面关系再钉一次:RPC 成功只保证控制面对象图更新;不保证 guest/initiator 已重连、不保证旧连接排空完成。vhost 热摘、nvmf 删 namespace 时,要在上层协议语义里找「进行中 I/O」的结局,而不是假设 JSON-RPC 事务跨越数据面。

save_config 的诚实边界:它导出的是框架认为可序列化的 RPC 视图。若你靠进程外脚本改了 hugepage、或手工 driver_override 却未记录,导出文件无法复现完整主机状态。因此「配置真相」应是 主机环境剧本 ⊕ SPDK JSON,而不是单文件神话。

多进程(--shm-id)场景下,谁有权改共享 NVMe、secondary 崩溃是否触发主配置重载,要在部署说明里写死;官方语义是内存与设备共享,不是分布式配置共识。把 secondary 当「高可用配置副本」是模型误用。


六、和编排系统的接口形状

生产很少长期手打 rpc.py。无论自研 operator、Ansible 还是厂商控制面,接口形状通常只有三种:

  1. 命令式:对运行中进程逐条 RPC(适合急救,难审计)。
  2. 声明式渲染:库存 → 生成 -c JSON → 滚动重启或受控 apply(适合真相源)。
  3. 混合:结构变更走声明式;权重/临时摘流走短 TTL 命令式并打审计。

SPDK 本身不提供 Envoy 式 ACK/NACK 版本树;你看到的「成功」是单次 JSON-RPC 响应。若需要「配置已在所有节点生效」的集群语义,必须自建版本号与对账——对账内容至少包括:bdev 名、nvmf listener、vhost socket 路径、transport 类型。缺对账时,排障会回到第 14 篇的「连错 socket / 看错实例」。

安全上:默认 Unix socket 的访问控制取决于文件系统权限;HTTP 代理示例带用户密码也只是基线。管理面应与数据网分离,并考虑 RPC allowlist(应用框架支持限制可调方法集合)。把 RPC 端口暴露到业务 VPC,等于把「删 namespace」按钮交给网络邻居。


七、本篇边界

写了:JSON-RPC 作为配置面、STARTUP/RUNTIME、save_config/-c、热插拔与重启存活表、rpc.py 用法、重启剧本与编排接口形状。

没写:每个 RPC 的参数百科(以官方 JSON-RPC 为准)、HTTP 代理加固全文、伪造的配置 dump、Kubernetes CRD 设计。

下一篇用同一配置面读 iostat / 官方 Performance Reports,把「看见的数字」和「能得出的结论」拆开。


从控制面审计角度看,每次成功的破坏性 RPC(删 namespace、detach controller、删 bdev)都应当留下「谁、在哪台、对哪个 sock、改了什么对象」的记录。SPDK 原生接口不强制这门审计;缺失时,事故复盘只能依赖 shell 历史与残缺监控,结论会变成不可核对的叙事。最低可行做法是:所有生产变更走包装后的 rpc 入口,统一打结构化日志,并拒绝人类直接对生产 sock 做未登记操作。

另一个常被低估的点是批处理文件与交互式 RPC 混用时的顺序依赖:先加 listener 再创建 transport、或先挂 namespace 再创建 subsystem,都会在不同版本的校验严格程度上表现为成功或 Invalid params。把依赖写成显式拓扑(bdev → subsystem/ns → listener,或 bdev → vhost controller),由工具生成顺序,比人肉记忆更稳。-c 导出结果应被当作「已排序剧本」回归测试的黄金文件,在升级 LTS 时重放并对比对象图。

升级 v26.01 到下一 LTS 时,除了二进制与依赖库,还应重放一份最小配置:至少一个 nvme 或 malloc bdev、一个 nvmf 或 vhost 出口、一次 save_config 往返。回归失败常见于 method 更名、默认参数收紧、或 STARTUP/RUNTIME 可调集合变化——这些都能在预发环境用「重放黄金 JSON」抓住,而不必等生产热变更踩雷。把该重放挂进 CI 或发布 checklist,是配置面成熟度的硬指标。

若使用 --wait-for-rpc,自动化必须处理「进程已听 sock 但尚未 RUNTIME」的窗口:健康检查若只看进程存活或 sock 文件存在,会把半初始化实例标绿。更可靠的探针是:rpc_get_methods 含预期 RUNTIME 方法,或对只读查询(如列出 bdev)得到成功响应。探针失败时的动作应是阻断流量接入,而不是继续挂载业务。

八、参考资料

规范 / 官方文档(A)

源码(A)

上一篇:vhost / virtio · 系列目录 · 下一篇:可观测与性能口径

读完这篇,下一步读什么

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


By .