模板如何变成可运行沙箱:Template、RootFS Artifact、AppSnapshot 与 LocalRunTemplate
在 CubeSandbox 里,模板不是一句“启动某个镜像”的别名。
从源码看,一个模板至少跨越 4 层对象:
- TemplateDefinition:控制面保存的模板定义,请求语义层
- RootFS Artifact:从 OCI 镜像导出的可复用根文件系统产物
- AppSnapshot / Replica:分发到节点上的可恢复运行快照
- LocalRunTemplate:节点本地真正参与创建沙箱的运行模板对象
这篇文章的目标,是把这 4 层对象和它们的衔接关系讲清楚,回答这个问题:
一个模板是怎样从“定义”一步步变成“某个节点上真的可以拿来启动沙箱的运行时资产”的?
1. 先看结论:模板不是单个对象,而是一条供应链
如果只用一句话概括这条链路,可以写成:
CubeMaster 负责生成和保存模板定义,把镜像/请求规范化为可分发资产;Cubelet 负责在节点上把 templateID 解析为本地
LocalRunTemplate,并在创建沙箱时把它真正注入运行流程。
所以“模板”这个词,在不同层里含义并不相同:
- 在 API / 控制面里,模板更像“可复用的创建请求”
- 在镜像构建链路里,模板更像“可下载、可校验的 rootfs 产物 + 快照副本”
- 在节点运行时里,模板更像“已在本地可用的
LocalRunTemplate”
这三者不是替代关系,而是逐层收敛关系。
2. 第一层:模板定义本质上是一个“归一化后的创建请求”
CubeMaster/pkg/templatecenter/store.go 里的 NormalizeRequest() 和 CreateTemplate(),是理解模板定义的起点。
2.1 NormalizeRequest() 做了什么
它会把一个原始的创建请求规整成模板语义:
- 确保有
templateID - 强制写入
appsnapshot create = true - 默认
instance_type = cubebox - 补齐模板版本号(默认
v2)
这一步说明一个重要事实:
模板的基础形态不是镜像,而是 CreateCubeSandboxReq。
也就是说,模板首先是“以后如何创建一个沙箱”的规范化请求,而不是某个 ext4 文件或某个 snapshot 路径。
2.2 normalizeStoredTemplateRequest() 为什么还要再删字段
模板存储时并不是把原始请求原封不动落库,而是会去掉一些运行期字段,例如:
SnapshotDirTimeoutInsId/InsIpRequestAppSnapshotCreate标记
这说明控制面保存模板时,有意把它压缩成:
- 可复用
- 去运行时噪音
- 与某次具体调用解耦
的模板定义。
也正因为如此,模板后来才能再次被 GetTemplateRequest() 取出,并重新赋予新的 RequestID 用于实际运行。
3. 第二层:TemplateDefinition 是“控制面真相”,不是节点真相
CreateTemplate() 在一开始就会执行 createDefinition(),把模板定义写入数据库中的 TemplateDefinition。
这个定义里保存的关键内容包括:
template_idinstance_typeversionstatusrequest_json
其中最重要的是 request_json:它保存的是模板化后的创建请求。
所以 TemplateDefinition 的职责不是:
- 记录某个节点上有没有副本
- 保存某个 snapshot 文件在哪
- 告诉 Cubelet 怎么挂载 rootfs
而是:
- 作为模板的“控制面定义源”
- 让后续
ResolveTemplate()能把 templateID 再展开成运行请求
换句话说:
TemplateDefinition 负责保存“怎么启动”,而不是“在哪儿启动”。
4. 第三层:从 OCI 镜像出发时,还要先生成 RootFS Artifact
当模板不是从现成 AppSnapshot 来,而是从 OCI 镜像构建时,链路会先进入 CubeMaster/pkg/templatecenter/template_image.go。
这里最核心的函数是:
ensureRootfsArtifact()buildRootfsArtifact()generateTemplateCreateRequest()
4.1 为什么需要 RootFS Artifact
因为镜像仓库里的 OCI 镜像,并不能直接成为 Cubelet 上可运行的运行时根盘。
中间至少要经历:
- 拉取 / inspect 源镜像
- 导出 rootfs
- 迁移到 artifact store
- 制作 ext4 镜像
- 计算 SHA256 与大小
- 生成下载 token 和下载 URL
也就是说,RootFS Artifact 是把“镜像世界”翻译成“CubeSandbox 运行时世界”的中间资产。
4.2 ensureRootfsArtifact() 的设计重点:它是可复用的
这段代码里最值得注意的不是构建过程,而是“重用逻辑”:
- 先用模板规格和源镜像 digest 计算 fingerprint
- 再推导
artifactID - 先找有没有现成 artifact 可复用
- 只有找不到或不匹配时才重新构建
这说明 RootFS Artifact 不是一次性临时产物,而是带 fingerprint 的可缓存构建产物。
因此,多个模板请求如果规格相同,完全可能复用同一个 rootfs artifact。
5. 第四层:镜像模板最终还是会被转回 CreateCubeSandboxReq
generateTemplateCreateRequest() 非常关键,因为它把 rootfs artifact 又重新组装回一个运行请求。
这里会生成:
- 模板 annotations
- artifact annotations
- writable layer 配置
- 容器镜像描述
- 卷与
/的 writable layer 挂载 - command / args / env / working dir
- 资源与 security context
- exposed ports 与 network context
这说明一个很重要的设计选择:
即使模板来源于 OCI 镜像,最终运行时消费的仍然是
CreateCubeSandboxReq。
也就是说,CubeSandbox 并没有为“镜像模板”另造一套完全平行的运行协议,而是把镜像构建结果重新折回统一的创建请求模型。
这让后续调度、分发、运行逻辑保持一致。
6. 第五层:模板真正“可用”,取决于副本而不是定义本身
CreateTemplate() 在写完定义之后,并不会直接宣布模板可用,而是继续执行:
resolveTemplateNodes()createTemplateReplicasOnNodes()finalizeTemplateReplicas()
这里出现了模板的另一层实体:Replica。
6.1 Replica 记录的是什么
每个 replica 至少带这些信息:
NodeID/NodeIPSpecSnapshotPathStatusPhaseArtifactIDErrorMessage
这说明模板并不是全局单体对象,而是“定义 + 多节点副本”的组合。
6.2 为什么要有 Replica
因为真正运行沙箱时,节点不能每次都回控制面临时现做模板。
节点需要本地已经准备好的:
- snapshot
- rootfs/base block
- 运行时元数据
所以模板是否 ready,本质上是:
- 是否至少有一个节点存在 ready replica
而不是:
- 数据库里是否已经有了模板定义这一行。
7. 第六层:AppSnapshot 是控制面分发到节点的关键桥梁
createReplicaOnNode() 做的事情非常关键:
- 克隆模板请求
ensureRuntimeTemplateRequest()补一个新的 runtime request idConstructCubeletReq()生成节点侧请求- 调用目标节点的
cubelet.AppSnapshot(...) - 取回
snapshotPath - 把该节点上的 replica 状态标为 ready / failed
从架构角度看,AppSnapshot 的角色是:
- 把“控制面模板定义”变成“节点本地可恢复快照”
它既不是普通创建请求,也不是最终用户访问的数据面接口,而是模板供应链里的内部转换步骤。
7.1 为什么模板副本不是直接传一个文件就完事
因为节点侧不是只需要一个 ext4 文件,还要把它变成节点本地的运行模板状态。
控制面需要确认:
- 这个模板在这个节点上是否 ready
- snapshot 路径是什么
- 失败在哪个 phase
- 后续是否需要 cleanup
所以 AppSnapshot 更像“节点侧模板安装/编译过程”,而不是纯文件分发。
8. 第七层:运行时请求会在创建前再次被 templateID 展开
到真正用户创建沙箱时,控制面并不是只把 templateID 发给节点,而是会先 ResolveTemplate()。
CubeMaster/pkg/templatecenter/store.go 的 ResolveTemplate() 会:
- 从请求 annotation 里取
templateID GetTemplateRequest(templateID)取出模板定义里的 request_jsonEnsureReadyReplica(templateID)确保至少已有可用副本applyTemplateRequest()把模板请求合并到当前运行请求中
8.1 applyTemplateRequest() 是模板注入的关键
它会把模板定义里的内容补进当前请求:
- annotations
- labels
- volumes
- containers
- network type
- runtime handler
- namespace
注意这里的语义是“补齐”,不是盲目覆盖:
- 如果调用方已经显式传了某些字段,就保留调用方值
- 模板负责填默认结构和基础运行定义
这说明模板在系统里的定位更像:
基础运行骨架 + 调用期可覆写的外层参数
这也是模板能够兼顾复用性与灵活性的原因。
9. 第八层:到了节点侧,templateID 会变成 LocalRunTemplate
到了 Cubelet,真正把 templateID 落成节点本地运行对象的,是 Cubelet/plugins/cube/internals/appsnapshot/appsnapshot_plugin.go。
这个插件在 Create() 阶段会判断:
- 是否带
templateID - 是否是 restore snapshot 场景
- 是否是
cubebox v2
如果满足条件,就调用:
runtemplateManager.EnsureCubeRunTemplate(ctx, templateID)
然后把结果写进:
opts.LocalRunTemplate
这一步非常关键,因为它意味着:
节点侧创建真正消费的,不再是抽象的 templateID,而是本地已经解析好的 LocalRunTemplate。
10. 第九层:LocalRunTemplate 真正进入沙箱创建结构体
在 Cubelet/services/cubebox/cube_container_create.go 里,创建 CubeBox 对象时会把:
LocalRunTemplate: flowOpts.LocalRunTemplate
直接写入本地沙箱对象。
这说明模板在节点上不是一个外围附加状态,而是被写入了沙箱的核心元数据对象中。
后面的运行流程——包括:
- 镜像 / block / snapshot 恢复
- volume 关联
- runtime 组件选择
- 后续 store 查询与索引
都可以继续依赖这份本地模板对象。
所以从节点运行视角,真正重要的不是“用户请求里写了哪个 templateID”,而是:
- 这个 sandbox 当前挂着哪个
LocalRunTemplate
11. 第十层:为什么节点还要给 LocalRunTemplate 建索引
Cubelet/pkg/controller/runtemplate/template_indexer.go 里能看到,节点侧会按这些维度给 LocalRunTemplate 建索引:
TemplateIDImageNamespaceIDBaseBlockNamespaceIDSnapshotID
这说明节点并不是把模板当成一个简单的 JSON 缓存,而是把它视作可检索、可复用、可按底层资产关联的运行实体。
这也解释了为什么模板系统可以支撑:
- 多模板共用某些底层镜像/块设备资产
- 按 snapshot 反查本地模板
- 节点恢复或重建时重新关联模板状态
12. 这条链路里最容易混淆的三组概念
12.1 TemplateDefinition vs Replica
TemplateDefinition:控制面保存“模板语义”Replica:某个节点上模板副本的可用状态
前者回答“模板是什么”,后者回答“模板在哪些节点上可用”。
12.2 RootFS Artifact vs AppSnapshot
RootFS Artifact:镜像导出的可复用根文件系统资产AppSnapshot:节点侧可恢复运行快照
前者更接近“静态基础盘产物”,后者更接近“运行态模板副本”。
12.3 templateID vs LocalRunTemplate
templateID:控制面/请求层引用LocalRunTemplate:节点侧真正使用的运行对象
一个是引用键,一个是本地可执行实体。
13. 用一句话重述整条链路
如果把整条链路压缩成一句话,可以写成:
模板先在 CubeMaster 中被保存为归一化后的创建请求;如果来源于镜像,还要先构建 RootFS Artifact;随后通过 AppSnapshot 在节点上生成 ready replica;真正创建沙箱时,控制面再把 templateID 展开回运行请求,而节点最终把它解析成
LocalRunTemplate注入本地创建流程。
这就是 CubeSandbox 模板系统真正的层次结构。