Velero 对象图谱清单(Object Graph Manifest)设计解析:为 dry-run、并发备份恢复与图依赖恢复奠定基础
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
导读
本文以 design/graph-manifest.md 设计提案为骨架,深入解析 Velero 提出的Object Graph Manifest(对象图谱清单)数据结构:一个随备份一起存放于对象存储、以最小字段描述备份内容全貌的manifest.json。读完本文,你将理解 Velero 现有备份/恢复在"内容感知"上的局限,掌握清单数据结构的 Go 类型设计、序列化与兼容性策略,并看到它如何为 dry-run、无重叠并行操作以及基于依赖图的复杂应用恢复铺平道路。
背景:为什么 Velero 需要一份"备份内容清单"
Velero 当前的工作方式是:备份时逐个处理对象,按 API Group 与命名空间排序;恢复时同样逐个处理对象,借助restoreResourcePriorities标志指定各 API Group 的对象恢复先后顺序。
这种线性处理模式对大多数应用足够,但对依赖关系呈图状而非严格线性的复杂应用存在明显挑战。文档以 Cluster API(CAPI)为例:CAPI 集群是一组复杂的 Kubernetes 对象,要求"根对象"先于"叶子对象"恢复——如果ClusterResourceSetBinding所引用的Cluster尚不存在,整个 CAPI 集群的恢复就会失败。
此外,Velero 缺少一种**在不真正执行操作的前提下可靠告知用户"哪些对象会受影响"**的机制。这会带来两个连锁问题:
- dry-run 困难:用户必须真实执行备份/恢复动作,才能知道会触碰哪些对象;
- 并发受限:当前无法判断某个 Kubernetes 对象是否同时被多个备份或恢复覆盖。一旦 Velero 走向更高并发,就可能导致不可靠、死锁乃至竞态条件。
目标与非目标
目标(Goals)
- 引入一个定义备份内容的数据结构(Manifest);
- 将 Manifest 数据与既有备份数据一并存入对象存储。
非目标(Non Goals)
本提案明确说明,以下能力是"被开启"而非"被定义"的,不会与数据结构的引入同期实现:
- 在 Velero 现有并发之外实现更高并发;
- 实现 dry-run 特性;
- 实现新的恢复排序流程。
数据结构设计必须为上述场景留出空间,但它们不属于本提案的交付范围。
高层设计:用最小字段唯一标识一个 Kubernetes 对象
要在一个集群或备份内唯一标识 Kubernetes 对象,以下字段已经足够(见 design/graph-manifest.md):
- API Group 与 Version(例如
backup.velero.io/v1) - Namespace
- Name
- Labels
这组标准覆盖了 Velero 绝大部分的包含/排除(inclusion/exclusion)逻辑。此外,以下附加字段可进一步解锁更多用例:
- Owners:与该对象存在某种关系的其他 Kubernetes 对象,可能是严格依赖,也可能是软依赖;
- Annotations:对象额外的元数据,方便其他程序消费;
- UUID:Kubernetes 生成的唯一标识,用于定义 Owner 关系,为对象提供单一且不可变的主键。注意:UUID在恢复时不会被考虑,仅内部用于定义对象间链接。
为什么不直接解析备份 tarball?
上述信息其实都已存在于 Velero 备份 tarball 中,但提取成本高昂:必须完整下载并解压整个 tarball,再解析其中的 JSON 才能读到 labels、owners、annotations 和 UUID;其余信息则编码在 tarball 的目录结构里。可行但"重量级",耗时且可能消耗大量内存。
从仓库源码可以印证这种结构的实际形态。备份 tarball 内的对象文件路径由 pkg/archive/filesystem.go 的GetVersionedItemFilePath生成:
path := filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, versionPath, GetScopeDir(namespace), namespace, name+".json")即形如<root>/resources/<groupResource>/<version>/<cluster 或 namespaces>/<namespace>/<name>.json(命名空间级对象)与<root>/resources/<groupResource>/<version>/cluster/<name>.json(集群级对象)。对应测试见 pkg/archive/filesystem_test.go。
因此,提案主张新增一种与备份 tarball 并排存放的 Manifest 结构,它只包含上述字段,可用来:
- 对备份执行包含/排除逻辑;
- 从备份中选择特定资源;
- 对备份或恢复内容做集合运算,识别重叠资源。
清单将解锁的三类场景
- 备份/恢复的 dry-run:先构建并保存一份 Manifest,让用户看到"如果执行会选中什么",确认后再真正执行备份;恢复操作同理。
- 无重叠的高效并行:在执行备份/恢复前先构建或读取 Manifest,判断资源是否存在重叠——无重叠则并行执行,有重叠则串行执行。
- 面向非线性依赖的图式恢复:Kubernetes 集群中并非所有资源都能用严格线性方式定义,它们可能有多个 Owner。与其要求插件作者在
BackupItemAction/RestoreItemAction里手工返回一长串 Owner 链,不如由 Velero 基于包含足够信息的 Manifest,自动构建"依赖先于被依赖者恢复"的离散列表,从而大幅降低插件作者的负担。
详细设计:Go 数据结构与接口
核心类型定义
Manifest 数据结构的 Go 类型设计如下(原文完整摘录自 design/graph-manifest.md):
// NamespacedItems maps a given namespace to all of its contained items. type NamespacedItems map[string]*Item // APIGroupNamespaces maps an API group/version to a map of namespaces and their items. type KindNamespaces map[string]NamespacedItems type Manifest struct { // Kinds holds the top level map of all resources in a manifest. Kinds KindNamespaces // Index is used to look up an individual item quickly based on UUID. // This enables fetching owners out of the maps more efficiently at the cost of memory space. Index map[string]*Item } // Item represents a Kubernetes resource within a backup based on it's selectable criteria. // It is not the whole Kubernetes resource as retrieved from the API server, but rather a collection of important fields needed for filtering. type Item struct { // Kubernetes API group which this Item belongs to. // Could be a core resource, or a CustomResourceDefinition. APIGroup string // Version of the APIGroup that the Item belongs to. APIVersion string // Kubernetes namespace which contains this item. // Empty string for cluster-level resource. Namespace string // Item's given name. Name string // Map of labels that the Item had at backup time. Labels map[string]string // Map of annotations that the Item had at Backup time. // Useful for plugins that may decide to process only Items with specific annotations. Annotations map[string]string // Owners is a list of UUIDs to other items that own or refer to this item. Owners []string // Manifest is a pointer to the Manifest in which this object is contained. // Useful for getting access to things like the Manifest.Index map. Manifest *Manifest }结构要点:
Manifest.Kinds采用三级映射(Kind → Namespace → Items),天然对齐 Velero 按 API Group 与命名空间组织的处理模型;Manifest.Index是以 UUID 为键的索引表,用于快速定位某个 Item,从而高效解析 Owner 关系——以内存换取查询效率;Item刻意不保存完整的 Kubernetes 资源对象,只保存过滤所需的关键字段,这正是内存开销可控的根本原因;Item.Owners存的是其他 Item 的 UUID 列表,配合Index即可在清单内完成图遍历。
便捷接口与哨兵错误
除数据类型外,提案还提供以下 Go 接口:
type Itermer interface { // Returns the Item as a string, following the current Velero backup version 1.1.0 tarball structure format. // <APIGroup>/<Namespace>/<APIVersion>/<name>.json String() string // Owners returns a slice of realized Items that own or refer to the current Item. // Useful for building out a full graph of Items to restore. // Will use the UUIDs in Item.Owners to look up the owner Items in the Manifest. Owners() []*Item // Kind returns the Kind of an object, which is a combination of the APIGroup and APIVersion. // Useful for verifying the needed CustomResourceDefinition exists before actually restoring this Item. Kind() *Item // Children returns a slice of all Items that refer to this item as an Owner. Children() []*Items } // This error type is being created in order to make reliable sentinel errors. // See https://dave.cheney.net/2019/06/10/constant-time for more details. type ManifestError string func (e ManifestError) Error() string { return string(e) } const ItemAlreadyExists = ManifestError("item already exists in manifest") type Manifester interface { // Set returns the entire list of resources as a set of strings (using Itemer.String). // This is useful for comparing two manifests and determining if they have any overlapping resources. // In the future, when implementing concurrent operations, this can be used as a sanity check to ensure resources aren't being backed up or restored by two operations at once. Set() sets.String // Adds an item to the appropriate APIGroup and Namespace within a Manifest // Returns (true, nil) if the Item is successfully added to the Manifest, // Returns (false, ItemAlreadyExists) if the Item is already in the Manifest. Add(*Item) (bool, error) }接口设计的意图值得展开:
String()将 Item 序列化为符合备份 tarball 目录约定的字符串<APIGroup>/<Namespace>/<APIVersion>/<name>.json。这与上述 pkg/archive/filesystem.go 中的 tarball 路径规则相呼应,只是 Manifest 用更轻量的方式表达了同一份"位置信息";Owners()/Children()借助Item.Owners(UUID 列表)与Manifest.Index在清单内正向/反向遍历依赖图,是构建"依赖优先恢复顺序"的直接入口;Kind()返回 APIGroup + APIVersion 的组合,可用于在真正恢复某个 Item 前校验所需 CRD 是否存在;Manifester.Set()把整份清单变成字符串集合,用于比较两份 Manifest 的重叠情况——这正是未来并发操作前的"健全性检查"(sanity check)原语;Add()幂等地向 Manifest 写入 Item,重复写入时返回哨兵错误ItemAlreadyExists。文档特意将错误定义为ManifestError字符串类型,借鉴 Go 社区"常量时间错误"(constant-time sentinel error)实践,使调用方可以用errors.Is/==做可靠判断。
序列化:manifest.json 的存储形态
整个Manifest会被序列化到单个备份对应的manifest.json文件中,存放于对象存储内;文档同时指出该文件可考虑压缩以节省空间。
从当前仓库的持久化层可以直观看到这种"备份目录下并列多个附属文件"的既有模式。pkg/persistence/object_store_layout.go 定义了备份目录下的各类产物 key,例如:
velero-backup.json(备份元数据)<backup>.tar.gz(备份内容 tarball)<backup>-logs.gz(日志)<backup>-resource-list.json.gz(资源清单)<backup>-podvolumebackups.json.gz、<backup>-volumesnapshots.json.gz、<backup>-itemoperations.json.gz等
而 pkg/persistence/object_store.go 中的BackupInfo与PutBackup方法(见 object_store.go)则把这些io.Reader逐个上传到对象存储。manifest.json正是要作为这一类新增附属文件加入上传流程,即文档"Implementation"一节所述:在persistence包中确保新文件"可上传且被允许(uploadable and allowed)"。
内存考量
由于Manifest只保存最小字段集合(而非完整对象),对绝大多数集群而言内存不应成为顾虑。文档在此留有一个 TODO:记录 API Group 名称、资源名称与 Kind 名称的已知字符长度限制,作为后续明确的上限依据。
安全与兼容性
安全(Security Considerations)
引入 Manifest不会扩大 Velero 的攻击面:清单中的字段本就存在于既有备份数据中;将manifest.json放在既有备份数据旁也不会改变访问模式(仍受同样的对象存储凭证与 BSL 配置约束)。
兼容性(Compatibility)
manifest.json的引入对应Velero 备份版本 1.2.0(即备份格式版本随之提升);- 该文件是**纯增量(additive)**的,不会干扰不支持
Manifest的旧版 Velero 读取备份; - 长期看,
manifest.json将取代<backup>-resource-list.json.gz文件,但为兼容起见,两者会并存一段时期(正如 object_store_layout.go 所展示的resource-listkey 仍然存在); - 首次落地时,Velero 只需在备份 Item 的过程中顺带构建 Manifest,并在备份结束时序列化保存。任何依赖
Manifest的逻辑变更都必须以独立的设计文档另行提出,并自带各自的兼容性考量。
实现路径:从类型到上传的落地点
文档明确了实现方向(design/graph-manifest.md):
Manifest不会实现为 Kubernetes CustomResourceDefinition,而是 Velero 的内部构造(internal construct);- 数据结构的实现量应尽量小:在独立的
manifest包中定义上述类型; - 备份流程创建
Manifest,并将其传递给backup包中的各个*Backupper,由这些方法逐个插入Item; - 在
persistence包中新增逻辑,确保新的manifest.json可被上传且被允许。
从源码结构看,pkg/backup包中的 item_backupper.go、backed_up_items_map.go、backup.go 正是文档所描述的"各种 Backupper"所在的实现位置;它们逐项采集对象并维护去重映射,天然适合在采集过程中同步向 Manifest 插入 Item。持久化侧则以 pkg/persistence/object_store.go 的PutBackup为参照,为manifest.json增加对应的 key 生成与上传逻辑。
开放问题与后续展望
文档以三个开放问题收尾,为社区后续讨论留白:
- 何时正式移除对
<backup>-resource-list.json.gz的兼容? - 除 Cluster API 外,还有哪些适合验证该特性的测试用 Kubernetes 资源/控制器?(Cluster API 是显而易见的选择,但显然不止于此)
- 既然 Manifest 不是 CRD,如何保留一份 Manifest 以便用户先执行 dry-run 再执行真实操作?一个思路是存放在 Velero 的临时目录中——但需要指出,这会让 Velero 自身变得更"有状态"(stateful)。
总结
Object Graph Manifest 是 Velero 在"备份内容可感知性"上的关键设计增量:它以一份极轻量的manifest.json随备份存入对象存储,用最小的字段集合完整描述备份内容与对象间依赖关系。它本身不直接交付 dry-run、并发或新恢复排序,但通过Index、Owners、Set()等精心设计的原语,为这些更高阶的能力提供了坚实的结构性基础——这也是本文将其称为 Velero 后续架构演进"地基"的原因。对这一提案感兴趣的读者,可继续阅读原始设计文档 design/graph-manifest.md,并结合 pkg/persistence/object_store_layout.go 与 pkg/archive/filesystem.go 理解其落地的存储与归档上下文。
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考