
Velero Backup 性能改进ItemBlock 分组备份与多 Worker 并发处理机制全解析【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero导读本篇文章以 Velero 仓库中的 backup-performance-improvements 设计文档 为核心深入剖析 Velero 为提升单次备份吞吐量而引入的ItemBlock条目块分组机制与多 Worker 线程并发备份架构以及它们如何为未来的VolumeGroupSnapshot卷组快照能力铺路。读完本文你将理解 Velero 如何识别必须一起备份的资源集合、如何将 Pod 钩子Hook的执行粒度从单条目标提升到 ItemBlock 级别、如何通过item-block-worker-count参数配置并发 Worker 数量以及这套设计在当前仓库源码中的真实落地形态。一、背景单线程备份流程的性能瓶颈在引入 ItemBlock 之前Velero 主备份控制器backup controller以单线程方式运行对 ItemCollector条目收集器返回的每一个资源依次完成主备份处理流程后才会处理下一个资源。这种一次一个的处理模型在资源量较大时会显著拉长备份总耗时。设计文档指出面对大规模备份时通常存在两种典型场景少量大卷的备份大部分耗时发生在异步阶段——例如 CSI 快照创建动作在snaphandleVolumeSnapshotContent 的快照句柄出现之后的处理以及 DataUpload数据上传的处理。这种场景下并行化带来的收益有限。大量小卷的备份大部分耗时发生在同步动作阶段。尤其是 CSI 快照创建时数千个卷的 VSCsnaphandle等待会累计出可观的等待时间。这类场景将从并行条目处理中获益最大——这正是本次性能改进设计的主要目标场景。设计文档 Goals 章节 给出的核心目标有三条识别需要一起备份的相关条目组ItemBlock将备份钩子的管理从每条目提升到 ItemBlock 级别使用 Worker 线程并发备份多个 ItemBlock。同时明确了若干非目标Non Goals本设计不实现 VolumeGroupSnapshots 的正式支持但包含其前置条件、不并行处理多个备份、不重构内部插件的 RPC 调用基础设施、不涉及 Restore恢复性能改进。二、总体设计ItemBlock 概念设计的基石是一个新的类型ItemBlock。本质上ItemBlock 是一组为保证备份完整性而必须一起备份的条目集合。未来将条目备份拆分到多个 Worker 线程时ItemBlock 会作为备份的基本单元被整体保留在同一 Worker 中处理。典型的 ItemBlock 例子包括一个 Pod、其挂载的 PVC以及这些 PVC 绑定的 PV一个 VolumeGroup相关联的 PVC 与 PV以及所有挂载这些卷的 Pod对于一个 ReadWriteManyRWXPVC该 PVC、其绑定的 PV以及所有挂载该 PVC 的 Pod。为了让 Velero 能够识别这种分组关系设计引入了一个新的插件类型ItemBlockActionIBA——任何必须与其他资源一起备份的资源都需要为其定义对应的 IBA 插件。设计分为两个开发阶段Phase 1重构备份工作流识别应一起备份的条目块并在块内协调备份钩子的执行Phase 2为条目块处理增加多个 Worker 线程——不再在识别出块后立即备份而是把块送入共享 Channel由空闲 Worker 取走处理。三、Phase 1 详解ItemBlock 处理3.1 新的 ItemBlockAction 插件类型设计文档解释了一个关键动机现有的BackupItemActionBIA插件的Execute方法虽然也能返回当前条目需要的附加条目但 Velero 需要在开始备份条目之前就预先知道这些关联关系。因此需要一个独立的新插件类型通过GetRelatedItems方法提前告知 Velero 哪些条目应当并入同一个块。这些由 IBA 返回的条目应当与 BIAExecute返回的附加条目保持一致但那些只有调用Execute之后才创建的条目不应返回因为它们此时尚不存在。设计文档给出了 ItemBlockAction 的 proto 定义经 protoc 编译为 Go 代码service ItemBlockAction { rpc AppliesTo(ItemBlockActionAppliesToRequest) returns (ItemBlockActionAppliesToResponse); rpc GetRelatedItems(ItemBlockActionGetRelatedItemsRequest) returns (ItemBlockActionGetRelatedItemsResponse); } message ItemBlockActionAppliesToRequest { string plugin 1; } message ItemBlockActionAppliesToResponse { ResourceSelector ResourceSelector 1; } message ItemBlockActionGetRelatedItemsRequest { string plugin 1; bytes item 2; bytes backup 3; } message ItemBlockActionGetRelatedItemsResponse { repeated generated.ResourceIdentifier relatedItems 1; }同时新增一个ItemBlockAction的PluginKind备份流程将使用该插件类型。设计文档特别指出任何 BIA 插件若在Execute()中返回需要在同一 Worker 内同步或顺序备份的附加条目就应当新增一个配套的 IBA 插件返回同样的条目去掉那些在 BIAExecute()调用前尚不存在的。这主要适用于操作 Pod 且其引用的资源受 Pod 钩子影响、必须随 Pod 一起备份的插件以及需要把多个 Pod 的卷同时备份的插件。3.2 新的数据结构BackupItemBlock、ItemBlock、ItemBlockItem设计文档给出了三个核心结构体的定义。在 pkg/itemblock/itemblock.go 中可以找到它们的真实实现package backup type BackupItemBlock struct { itemblock.ItemBlock // This is a reference to the shared itemBackupper for the backup itemBackupper *itemBackupper } package itemblock type ItemBlock struct { Log logrus.FieldLogger Items []ItemBlockItem } type ItemBlockItem struct { Gr schema.GroupResource Item *unstructured.Unstructured PreferredGVR schema.GroupVersionResource }从源码实现看ItemBlock还提供了两个实用方法pkg/itemblock/itemblock.goAddUnstructured(gr, item, preferredGVR)向块内追加条目FindItem(gr, namespace, name)在块内查找条目——当启用EnableAPIGroupVersions时可能返回多个版本的条目其中与preferredGVR匹配的版本排在前面。而BackupItemBlock在 pkg/backup/itemblock.go 中通过NewBackupItemBlock(log, itemBackupper)构造其addKubernetesResource方法会从 ItemCollector 生成的临时文件中读取条目内容读后即删除临时文件执行itemInclusionChecks排除检查然后将其加入块内。3.3 对 ItemCollector 结果循环的修改当前未改造前的工作流中BackupWithResolvers函数遍历 ItemCollector 返回的条目列表对每个条目从 ItemCollector 生成的临时文件加载条目内容、调用backupItem、成功后更新 GR 映射、删除临时文件、更新备份进度。设计改造后的循环逻辑如下部分逻辑应抽出为辅助函数以便对GetRelatedItems返回的条目递归调用循环开始前创建指向BackupItemBlock的指针表示当前正在处理的块若条目的inItemBlock为true说明已在块中直接跳过若当前itemBlock为nil则创建之将item加入itemBlock从 ItemCollector 文件加载条目加载后关闭并删除文件若存在同一条目的其他版本启用EnableAPIGroupVersions时一并加入itemBlock为条目获取匹配的 IBA 插件并调用GetRelatedItems对返回的每个条目若在条目列表中则从 ItemCollector 文件获取完整内容否则从集群拉取加入当前块、加入itemsInBlock映射并递归对每个条目重复上述步骤若当前条目与下一条目都是同一 GR 的有序条目ordered items则继续循环加入当前itemBlock块生成完毕后调用backupItemBlock(block)将backupItemBlock的返回值合并进backedUpGroupResources映射。为此ItemCollector 使用的kubernetesResource结构体需要增加两个布尔字段orderedResource当资源因有序资源列表而被移到每个 GroupResource 的开头时置为true。之所以需要标记是因为 ItemCollector 虽然已把有序资源排在前面但列表中并无信息区分哪些是来自有序资源列表、哪些是剩余的无序条目——而处理时每个 GroupResource 的有序资源必须按顺序在同一个 ItemBlock 中顺序处理inItemBlock在处理列表时条目被加入某个 ItemBlock 后置为true。3.4 新的 backupItemBlock 函数把钩子提升到块级别设计文档给出的函数签名是func (kb *kubernetesBackupper) backupItemBlock(block BackupItemBlock) []schema.GroupResource返回值是已备份资源的 GroupResource 切片。Velero 依靠它确定备份中需要包含哪些 CRD——不仅包含直接备份的资源还包括 BIAExecute()通过附加条目间接备份的资源。钩子处理逻辑先取block.items过滤出其中尚未被备份的 Pod依据block.itemBackupper.backupRequest.BackedUpItems对这批 Pod 逐一执行 pre 钩子逻辑从itemBackupper.backupItemInternal中抽出随后遍历block.items全部条目调用backupItem——虽然后面这些条目大多已备份过但重复调用无害因为backupItem第一步就会检查BackedUpItems映射并直接返回这样做的目的是兜底防止某些插件在GetAdditionalItems返回了条目却忘记在Execute的附加条目返回值中包含它导致条目漏备份最后用与 pre 钩子相同的过滤后的 Pod 列表执行 post 钩子。在 pkg/backup/backup.go 中可以看到该函数的真实实现其流程与设计完全一致遍历itemBlock.Items筛选出Gr kuberesource.Pods且尚未在BackedUpItems中的 Pod得到preHookPods调用handleItemBlockPreHooks执行 pre 钩子对钩子失败的 Pod打印错误并将其标记为已备份BackedUpItems.AddItem(key)避免卡死流程遍历块内所有条目调用backupItem备份并汇总grList若存在待执行的 post 钩子 Pod调用handleItemBlockPostHooks。值得注意的细节是 post 钩子的特殊处理handleItemBlockPostHookspkg/backup/backup.go在真正执行 post 钩子前会调用waitUntilPVBsProcessedpkg/backup/backup.go等待该块内所有 Pod 的 PodVolumeBackupPVB全部处理完成Completed 或 Failed 状态——因为 post 钩子往往依赖卷数据备份结果必须先等 PVB 结束才能安全执行。3.5 backupItemInternal 清理钩子逻辑从itemBackupper.backupItemInternal中迁出后需要从该方法中删除钩子处理代码避免同一 Pod 的钩子被执行两次。3.6 Finalize 阶段不受影响设计的 finalize收尾阶段不会被 ItemBlock 设计影响——它只是在各条目异步操作完成后更新资源状态没有并行执行的必要。四、Phase 2 详解单备份内多线程处理 ItemBlock4.1 新增配置项item-block-worker-countVelero 安装器install CLI与服务端serverCLI 都会新增输入字段itemBlockWorkerCount并传递到backupReconciler。从当前仓库源码看该配置已落地pkg/cmd/server/config/config.go 中服务端注册了--item-block-worker-count标志注释为 Number of worker threads to process ItemBlocks. Default is one. Optional.默认值由DefaultItemBlockWorkerCount 1pkg/cmd/server/config/config.go定义pkg/cmd/cli/install/install.go 中velero install命令同样提供--item-block-worker-count可将该值写入安装后的部署清单服务端启动时在 pkg/cmd/server/server.go 将配置传给备份控制器。4.2 ItemBlockWorkerPool共享 Channel WaitGroup设计文档引入了一个新的类型ItemBlockWorker最终实现命名为ItemBlockWorkerPool它管理一组处理条目块的 Worker goroutine、一个向 Worker 传递块的共享输入 Channel以及用于控制器退出时优雅关闭的 WaitGroup。其核心结构pkg/backup/item_block_worker_pool.go如下type ItemBlockWorkerPool struct { inputChannel chan ItemBlockInput wg *sync.WaitGroup logger logrus.FieldLogger cancelFunc context.CancelFunc } type ItemBlockInput struct { itemBlock *BackupItemBlock returnChan chan ItemBlockReturn } type ItemBlockReturn struct { itemBlock *BackupItemBlock resources []schema.GroupResource err error }Worker 池由StartItemBlockWorkerPool(ctx, workers, log)启动输入 Channel 的缓冲容量为max(workers, 10)即固定缓冲区至少能容纳 10 个待处理块并启动workers个 goroutine各自运行processItemBlockWorker。每个 Worker 循环从inputChannel读取ItemBlockInput调用itemBackupper.kubernetesBackupper.backupItemBlock(...)处理块把结果ItemBlockReturn含返回的 GR 列表与错误发送到该输入自带的returnChan然后处理下一个块收到ctx.Done()时优雅退出并wg.Done()。Stop()方法通过取消 context 并wg.Wait()等待所有 Worker 退出。4.3 修改处理循环把块送入 Worker 池而非内联备份Phase 1 实现的 ItemBlock 处理循环将被改造为把每个新建的 ItemBlock 发送到共享 Channel而不是内联调用BackupItemBlock并用 WaitGroup 管理进行中的块同时为本次备份单独启动一个 goroutine 处理返回值。循环完成后Velero 用 WaitGroup 等待所有块处理完毕再继续后续流程。设计文档给出了简化示意// omitting cancel handling, context, etc ret : make(chan ItemBlockReturn) wg : sync.WaitGroup{} // Handle returns go func() { for { select { case response : -ret: // process each BackupItemBlock response func() { defer wg.Done() responses append(responses, response) }() case -ctx.Done(): return } } }() // Simplified illustration, looping over and assumed already-determined ItemBlock list for _, itemBlock : range itemBlocks { wg.Add(1) inputChan - ItemBlockInput{itemBlock: itemBlock, returnChan: ret} } done : make(chan struct{}) go func() { defer close(done) wg.Wait() }() // Wait for all the ItemBlocks to be processed select { case -done: logger.Info(done processing ItemBlocks) } // responses from BackupItemBlock calls are in responses处理响应时核心动作是为每个返回的 GR 设置backedUpGroupResources[item.groupResource]true——这与当前实现逐条处理并设置该字段的效果完全一致。4.4 有序资源的顺序保证处理循环被拆分为两轮迭代第一轮只处理循环开头被标记为orderedResources的条目这些资源生成的块送入 Worker Channel 后必须等待其响应再继续下一个块第二轮处理剩余条目生成的块直接送入 Worker Channel无需等待响应从而让这些块并行处理。这样设计的原因在于有序资源是用户指定的必须最先备份、且按特定顺序备份的资源列表因此必须逐个串行执行pkg/backup/backup.go 中orderedResource标记与第一轮等待逻辑相互配合。4.5 BackedUpItems 映射的并发安全Velero 用BackedUpItems映射追踪已备份的条目防止重复备份并防止附加条目间的循环依赖造成死循环。由于并行 goroutine 会同时访问该映射必须用互斥锁mutex同步对它的访问。在 pkg/backup/request.go 中可以印证这一并发化改造Request结构体包含BackedUpItems *backedUpItemsMap具备线程安全访问能力的专用类型、WorkerPool *ItemBlockWorkerPool、ResolvedItemBlockActions []framework.ItemBlockResolvedAction以及为并发访问而采用sync.Map的NamespaceFilterCache还有带锁的SynchronizedVSListVolumeSnapshots——这些都是并发备份流程的直接证据。五、备选方案对比5.1 备选方案一给 BackupItemAction 增加 GetAdditionalItems 方法与其新增ItemBlockAction插件类型也可以直接在BackupItemAction上加一个GetAdditionalItems方法。该方案被否决原因是新插件类型提供了更干净的接口将条目分组的职责与修改条目内容用于备份的职责彻底分离。5.2 备选方案二Per-backup Worker 池当前设计采用永久 Worker 池备份控制器启动时创建。与之相对的方案是处理备份时临时创建、备份完成后销毁的临时 Worker 池。两者的用户可见 API 差异在于配置语义永久 Worker 池worker 数量代表所有备份共享的总并发处理数并发备份数代表同时运行的备份数。任何时刻并发备份条目的最大数量等于 worker 数。例如 worker15、concurrent-backups3 时最多同时处理 15 个条目分摊给最多 3 个运行中的备份Per-backup Worker 池worker 数量代表每个备份各自拥有的并发处理数。同样 worker15、concurrent-backups3 时最多同时处理 45 个条目每个备份各 15 个。两种方案的取舍永久 Worker 池优势更符合 Kubernetes 常见模式遵循标准实践更稳妥用户更容易理解最大并发处理条目数直接影响性能与 Velero Pod 的资源需求无需心算相乘为未来并发备份增强保留更多灵活性——例如备份优先级共享 Worker 池可以优先取走高优先级备份的 ItemBlock使大而低优先级的备份被高优先级备份抢占无需显式中断该备份的主控制器流程。Per-backup Worker 池优势内存占用更低但阻塞在输入上的 Worker 内存消耗本就很小若只有 1020 个 Worker差异可忽略。六、兼容性与插件示例PodAction 的 IBA 化设计文档给出了一个BIA 插件返回附加条目时配套 IBA 实现示例取自内部pod_action.go——它识别给定 Pod 所需的条目。由于该插件的唯一功能就是返回附加条目可以直接改造成 IBA 插件如果插件还有修改 Pod 内容的动作则相关条目与内容操作需要拆分到不同插件。该示例已经在当前仓库中落地为 pkg/itemblock/actions/pod_action.go实际签名为GetRelatedItems(item runtime.Unstructured, backup *v1.Backup) ([]velero.ResourceIdentifier, error)逻辑提取到 pkg/util/actionhelpers/pod_helper.go 的RelatedItemsForPod// PodAction implements ItemBlockAction. type PodAction struct { log logrus.FieldLogger } // NewPodAction creates a new ItemBlockAction for pods. func NewPodAction(logger logrus.FieldLogger) *PodAction { return PodAction{log: logger} } // AppliesTo returns a ResourceSelector that applies only to pods. func (a *PodAction) AppliesTo() (velero.ResourceSelector, error) { return velero.ResourceSelector{ IncludedResources: []string{pods}, }, nil } // GetRelatedItems scans the pods spec.volumes for persistentVolumeClaim volumes and returns a // ResourceIdentifier list containing references to all of the persistentVolumeClaim volumes used by // the pod. This ensures that when a pod is backed up, all referenced PVCs are backed up along with the pod. func (a *PodAction) GetRelatedItems(item runtime.Unstructured, backup *v1.Backup) ([]velero.ResourceIdentifier, error) { a.log.Info(Executing pod ItemBlockAction) defer a.log.Info(Done executing pod ItemBlockAction) pod : new(corev1api.Pod) if err : runtime.DefaultUnstructuredConverter.FromUnstructured(item.UnstructuredContent(), pod); err ! nil { return nil, errors.WithStack(err) } return actionhelpers.RelatedItemsForPod(pod, a.log), nil } func (a *PodAction) Name() string { return PodItemBlockAction }RelatedItemsForPod会扫描 Pod 的spec.volumes找出所有persistentVolumeClaim卷同时补充 Pod 的 PriorityClass返回对应的ResourceIdentifier列表确保 Pod 备份时其引用的 PVC 一并备份。仓库中还实现了更多 IBA 插件展示了分组逻辑的多样性pkg/itemblock/actionsPVCActionpvc_action.go当 PVC 处于Bound状态且spec.volumeName非空时返回其绑定的 PV随后通过getPVCList找到所有挂载该 PVC 的 Pod一并加入块——这保证了多个 Pod 挂载同一个 RWX 卷时能一起备份更进一步它会基于备份 Spec 中的VolumeGroupSnapshotLabelKey卷组快照标签键通过getGroupedPVCs找出同一命名空间内具有相同labelKeygroupID标签的其他 PVC 加入块内使它们在同一 ItemBlock 中被处理——这正是设计文档所说为 VolumeGroupSnapshot 打基础的直接体现ServiceAccountActionservice_account_action.go处理 ServiceAccount 相关的关联条目。这三个插件均可在 pkg/itemblock/actions 下找到配套的单元测试如 pod_action_test.go。七、实现节奏与当前仓库落地状态设计文档明确Phase 1 与 Phase 2 可以在同一个 Velero 发布周期内实现但并非必须——Phase 1 预计在 Velero 1.15 落地Phase 2 预计在 Velero 1.16 落地。从当前仓库源码结构看两阶段设计均已实现并共存ItemBlock 相关核心类型位于 pkg/itemblockItemBlock/ItemBlockItem及 IBA 插件备份侧包装与处理逻辑位于 pkg/backupBackupItemBlockitemblock.go、backupItemBlock及钩子处理backup.go、ItemBlockWorkerPoolitem_block_worker_pool.go配置入口为--item-block-worker-countpkg/cmd/server/config/config.go 与 pkg/cmd/cli/install/install.go默认值为 1用户可按需调大以提升大量小卷场景的备份吞吐。八、总结与实践建议ItemBlock 机制是 Velero 备份性能演进的关键一步它先以ItemBlockAction插件从语义层面把必须一起备份的资源聚合成块再将 Pod 钩子提升到块级别统一编排最后通过ItemBlockWorkerPool让多个 Worker 并发消费块从而在不牺牲备份完整性的前提下显著提升大量小卷场景的备份速度。与此同时块级分组天然是 VolumeGroupSnapshot 的前置能力——PVCAction依据VolumeGroupSnapshotLabelKey把同组 PVC 聚合进同一块已经为卷组快照铺好了路。实践建议如果你的集群以大量小 PVC CSI 快照为主可以适当调大服务端与velero install的--item-block-worker-count并配合--concurrent-backups理解整体并发语义同时注意 Worker 增多会提高 Velero Pod 的资源消耗需要同步评估内存与 CPU 限额。延伸阅读本文对应的完整设计文档见 design/Implemented/backup-performance-improvements.md与卷组快照相关的前置设计可参考 design/Implemented/volume-group-snapshot.md 与 design/Implemented/vsv2-design.md。【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考