Velero 恢复依赖等待机制深度解析:RestoreItemAction的AdditionalItems就绪等待设计
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
导读
本文围绕 Velero 恢复(Restore)流程中的一个经典竞态问题展开:当RestoreItemAction插件通过AdditionalItems声明"当前资源依赖的其他资源"时,Velero 默认不会等待这些额外资源真正就绪,就立刻恢复当前资源,从而可能导致恢复失败。文中详细解析了该设计文档(design/Implemented/wait-for-additional-items.md)提出的"等待额外资源就绪"机制:包括RestoreItemAction插件接口新增的AreAdditionalItemsReady方法、RestoreItemActionExecuteOutput新增的WaitForAdditionalItems与AdditionalItemsReadyTimeout字段、WithItemsWait()辅助函数、超时控制与向后兼容策略。读者读完后将掌握该机制的设计动机、接口契约、调用链与插件实现要点,并能在自研恢复插件中正确使用这一能力。
背景:AdditionalItems恢复顺序带来的竞态问题
在 Velero 的恢复流程中,RestoreItemAction插件的Execute()函数除了可以修改被恢复对象本身之外,还可以通过返回值中的AdditionalItems字段声明一批"当前资源恢复前必须先恢复"的关联资源(ResourceIdentifier列表,包含 GroupResource、Namespace 与 Name)。Velero 会先恢复这些额外资源,再恢复当前资源。
但问题在于:"已经执行了恢复"与"已经处于可用状态"是两个概念。Velero 在触发额外资源的恢复操作后并不会等待其真正就绪(ready),而是立即继续恢复当前资源。此时,如果当前资源与额外资源之间存在强依赖关系(例如当前资源引用了额外资源中的字段、需要额外资源的控制器先完成初始化等),当前资源的恢复就可能因为额外资源尚未可用而失败。
这一竞态在 Kubernetes 生态中非常典型:某些自定义资源(CRD)只有在底层的 CRD 定义被 API Server 完全接纳(Established)之后才能创建;某些 Secret、ServiceAccount、存储类(StorageClass)等资源在创建后还需要控制器完成后续处理才能真正被使用。设计文档明确指出了这一点:"Because Velero does not wait after restoring additional items to restore the current item, in some cases the current item restore will fail if the additional items are not yet ready."因此,Velero 需要与插件协同实现"等到额外资源就绪后再恢复当前资源"的能力。
设计目标
- 让 Velero 能够确保:恢复插件
Execute()返回的AdditionalItems在当前资源被恢复之前已经就绪。 - 扩展
RestoreItemAction插件接口,允许插件自行判定"额外资源何时算作就绪"——因为"就绪"的定义高度依赖具体资源类型,只有资源自身的插件才具备这种领域知识。
高层设计:在恢复当前资源前等待额外资源
设计文档给出的高层思路非常清晰:在每次RestoreItemAction.Execute()调用返回、并且其声明的AdditionalItems完成恢复之后,Velero 需要对这些额外资源执行"等待就绪"逻辑,然后再恢复当前资源。
为了实现这一点,需要对RestoreItemActionExecuteOutput结构体进行扩展:让返回了额外资源的插件能够决定是否等待、以及等待多久。整个机制可以拆解为三部分:
restoreItem恢复流程中的等待逻辑(itemsAvailable);RestoreItemAction插件接口新增AreAdditionalItemsReady方法;RestoreItemActionExecuteOutput新增两个可选字段与一个链式辅助函数WithItemsWait()。
详细设计一:restoreItem中的等待逻辑(itemsAvailable)
调用时机:在恢复额外资源之后、恢复当前资源之前
在恢复单个资源时,restoreItem会依次执行所有匹配的RestoreItemAction。当某个插件的Execute()返回后,RestoreItemActionExecuteOutput中携带了必须先行恢复的AdditionalItems切片。此时 Velero 会遍历这些额外资源逐一执行恢复(对应设计文档中所指的 restore.go 中循环恢复额外资源的代码段),完成之后仍然持有两份关键引用:额外资源的标识(GroupResource+ namespaced name)以及要求这些额外资源的插件实例。
就在这个时点——额外资源恢复完毕、但当前资源尚未恢复——插入"等待就绪"逻辑是最合适的。当前仓库中这段调用位于 pkg/restore/restore.go:
var filteredAdditionalItems []velero.ResourceIdentifier for _, additionalItem := range executeOutput.AdditionalItems { // ... 定位备份归档中的额外资源文件、执行恢复(递归调用 restoreItem)... w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace, mustIncludeAdditionalItems) if additionalItemExists { filteredAdditionalItems = append(filteredAdditionalItems, additionalItem) } warnings.Merge(&w) errs.Merge(&e) } executeOutput.AdditionalItems = filteredAdditionalItems available, err := ctx.itemsAvailable(action, executeOutput) if err != nil { errs.Add(namespace, errors.Wrapf(err, "error verifying additional items are ready to use")) } else if !available { errs.Add(namespace, fmt.Errorf("additional items for %s are not ready to use", resourceID)) }从源码可以确认两点实现细节:其一,恢复失败的额外资源会从AdditionalItems切片中被过滤掉(只有additionalItemExists为 true 的资源才会被保留到filteredAdditionalItems),随后再传给就绪等待逻辑;其二,itemsAvailable返回的错误或"未就绪"状态都会作为恢复错误(errs)被记录,其处理方式与"额外资源本身恢复失败"时的错误处理保持一致。
itemsAvailable的具体实现
设计文档提出:当RestoreItemActionExecuteOutput.WaitForAdditionalItems为true时,调用一个与既有crdAvailable(等待 CRD 就绪的既有实现,同样定义在 restore.go 中)类似的函数itemsAvailable。当前仓库中的实现位于 pkg/restore/restore.go:
// itemsAvailable waits for the passed-in additional items to be available for use before letting the restore continue. func (ctx *restoreContext) itemsAvailable(action framework.RestoreItemResolvedActionV2, restoreItemOut *velero.RestoreItemActionExecuteOutput) (bool, error) { // if RestoreItemAction doesn't define set WaitForAdditionalItems, then return true if !restoreItemOut.WaitForAdditionalItems { return true, nil } var available bool timeout := ctx.resourceTimeout if restoreItemOut.AdditionalItemsReadyTimeout != 0 { timeout = restoreItemOut.AdditionalItemsReadyTimeout } err := wait.PollUntilContextTimeout(go_context.Background(), time.Second, timeout, true, func(go_context.Context) (bool, error) { var err error available, err = action.AreAdditionalItemsReady(restoreItemOut.AdditionalItems, ctx.restore) if err != nil { return true, err } if !available { ctx.log.Debug("AdditionalItems not yet ready for use") } // If the AdditionalItems are not available, keep polling (false, nil) // If the AdditionalItems are available, break the poll and return back to caller (true, nil) return available, nil }) if wait.Interrupted(err) { ctx.log.Debug("timeout reached waiting for AdditionalItems to be ready") } return available, err }该实现与设计文档的对应关系可以逐条印证:
- 不等待的默认路径:若插件未设置
WaitForAdditionalItems,itemsAvailable直接返回(true, nil),保持 Velero 原有行为。 - 超时来源:等待的超时默认取自恢复上下文的
resourceTimeout(服务器端--resource-timeout参数,默认 10 分钟,定义见 pkg/cmd/server/config/config.go);若插件在输出中显式设置了AdditionalItemsReadyTimeout(非零),则用插件值覆盖服务器级默认值。 - 轮询策略:以 1 秒为间隔调用插件的
AreAdditionalItemsReady,直到返回true或到达超时;返回错误则立即终止轮询并向上传播。 - 超时后的行为:超时(
wait.Interrupted)不会当作错误返回,而是返回(false, nil)。在调用方restoreItem中,available == false会记录一条错误("additional items for ... are not ready to use"),随后流程继续——也就是说,等待超时不会无限阻塞恢复,Velero 会继续尝试恢复当前资源,与设计文档"if the timeout is reached without ready returning true, velero will continue on to attempt restore of the current item"的描述一致。
详细设计二:RestoreItemAction插件接口新增AreAdditionalItemsReady
要让等待逻辑生效,插件必须有能力回答"这批额外资源是否已就绪"。因此设计文档为RestoreItemAction接口新增了一个方法:
type RestoreItemAction interface { // AppliesTo returns information about which resources this action should be invoked for. // A RestoreItemAction's Execute function will only be invoked on items that match the returned // selector. A zero-valued ResourceSelector matches all resources. AppliesTo() (ResourceSelector, error) // Execute allows the ItemAction to perform arbitrary logic with the item being restored, // including mutating the item itself prior to restore. The item (unmodified or modified) // should be returned, along with an optional slice of ResourceIdentifiers specifying additional // related items that should be restored, a warning (which will be logged but will not prevent // the item from being restored) or error (which will be logged and will prevent the item // from being restored) if applicable. Execute(input *RestoreItemActionExecuteInput) (*RestoreItemActionExecuteOutput, error) // AreAdditionalItemsReady allows the ItemAction to communicate whether the passed-in // slice of AdditionalItems (previously returned by Execute()) // are ready. Returns true if all items are ready, and false // otherwise. The second return value is an error string if an // error occurred. AreAdditionalItemsReady(restore *api.Restore, AdditionalItems []ResourceIdentifier) (bool, string) }值得注意的是,设计文档中的AreAdditionalItemsReady第二个返回值是string(错误字符串),而当前仓库中的 v2 接口实际实现返回的是error类型——例如 pkg/restore/actions/csi/volumesnapshotclass_action.go 中的实现:
func (p *volumeSnapshotClassRestoreItemAction) AreAdditionalItemsReady( additionalItems []velero.ResourceIdentifier, restore *velerov1api.Restore, ) (bool, error) { return true, nil }同时参数顺序也与设计初稿相反(当前实现为additionalItems在前、restore在后)。这说明该设计在落地过程中经历过迭代修正,读者在参考设计文档编写插件时,应以当前仓库 v2 接口的实际签名为准。
源码佐证:v2 插件协议与 RPC 调用链
当前仓库中AreAdditionalItemsReady已经完整落地于插件协议层。从 pkg/plugin/proto/restoreitemaction/v2/RestoreItemAction.proto 可以看到,RestoreItemAction服务中新增了对应的 RPC:
service RestoreItemAction { rpc AppliesTo(RestoreItemActionAppliesToRequest) returns (RestoreItemActionAppliesToResponse); rpc Execute(RestoreItemActionExecuteRequest) returns (RestoreItemActionExecuteResponse); rpc Progress(RestoreItemActionProgressRequest) returns (RestoreItemActionProgressResponse); rpc Cancel(RestoreItemActionCancelRequest) returns (google.protobuf.Empty); rpc AreAdditionalItemsReady(RestoreItemActionItemsReadyRequest) returns (RestoreItemActionItemsReadyResponse); }其中RestoreItemActionItemsReadyRequest携带plugin(插件名)、restore(序列化的 Restore 对象)与additionalItems(repeated generated.ResourceIdentifier),响应RestoreItemActionItemsReadyResponse只含一个ready布尔字段——这也解释了为什么设计文档中该函数只返回(bool, string)而不包含更复杂的就绪信息。
完整的 RPC 调用链为:
- Velero 主进程通过 pkg/plugin/clientmgmt/restoreitemaction/v2/restartable_restore_item_action.go 中的
RestartableRestoreItemAction.AreAdditionalItemsReady发起调用(该方法会先确保插件进程存活再委托给 gRPC 客户端); - gRPC 客户端在 pkg/plugin/framework/restoreitemaction/v2/restore_item_action_client.go 中构造
RestoreItemActionItemsReadyRequest并调用远端 RPC; - 插件侧服务端在 pkg/plugin/framework/restoreitemaction/v2/restore_item_action_server.go 中反序列化
Restore与AdditionalItems,调用插件实现并回传ready。
此外,pkg/plugin/clientmgmt/restoreitemaction/v2/restartable_restore_item_action.go 中还有一个值得注意的适配层:v1 旧版插件适配器AdaptedV1RestartableRestoreItemAction的AreAdditionalItemsReady直接返回true——因为 v1 插件不参与等待逻辑,这保证了旧插件在 v2 协议下的兼容性。
详细设计三:RestoreItemActionExecuteOutput新增字段与WithItemsWait()
两个可选字段
设计文档为RestoreItemActionExecuteOutput新增了两个可选字段,当前仓库的完整定义见 pkg/plugin/velero/restore_item_action_shared.go:
// RestoreItemActionExecuteOutput contains the output variables for the ItemAction's Execution function. type RestoreItemActionExecuteOutput struct { // UpdatedItem is the item being restored mutated by ItemAction. UpdatedItem runtime.Unstructured // AdditionalItems is a list of additional related items that should // be restored. AdditionalItems []ResourceIdentifier // SkipRestore tells velero to stop executing further actions // on this item, and skip the restore step. When this field's // value is true, AdditionalItems will be ignored. SkipRestore bool // v2 and later // OperationID is an identifier which indicates an ongoing asynchronous action which Velero will // continue to monitor after restoring this item. If left blank, then there is no ongoing operation. OperationID string // v2 and later // WaitForAdditionalItems determines whether velero will wait // until AreAdditionalItemsReady returns true before restoring // this item. If this field's value is true, then after restoring // the returned AdditionalItems, velero will not restore this item // until AreAdditionalItemsReady returns true or the timeout is // reached. Otherwise, AreAdditionalItemsReady is not called. WaitForAdditionalItems bool // v2 and later // AdditionalItemsReadyTimeout will override serverConfig.additionalItemsReadyTimeout // if specified. This value specifies how long velero will wait // for additional items to be ready before moving on. AdditionalItemsReadyTimeout time.Duration }两个字段的语义分别如下:
WaitForAdditionalItems(bool):等待开关。为true时,restoreItem会在恢复完AdditionalItems后调用itemsAvailable,进而调用插件的AreAdditionalItemsReady轮询,直到返回true或超时;为false(默认值)时,保持 Velero 原有行为,AreAdditionalItemsReady不会被调用。AdditionalItemsReadyTimeout(time.Duration):单插件超时覆盖项。非零时覆盖服务器全局配置(设计初稿中为serverConfig.additionalItemsReadyTimeout,默认 10 分钟;当前仓库实现中该默认值体现为--resource-timeout的默认值 10 分钟,见 pkg/cmd/server/config/config.go)。这一字段的价值在于:不同依赖类型的"就绪"耗时差异极大,例如等待一个存储类注册可能只需几秒,而等待大规模自定义资源控制器收敛可能需要数分钟,插件可以按需收紧或放宽等待窗口。
在 gRPC 传输层,这两个字段对应 RestoreItemAction.proto 中的bool waitForAdditionalItems = 5;与google.protobuf.Duration additionalItemsReadyTimeout = 6;,客户端在 restore_item_action_client.go 中通过res.WaitForAdditionalItems与res.AdditionalItemsReadyTimeout.AsDuration()完成回填。
链式辅助函数WithItemsWait()
设计文档还提出新增一个与既有WithoutRestore()风格一致的链式函数WithItemsWait(),用于把WaitForAdditionalItems置为true。当前仓库实现见 pkg/plugin/velero/restore_item_action_shared.go:
// WithItemsWait returns RestoreItemActionExecuteOutput with WaitForAdditionalItems set to true. func (r *RestoreItemActionExecuteOutput) WithItemsWait() *RestoreItemActionExecuteOutput { r.WaitForAdditionalItems = true return r }插件中的典型用法
设计文档给出了一个完整的使用范式:插件实现AreAdditionalItemsReady(内部按资源类型执行具体的就绪判定),并在Execute()中通过WithItemsWait()声明需要等待:
func AreAdditionalItemsReady(restore *api.Restore, additionalItems []ResourceIdentifier) (bool, string) { // ... 按资源类型检查每个 additional item 的就绪状态 ... return true, "" } func (p *RestorePlugin) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { // ... 构造 AdditionalItems 与其余逻辑 ... return velero.NewRestoreItemActionExecuteOutput(input.Item).WithItemsWait(), nil }即:先通过NewRestoreItemActionExecuteOutput(input.Item)创建输出,再链式调用.WithItemsWait()打开等待开关(必要时同时设置AdditionalItemsReadyTimeout)。这样插件既声明了"我依赖这些额外资源",又声明了"我需要等待它们就绪",而"就绪"的判定逻辑完全由插件自己掌控。
设计迭代:插件版本化与向后兼容(2021 年 2 月修订)
设计文档后半部分记录了一次重要的实现迭代,值得单独说明——因为它直接影响了接口的最终形态与版本发布节奏。
最初的实现思路是在RestoreItemActionExecuteOutput中存放一个"等待函数指针"(wait func pointer),但实现过程中发现了一个关键约束:Velero 插件调用基于 gRPC + Protocol Buffers 的预定义 RPC 消息格式,函数是固定的 RPC 调用,无法在结构体中直接传递普通的 Go 函数指针(自动生成的 Go 代码不支持这种模式)。因此设计被修正为显式的AreAdditionalItemsReady函数。
由于向RestoreItemAction接口新增方法会破坏与现有插件的向后兼容性(所有已存在的插件都会因缺少该方法而编译失败),设计文档明确建议:该特性的实现应等待 Velero 的插件版本化机制(plugin versioning,对应 upstream issue #3285)落地之后再进行。有了插件版本化之后,不定义AreAdditionalItemsReady的旧版插件(无版本号或 1.0 版本)可以与定义了新方法的 2.0(或 1.1)版本RestoreItemAction插件共存,而不会破坏既有生态。
当前仓库中这一结论已经得到落地验证:AreAdditionalItemsReady完整存在于 v2 插件协议(RestoreItemAction.proto)、v2 gRPC 客户端/服务端(restore_item_action_client.go、restore_item_action_server.go)以及 v1 适配层(restartable_restore_item_action.go,v1 插件适配器恒返回true以保持行为兼容)。这与设计文档"等待插件版本化后再实现"的规划完全一致。
设计文档还指出:迁移到新插件版本后,绝大多数插件其实并不需要等待额外资源。它们应对接口变更的最小改动,仅仅是补上一个恒真实现:
func AreAdditionalItemsReady(restore *api.Restore, additionalItems []ResourceIdentifier) (bool, string) { return true, "" }只要插件从不把WaitForAdditionalItems置为true,这个函数就不会被调用;即便被调用,由于恒返回true,也不会有任何等待开销。
测试与验证
该特性在仓库中拥有对应的单元测试覆盖。在 pkg/restore/restore_test.go 中,测试用的 mock 插件recordResourcesAction与pluggableAction都实现了AreAdditionalItemsReady(见 restore_test.go 与 restore_test.go),并且测试结构体中支持配置WaitForAdditionalItems(见 restore_test.go),用于验证restoreItem在等待开关开启时是否正确地进入就绪轮询路径。
此外,内置的 CSI 恢复插件是理解该接口落地的现成范例:volumeSnapshotClassRestoreItemAction在Execute()中把关联的 lister Secret 作为AdditionalItems返回(见 pkg/restore/actions/csi/volumesnapshotclass_action.go),并实现了恒真的AreAdditionalItemsReady(见同文件 L101-L106)。同一目录下的pvc_action.go、volumesnapshot_action.go、volumesnapshotcontent_action.go也都实现了该方法,共同构成了该特性在内置插件中的最佳实践参照。
总结
"等待AdditionalItems就绪"机制解决了 Velero 恢复流程中一个真实的竞态问题:它把"额外资源已恢复"与"额外资源已可用"两个阶段明确区分开,并借助RestoreItemAction插件自身的领域知识来定义"就绪"。整套机制的三个核心契约可以归纳为:
- 接口层面:
RestoreItemAction新增AreAdditionalItemsReady(additionalItems []ResourceIdentifier, restore *api.Restore) (bool, error),由插件判定就绪状态; - 输出层面:
RestoreItemActionExecuteOutput新增可选字段WaitForAdditionalItems与AdditionalItemsReadyTimeout,并配套WithItemsWait()链式函数; - 流程层面:
restoreItem在恢复完额外资源后调用itemsAvailable,以 1 秒间隔轮询插件就绪判定,超时(默认 10 分钟,可由插件覆盖)后继续尝试恢复当前资源而不是无限阻塞。
同时,该特性依托插件版本化机制平滑落地:v1 旧插件通过适配层恒返回就绪,新插件按需启用等待,既不破坏既有生态,又为依赖敏感型资源(如需要等 CRD Established、等控制器收敛的资源)提供了可靠的恢复保障。
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考