Velero BackupItemAction v2 API 设计详解:异步操作、进度上报与 v1 兼容适配
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
本文深入剖析 Velero 开源项目中 BackupItemAction(BIA)v2 API 的完整设计(对应仓库中的设计文档)。BIA 是 Velero 备份插件体系中针对"单个被备份对象"执行自定义逻辑的核心扩展点;v2 版本在其上引入了异步长时操作能力:插件可以在Execute()返回后继续在后台执行操作,并由 Velero 周期性地查询进度、在超时时发起取消。读完本文,你将掌握 v2 接口的 proto/gRPC 定义、Go 接口签名、OperationProgress进度模型、BackupItemActionV2插件类型,以及让既有 v1 插件零改动平滑运行的适配器实现原理,并能对照仓库源码自行验证与扩展。
一、背景与动机:为什么需要 BIA v2
在 BIA v1 中,Execute()方法被设计为同步调用:插件对单个 Kubernetes 资源执行自定义逻辑(如改写对象、注入注解、备份关联资源),在方法返回时操作必须已经完成。然而真实场景中存在一类执行耗时可能超过单次 RPC 调用生命周期的插件操作,例如:
- 调用外部 API 做数据一致性快照;
- 需要长时间等待云厂商异步任务完成;
- 操作本身是"发起任务 + 后台轮询"模式。
若继续沿用同步模型,Velero 只能阻塞等待,既拖慢备份进度,也容易因进程重启、超时中断而丢失操作状态。BIA v2 正是为 Item Action Progress Monitoring 功能而设计:允许Execute()返回一个operationID来"挂起"一个后台操作,Velero 随即继续处理下一个插件、下一个对象,随后再通过新增的Progress()/Cancel()方法与插件交互。
关键设计取舍在于:该能力是可选特性。不需要异步能力的插件只需返回空的operationID,并让新增方法成为 no-op,完全不影响原有行为。
二、目标与非目标
设计文档明确了边界:
- 目标:允许 BIA
Execute()可选地发起一个长时运行的操作,并上报该操作的进度状态。 - 非目标:不让 Velero 控制长时操作何时开始——操作何时启动、如何调度完全由插件自身决定,Velero 只在操作进行中负责查询与取消。
这一边界保证了 v2 只是对现有同步模型的"增量扩展",而不是对插件执行语义的重新定义。
三、高层设计概览
根据 Plugin Versioning 的版本化策略,v2 设计包含四部分工作:
- 新建 BIAv2 插件
.proto文件,定义新的 gRPC 接口(仓库中落地为 pkg/plugin/proto/backupitemaction/v2/BackupItemAction.proto)。 - 在
plugin/clientmgmt/backupitemaction与plugin/framework/backupitemaction下分别创建 v2 的 Go 文件(客户端管理端与插件框架端)。 - 新增一个插件类型(PluginKind)
BackupItemActionV2,并修改 Velero 备份流程,使其引用 v2 插件而非 v1 插件。 - 创建一个 v1→v2 适配器,使所有存量 BIA v1 插件在备份执行时仍能以 v2 插件的形式被调用。
四、详细设计(一):proto 变更
v2 的BackupItemAction.proto整体沿袭 v1,仅做三处修改。以下代码与仓库中的实际 proto 文件一致:
4.1 ExecuteResponse 新增 operationID 字段
message ExecuteResponse { bytes item = 1; repeated generated.ResourceIdentifier additionalItems = 2; string operationID = 3; repeated generated.ResourceIdentifier postOperationItems = 4; }operationID是本次变更的核心:插件在发起后台操作后,通过该字段返回一个全局唯一的操作标识,供后续Progress()/Cancel()引用。此外新增第 4 个字段postOperationItems(设计文档早期草案中写作itemsToUpdate,最终实现定名为postOperationItems),表示"在所有操作完成后需要被备份的相关资源",仅当operationID非空时才有意义。
4.2 BackupItemAction 服务新增两个 RPC
service BackupItemAction { rpc AppliesTo(BackupItemActionAppliesToRequest) returns (BackupItemActionAppliesToResponse); rpc Execute(ExecuteRequest) returns (ExecuteResponse); rpc Progress(BackupItemActionProgressRequest) returns (BackupItemActionProgressResponse); rpc Cancel(BackupItemActionCancelRequest) returns (google.protobuf.Empty); }4.3 新增请求/响应消息类型
message BackupItemActionProgressRequest { string plugin = 1; string operationID = 2; bytes backup = 3; } message BackupItemActionProgressResponse { generated.OperationProgress progress = 1; } message BackupItemActionCancelRequest { string plugin = 1; string operationID = 2; bytes backup = 3; }三个请求消息统一携带plugin(插件名)、operationID(操作标识)和backup(序列化后的 Backup 对象,供插件在查询/取消时读取上下文);Cancel的响应使用google.protobuf.Empty,因为取消不需要返回值。
4.4 共享消息类型 OperationProgress
OperationProgress被设计为跨插件类型的共享消息,未来 v2 的 RestoreItemAction 与 VolumeSnapshotter 同样需要它,因此放在共享 proto(仓库中为 pkg/plugin/proto/Shared.proto):
message OperationProgress { bool completed = 1; string err = 2; int64 nCompleted = 3; int64 nTotal = 4; string operationUnits = 5; string description = 6; google.protobuf.Timestamp started = 7; google.protobuf.Timestamp updated = 8; }各字段语义:
| 字段 | 类型 | 含义 |
|---|---|---|
completed | bool | 操作是否已完成 |
err | string | 操作出错时的错误信息(空表示无错误) |
nCompleted | int64 | 已完成的工作单元数 |
nTotal | int64 | 总工作单元数(与nCompleted配合表示进度) |
operationUnits | string | 工作单元的名称/单位描述(如 "items"、"bytes") |
description | string | 操作的文字描述,供 Velero 展示 |
started | Timestamp | 操作开始时间 |
updated | Timestamp | 操作状态最近一次更新时间 |
五、详细设计(二):Go 接口定义与新的 PluginKind
5.1 BackupItemAction v2 Go 接口
除了两个新 RPC,接口还增加了一个Name()方法。该方法仅由 Velero 内部使用,用于获取插件注册时使用的名称,不会通过 RPC 委托给插件进程——但它仍必须被实现,以完成接口定义。仓库中的完整接口见 pkg/plugin/velero/backupitemaction/v2/backup_item_action.go:
type BackupItemAction interface { // Name returns the name of this BIA. ...其内容不重要,不会被 RPC 调用, // Velero 插件基础设施将直接实现它,以返回插件注册时使用的名称。 Name() string // AppliesTo 返回该 action 应作用于哪些资源;零值 ResourceSelector 匹配所有资源。 AppliesTo() (velero.ResourceSelector, error) // Execute 允许插件对被备份对象执行任意逻辑(含在备份前修改对象)。 Execute(item runtime.Unstructured, backup *api.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, []velero.ResourceIdentifier, error) // Progress 上报异步操作的进度。 Progress(operationID string, backup *api.Backup) (velero.OperationProgress, error) // Cancel 取消异步操作(如支持);若插件不支持取消,直接返回即可,无需返回错误。 Cancel(operationID string, backup *api.Backup) error }5.2 Execute 返回值的五个组成部分
Execute()的返回签名由 v1 的三元组扩展为五元组:
runtime.Unstructured:修改后的对象(若未修改,可返回原对象,gRPC 服务端会自动回退为原始 item,见下文服务端实现);[]velero.ResourceIdentifier:需要立即备份的附加相关资源;string:operationID,非空表示插件发起了一个异步操作;[]velero.ResourceIdentifier:postOperationItems,需要在所有操作完成后备份的相关资源;error:执行错误。
关于第 4 个返回值,接口注释特别强调:它只在operationID非空时才会被 Velero 关注;只有当操作过程中会更新该资源的 Kubernetes 元数据(且这些元数据在恢复时必须存在)时,才应填入该列表。
5.3 异步操作与备份阶段(Finalize)的约束
接口注释明确了一条重要限制:Finalize 阶段的备份不支持异步操作。当备份 Phase 为Finalizing或FinalizingPartiallyFailed时,插件不应返回operationID——因为备份已经越过"等待插件操作完成"的阶段,此时 Velero 只会为postOperationItems中返回的资源再次调用插件。因此插件在发起操作前应检查传入backup.Status.Phase。
5.4 新增 PluginKind:BackupItemActionV2
设计文档规定新增BackupItemActionV2插件类型,并让备份流程改用该类型。仓库中该类型的注册可见于 pkg/plugin/clientmgmt/process/client_builder.go,其将 v2 插件名映射到biav2.NewBackupItemActionPlugin(...);而 pkg/plugin/clientmgmt/manager.go 则通过GetBackupItemActionsV2()与GetBackupItemActionV2(name)统一获取 v2 action(PluginKindBackupItemActionV2注册表查询后包装为可重启的 v2 action)。
六、源码实现纵深:gRPC 服务端与客户端
6.1 服务端(插件进程侧)
v2 的 gRPC 服务端实现位于 pkg/plugin/framework/backupitemaction/v2/backup_item_action_server.go,每个 RPC 的执行流程都是"通过ServerMux按插件名取出实现 → 调用 → 序列化返回"。几个值得注意的实现细节:
- Execute 的 nil 回退:若插件返回的
updatedItem为 nil(表示未修改对象),服务端会把updatedItemJSON重置为req.Item,避免客户端因空对象反序列化失败:
var updatedItemJSON []byte if updatedItem == nil { updatedItemJSON = req.Item } else { updatedItemJSON, err = json.Marshal(updatedItem.UnstructuredContent()) ... }- Progress 的时间戳转换:Go 侧
OperationProgress的Started/Updated为time.Time,通过timestamppb.New(...)转为 proto 的google.protobuf.Timestamp。 - Name() 占位:服务端的
Name()恒返回空字符串,因为该调用永远不应被委托到插件进程(注释明确说明这一设计)。 - panic 防护:每个 RPC 都通过
defer+common.HandlePanic(recover())将插件 panic 转为 gRPC 错误返回,防止插件崩溃拖垮整个 Velero 进程。
6.2 客户端(Velero 进程侧)
客户端实现在 pkg/plugin/framework/backupitemaction/v2/backup_item_action_client.go,其职责是将 Go 对象序列化为 proto 消息并转发到插件进程:
Execute:将 item 与 backup 分别json.Marshal后放入ExecuteRequest,返回时把AdditionalItems与PostOperationItems逐个还原为velero.ResourceIdentifier(按 Group/Resource/Namespace/Name 组装);Progress:调用后把res.Progress的 8 个字段逐一映射回velero.OperationProgress,其中时间戳用AsTime()还原;Cancel:成功后返回 nil。
6.3 可重启封装:RestartableBackupItemAction
restartable_backup_item_action.go 中的RestartableBackupItemAction是客户端管理端的核心封装:每次方法调用前先通过SharedPluginProcess.ResetIfNeeded()检查插件进程是否存活、必要时自动重启,再按{Kind: PluginKindBackupItemActionV2, Name}取回委托对象执行调用。其中Name()直接返回r.Key.Name——即注册名,这正是设计文档所说"该值不真正重要、但必须实现接口"的落地方式。相应单元测试见同目录下的restartable_backup_item_action_test.go。
七、v1 兼容:适配器如何让老插件零改动运行
设计文档的兼容性目标——"任何既有 BackupItemAction 插件都能按预期工作"——由 restartable_backup_item_action.go 中的AdaptedV1RestartableBackupItemAction实现。其行为完全对应设计文档的描述:
| 方法 | 适配器行为 | 原因 |
|---|---|---|
Execute | 委托 v1 的Execute(item, backup),返回(updatedItem, additionalItems, "", nil, err) | v1 插件没有异步概念,operationID恒为空、postOperationItems恒为 nil |
Progress | 直接返回AsyncOperationsNotSupportedError() | v1 插件永远不会返回 operationID,因此传入的任意 operationID 都是无效的 |
Cancel | 直接返回 nil | v1 插件未实现取消逻辑,静默忽略即可 |
type AdaptedV1RestartableBackupItemAction struct { V1Restartable *biav1cli.RestartableBackupItemAction } func (r *AdaptedV1RestartableBackupItemAction) Execute(item runtime.Unstructured, backup *api.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, []velero.ResourceIdentifier, error) { updatedItem, additionalItems, err := r.V1Restartable.Execute(item, backup) return updatedItem, additionalItems, "", nil, err } func (r *AdaptedV1RestartableBackupItemAction) Progress(operationID string, backup *api.Backup) (velero.OperationProgress, error) { return velero.OperationProgress{}, biav2.AsyncOperationsNotSupportedError() } func (r *AdaptedV1RestartableBackupItemAction) Cancel(operationID string, backup *api.Backup) error { return nil }而AdaptedBackupItemActions()函数将两种插件类型统一注册进可重启机制:PluginKindBackupItemActionV2直接使用原生 v2 封装,PluginKindBackupItemAction(v1)则包装为适配器。这意味着备份流程只需面向统一的 v2 接口编程,不必关心插件底层是 v1 还是 v2。
八、插件作者实践要点
若要开发一个支持异步操作的 v2 BIA 插件,需注意以下要点(综合设计文档与源码注释):
- 发起操作:在
Execute()中自行启动后台任务(goroutine 或外部系统任务),返回唯一的operationID;若不支持异步,返回空字符串即可。 - 检查备份阶段:发起操作前检查
backup.Status.Phase,若为Finalizing/FinalizingPartiallyFailed则不要返回operationID。 - 上报进度:实现
Progress(operationID, backup),返回包含Completed、Err、NCompleted、NTotal、OperationUnits、Description、Started、Updated的OperationProgress;Velero 会周期性轮询直到Completed == true。 - 实现取消:实现
Cancel(operationID, backup)尽量中止操作;若无法取消,直接返回 nil(不视为错误)。 - 必须实现
Name():返回值不会被 Velero 用于定位插件(注册名由基础设施直接提供),但必须实现以满足接口。
九、兼容性、实现时间线与总结
- 兼容性:v1 适配器保证了任何存量 BackupItemAction 插件均可继续工作,无需修改、无需重新编译,属于向后兼容的增量 API 演进。
- 实现时间线:设计文档明确本变更在Velero 1.11 开发周期内实现;当前仓库中的 proto、框架代码、可重启封装与适配器均已落地并配套测试(见
restartable_backup_item_action_test.go与backup_item_action_test.go),印证了设计的完整闭环。
总结而言,BIA v2 是 Velero 插件体系的一次"增量式"演进:它用最少的接口变更(一个返回字段 + 两个 RPC + 一个内部方法),为插件作者打开了异步长时操作与进度上报的空间,同时通过适配器层保障了海量存量 v1 插件的平滑过渡。对于需要在备份流程中执行耗时任务的插件场景(如等待云侧快照完成、调用外部系统协调数据一致性),BIA v2 提供的Progress/Cancel/OperationProgress协议即是标准答案。
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考