Skip to content

模板如何变成可运行沙箱:Template、RootFS Artifact、AppSnapshot 与 LocalRunTemplate

在 CubeSandbox 里,模板不是一句“启动某个镜像”的别名。

从源码看,一个模板至少跨越 4 层对象:

  1. TemplateDefinition:控制面保存的模板定义,请求语义层
  2. RootFS Artifact:从 OCI 镜像导出的可复用根文件系统产物
  3. AppSnapshot / Replica:分发到节点上的可恢复运行快照
  4. 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() 为什么还要再删字段

模板存储时并不是把原始请求原封不动落库,而是会去掉一些运行期字段,例如:

  • SnapshotDir
  • Timeout
  • InsId / InsIp
  • Request
  • AppSnapshotCreate 标记

这说明控制面保存模板时,有意把它压缩成:

  • 可复用
  • 去运行时噪音
  • 与某次具体调用解耦

模板定义

也正因为如此,模板后来才能再次被 GetTemplateRequest() 取出,并重新赋予新的 RequestID 用于实际运行。


3. 第二层:TemplateDefinition 是“控制面真相”,不是节点真相

CreateTemplate() 在一开始就会执行 createDefinition(),把模板定义写入数据库中的 TemplateDefinition

这个定义里保存的关键内容包括:

  • template_id
  • instance_type
  • version
  • status
  • request_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 上可运行的运行时根盘。

中间至少要经历:

  1. 拉取 / inspect 源镜像
  2. 导出 rootfs
  3. 迁移到 artifact store
  4. 制作 ext4 镜像
  5. 计算 SHA256 与大小
  6. 生成下载 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 / NodeIP
  • Spec
  • SnapshotPath
  • Status
  • Phase
  • ArtifactID
  • ErrorMessage

这说明模板并不是全局单体对象,而是“定义 + 多节点副本”的组合。

6.2 为什么要有 Replica

因为真正运行沙箱时,节点不能每次都回控制面临时现做模板。

节点需要本地已经准备好的:

  • snapshot
  • rootfs/base block
  • 运行时元数据

所以模板是否 ready,本质上是:

  • 是否至少有一个节点存在 ready replica

而不是:

  • 数据库里是否已经有了模板定义这一行。

7. 第六层:AppSnapshot 是控制面分发到节点的关键桥梁

createReplicaOnNode() 做的事情非常关键:

  1. 克隆模板请求
  2. ensureRuntimeTemplateRequest() 补一个新的 runtime request id
  3. ConstructCubeletReq() 生成节点侧请求
  4. 调用目标节点的 cubelet.AppSnapshot(...)
  5. 取回 snapshotPath
  6. 把该节点上的 replica 状态标为 ready / failed

从架构角度看,AppSnapshot 的角色是:

  • 把“控制面模板定义”变成“节点本地可恢复快照”

它既不是普通创建请求,也不是最终用户访问的数据面接口,而是模板供应链里的内部转换步骤。

7.1 为什么模板副本不是直接传一个文件就完事

因为节点侧不是只需要一个 ext4 文件,还要把它变成节点本地的运行模板状态。

控制面需要确认:

  • 这个模板在这个节点上是否 ready
  • snapshot 路径是什么
  • 失败在哪个 phase
  • 后续是否需要 cleanup

所以 AppSnapshot 更像“节点侧模板安装/编译过程”,而不是纯文件分发。


8. 第七层:运行时请求会在创建前再次被 templateID 展开

到真正用户创建沙箱时,控制面并不是只把 templateID 发给节点,而是会先 ResolveTemplate()

CubeMaster/pkg/templatecenter/store.goResolveTemplate() 会:

  1. 从请求 annotation 里取 templateID
  2. GetTemplateRequest(templateID) 取出模板定义里的 request_json
  3. EnsureReadyReplica(templateID) 确保至少已有可用副本
  4. 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 建索引:

  • TemplateID
  • ImageNamespaceID
  • BaseBlockNamespaceID
  • SnapshotID

这说明节点并不是把模板当成一个简单的 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 模板系统真正的层次结构。


14. 建议继续阅读