数据面(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-RPC、An Overview of SPDK Applications;应用框架源码脉络见
lib/event/app.c(加载-c、切换 RPC 状态)。
一、配置面在哪:不是第二个数据面
SPDK
目标应用(nvmf_tgt、vhost、统一
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:谁先动、谁能动
应用状态从 STARTUP 到
RUNTIME(App 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
- 带
--wait-for-rpc(短选项文档写作-w语境下的 deferred init):进程在子系统初始化前停下;RPC 服务器已就绪,但 仅允许设置初始化参数的一小子集。 - 客户端配完后必须调用一次且仅一次
framework_start_init;返回true后进入RUNTIME,可用方法集合变大。 - 初始化参数在进入 RUNTIME 后不可再改。想改池大小、某些全局选项,只能停进程、带新参数或新的 STARTUP 序列重来。
rpc_get_methods带current: true可列出当前状态下可调用方法——排障时以它为准,不以记忆中的「完整 RPC 表」为准。framework_wait_init:阻塞到子系统初始化完成且 RPC 已在 running;已在运行则立即返回。
工程含义:把「改 bdev 选项」和「改只能在 STARTUP 生效的全局参数」混在同一条自动化流水线,会得到 -1 Invalid state 或静默以为改成功其实未生效。发布剧本应显式分支:冷启动序列 vs 热 RPC。
-c
文件与状态机的配合:框架会在适当阶段加载配置中的 STARTUP /
RUNTIME RPC(app.c 在进入 RUNTIME 后再加载
RUNTIME 段)。--wait-for-rpc 与手写
-c
叠加时,必须按官方状态约束拆步骤,不能假定「一个 JSON
里任意顺序都合法」。
三、热插拔:进程内可变,不等于持久化
RUNTIME 下大量对象可热变更,例如:
bdev_nvme_attach_controller/ detach- nvmf subsystem、listener、namespace 映射
- vhost-scsi 的 add/remove target(第 11 篇)
这些变更的生命周期是:
| 状态 | 进程内 | 进程退出后 |
|---|---|---|
| 已 RPC 创建的 bdev / nvmf / vhost 对象 | 有效,直到删除或进程结束 | 默认丢失 |
经 save_config 落盘并在下次 -c
加载 |
可复现结构 | 仅当运维保证文件与设备仍存在 |
| 进行中的 I/O、连接、guest 会话 | 随对象与传输状态变化 | 不存活 |
bdev_get_iostat 计数 |
进程内累计;可被 reset | 不存活;且 reset 影响所有消费者(见下篇) |
| VFIO 绑定、大页预留 | 属主机环境 / 第 3 篇 | 独立于 SPDK 进程;回退驱动另论 |
热插拔成功 ≠ 已写入真相源。 若编排系统只记「昨晚 RPC 过」,重启后盘与 subsystem 都不会自己长回来。正确模型是:
- 运行态以进程内存 + 当前 RPC 视图为准;
- 持久态以你保存的 JSON(或等价编排库存)为准;
- 设备独占(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 为准实践清单:
- 连错 socket(多实例
-r)时,所有「配置没生效」都是假症状。 - 先
rpc_get_methods(current)确认状态,再怀疑业务 RPC。 - 变更后若需可重启复现:立刻
save_config并纳入版本控制 / 配置库存;不要依赖 shell 历史。 - 插件扩展(
SPDK_RPC_REGISTER+rpc.pyplugin)属于二次开发面;生产基线先钉官方 method 集合。
与数据面关系:RPC 走管理线程/消息,不替代 reactor 上禁止阻塞 的纪律。慢 RPC 或错误地在 poller 上下文做重活,会表现为「管理面卡、数据面 CPU 仍 100%」——两轴分开看。
五、重启剧本:什么该进库存,什么不该
把生命周期写成可执行剧本,比再背一批 RPC 名有用。最小闭环是:
- 环境层(主机):大页、
setup.sh绑盘、IOMMU;不属于save_config。 - 冷启动参数:
-m、-s、-r、--wait-for-rpc、PCI allow/block;进 systemd/unit 或编排模板。 - STARTUP RPC(若使用 deferred
init):只能放初始化全局选项;用
rpc_get_methods校验。 framework_start_init:一次;失败则进程不宜假装「半初始化可服务」。- RUNTIME RPC /
-c中 RUNTIME 段:bdev、nvmf、vhost 对象图。 - 运行中热变更:必须回流到库存(再次
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 还是厂商控制面,接口形状通常只有三种:
- 命令式:对运行中进程逐条 RPC(适合急救,难审计)。
- 声明式渲染:库存 → 生成
-cJSON → 滚动重启或受控 apply(适合真相源)。 - 混合:结构变更走声明式;权重/临时摘流走短 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)
- SPDK
JSON-RPC:Overview、错误码、
save_config、framework_start_init/framework_wait_init、rpc_get_methods。 - SPDK An Overview of SPDK
Applications:命令行、
--wait-for-rpc、-c、-r、--shm-id。
源码(A)
lib/event/app.c:JSON 配置加载与SPDK_RPC_STARTUP/SPDK_RPC_RUNTIME切换 @ v26.01。scripts/rpc.py。
→ 上一篇:vhost / virtio · 系列目录 · 下一篇:可观测与性能口径
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【SPDK 用户态存储】用户态存储全景:从 Reactor 到 NVMe-oF 的坐标系
定位 SPDK 相对内核块层、O_DIRECT+io_uring 与 NVMe 协议百科的生态位;钉住 I/O 路径、线程模型、设备独占、块抽象、远端传输五条坐标系,并给出 16 篇阅读路线。
【SPDK 用户态存储】Reactor 与线程模型:Event、Poller 与 Message Passing
钉住 SPDK event framework:每核 reactor、事件队列、poller 轮询与跨核消息;说明 shared-nothing 边界,以及在 reactor 上阻塞等于饿死同核所有工作。
【SPDK 用户态存储】环境与绑盘:Hugepage、CPU 亲和与 VFIO/UIO
说明 SPDK 运行前 env:大页分配、CPU 绑核语义、setup.sh 将 NVMe 从内核驱动解绑到 VFIO/UIO;钉住设备独占代价与 reset 回退路径。
【SPDK 用户态存储】用户态 NVMe 驱动:Controller、Qpair 与 Poll Group
拆解 SPDK 被动式 NVMe 库:probe/attach、admin 与 I/O qpair、完成轮询与 poll group;说明单线程独占 qpair、零拷贝提交路径及相对内核驱动的边界。