Velero Node-agent 负载舒缓(Load Soothing)机制:用prepareQueueLength约束数据移动 Pod 与快照的过度创建
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
导读
在 Velero 的 CSI 快照数据移动(Volume Snapshot Data Movement)与文件系统备份(fs-backup)场景中,VGDP 并发限额(loadConcurrency)只有在数据移动 Pod 启动进入running之后才会真正生效,导致大批并行卷备份时会先创建大量“闲置”的数据移动 Pod 与卷快照,白白占用节点 Pod 配额、系统盘空间,甚至拖垮存储性能。本文以 Node-agent Load Soothing 设计文档 为核心,完整讲解 Velero 引入的prepareQueueLength配置项:它如何以“尽力而为(best effort)”的方式在 VGDP 节流之前就先抑制数据移动 CR(DataUpload / DataDownload / PodVolumeBackup / PodVolumeRestore)的进入Accepted/Prepared阶段,从而从源头削减中间对象;并结合仓库源码逐层还原其数据结构、Controller 工作流程、计数缓存与重排队机制,帮助你判断该特性是否适合你的集群环境并正确配置。
VGDP 与数据移动负载:为什么需要“舒缓”
VGDP 与并发限额的既有机制
Velero Generic Data Path(VGDP)是 Velero 在统一仓库(Unified Repository)设计中引入的模块集合,用于完成各类数据传输任务,包括 PodVolume 备份/恢复与卷快照数据移动,其核心组成部分是各类 uploader 与备份仓库(backup repository)。
VGDP 的并发控制机制来源于 Node-agent Concurrency 设计:CSI 快照数据移动的备份/恢复以及 fs-backup 的所有数据移动活动,都遵循node-agent-configmap中配置的loadConcurrency设置;一旦现有负载数量超过对应的并发数,多余负载就会被节流挂起,直到 VGDP 配额释放。
然而,这一节流存在一个关键盲区:它只发生在数据移动 Pod 启动并进入running之后。也就是说,当有大量并发卷备份时,可能会先创建出许多数据移动 Pod,而它们内部的 VGDP 实例实际处于挂起等待状态。
过度创建带来的三类问题
设计文档明确列举了这种“先建后等”模式的危害:
- Pod 配额阻塞:在某些环境中,集群或单节点存在 Pod 数量上限,大量未激活的数据移动 Pod 可能阻塞其他重要 Pod 的调度;
- 系统盘空间侵占:部分环境中节点系统盘容量有限,而 Pod 也占用系统盘空间,大量闲置数据移动 Pod 会挤占空间,导致关键 Pod 被驱逐;
- 快照数量与生命周期失控:对于 CSI 快照数据移动备份,在创建数据移动 Pod 之前卷快照就已创建;由于 VGDP 要等配额释放才开始工作,快照会大量产生并长时间存活。而某些环境中大量快照不被允许,或会造成存储性能下降。
需要强调的是,VGDP 节流本身是精确的控制机制——被节流的正是所需数量的数据移动 Pod;但它“启动后才生效”的时序,决定了必须有一个更前置的“舒缓”机制来避免上述问题的发生。
设计目标与边界
设计文档为本机制划定了明确的目标与非目标:
- 目标 1:允许用户配置期望的、等待 VGDP 负载并发配额的负载数量上限;
- 目标 2:创建一个舒缓机制,当现有负载数量超过预期值时,阻止新负载启动。
- 非目标:不追求从负载发起阶段就进行精确控制。
之所以不追求精确控制,是因为数据移动 Pod 会被调度到哪一组节点几乎无法预测——受所选节点、亲和性(affinity)、节点操作系统等多种复杂因素影响,精确预测不切实际。因此该机制只要求“有效减少”过多闲置数据移动 Pod 与卷快照,而非“精准控制”。
prepareQueueLength:配置结构与作用原理
新增字段与数据结构
解决方案是在node-agent-configmap的loadConcurrency中引入一个新字段prepareQueueLength,它表示允许处于“准备中(preparing/expose)”状态的负载数量。所谓“准备中”,是指负载的 CR 已进入Accepted或Prepared阶段。
设计文档给出的数据结构如下,该结构已在仓库源码 pkg/types/node_agent.go 中原样落地:
type LoadConcurrency struct { // GlobalConfig specifies the concurrency number to all nodes for which per-node config is not specified GlobalConfig int `json:"globalConfig,omitempty"` // PerNodeConfig specifies the concurrency number to nodes matched by rules PerNodeConfig []RuledConfigs `json:"perNodeConfig,omitempty"` // PrepareQueueLength specifies the max number of loads that are under expose PrepareQueueLength int `json:"prepareQueueLength,omitempty"` }在 NodeAgentConfigs 中,LoadConcurrency作为loadConcurrencyJSON 字段被整体挂载在 node-agent 配置上。
生效语义
prepareQueueLength必须是正数,负数会被忽略;- 一旦设置了有效值,舒缓机制即开始生效:以尽力而为的方式,只允许配额数量的 CR 进入
Accepted或Prepared阶段,其余 CR 保持New状态继续等待;由此,数据移动 Pod 与卷快照的创建数量也被限制在配额之内; - 如果未设置(或为无效值),node-agent 保持与传统行为完全一致:CR 一经 Controller 处理就进入
Accepted/Prepared,数据移动 Pod 与卷快照无约束地创建。
设计文档同时给出了一条重要的取舍建议:只有当环境中确实存在 Pod 或卷快照数量约束、需要限制过量的待处理数据移动 Pod 与快照时,才根据 VGDP 负载并发数来设置该值;否则无需使用该特性——并行准备反而有助于提高并发度。
生效时机:启动时读取,修改后需重启
node-agent server 在启动时检查该配置并用它初始化相关的 VGDP 模块。因此用户虽然可以随时编辑 ConfigMap,但要让修改生效,必须重启 node-agent server。
这一点在源码 pkg/cmd/cli/nodeagent/server.go 中有直接体现:当dataPathConfigs.LoadConcurrency.PrepareQueueLength > 0时,调用exposer.StartVgdpCounter启动 VGDP 计数器,并记录日志"VGDP loads are constrained with %d";若启动失败,仅记录告警日志"Failed to start VGDP counter, VDGP loads are not constrained",不会导致 node-agent 崩溃。官方文档 Node-agent Configuration 也明确要求:编辑 ConfigMap 后执行kubectl rollout restart -n <velero-namespace> daemonset/node-agent使配置生效。
ConfigMap 完整示例与创建方法
组合完整配置的示例
设计文档给出了一个同时包含globalConfig、perNodeConfig与prepareQueueLength的完整示例,其中globalConfig设置所有节点的默认并发数,perNodeConfig通过 label selector 为特定节点指定不同并发数:
{ "loadConcurrency": { "globalConfig": 2, "perNodeConfig": [ { "nodeSelector": { "matchLabels": { "kubernetes.io/hostname": "node1" } }, "number": 3 }, { "nodeSelector": { "matchLabels": { "beta.kubernetes.io/instance-type": "Standard_B4ms" } }, "number": 5 } ], "prepareQueueLength": 2 } }上述perNodeConfig的规则结构对应源码中的 RuledConfigs 类型:NodeSelector为匹配节点的 label selector,Number为匹配节点的并发数值。
创建 ConfigMap
将类似上面的示例保存为 JSON 文件后,执行以下命令创建 ConfigMap(官方文档 node-agent-prepare-queue-length 与 node-agent-configmap 中的操作方式一致):
kubectl create cm node-agent-config -n velero --from-file=<json file name>ConfigMap 的生命周期管理由用户负责,Velero 不参与其创建与维护。
将 ConfigMap 提供给 node-agent
安装时可通过velero install --node-agent-configmap=<ConfigMap-Name>指定(见 node-agent-configmap 文档);安装后则编辑 node-agent DaemonSet:
kubectl edit ds node-agent -n velero在spec.template.spec.containers中加入--node-agent-configmap参数:
spec: template: spec: containers: - args: - --node-agent-configmap=<configMap name>只配置prepareQueueLength的简化示例
如果只想开启舒缓机制、沿用默认并发,只需(见 官方文档示例):
{ "prepareQueueLength": 10 }配置项的取值要点(依据 node-agent-configmap 文档):
- 取值范围:从 1 起,无上限;
- 作用范围:全局作用于所有 node-agent Pod 上的 PVB、PVR、DataUpload、DataDownload Pod 的待处理数量;
- 默认值:不设置则无限制;
- 受影响 CR 阶段:DataUpload/DataDownload 处于
Accepted或Prepared阶段的 CR,以及 PodVolumeBackup/PodVolumeRestore 处于准备阶段的 CR。
详细设计:Controller 层面的实现
四条控制链路
该机制涉及四个 Controller 的改动:DataUpload Controller、DataDownload Controller、PodVolumeBackup Controller、PodVolumeRestore Controller。设计文档规定了四步处理流程:
- 舒缓仅作用于处于
New状态的数据移动 CR(DataUpload、DataDownload、PodVolumeBackup 或 PodVolumeRestore); - 开始处理 CR 之前,对应 Controller 统计集群中已存在或等待暴露(expose)的 CR 总数,即所有处于
Accepted或Preparing状态的 DataUpload、DataDownload、PodVolumeBackup、PodVolumeRestore 之和; - 若总数未超过允许值,Controller 将 CR 的阶段置为
Accepted; - 一旦总数超过允许值,Controller 放弃处理该 CR 并稍后重新入队(requeue),重试延迟为5 秒。
源码实现与这四步完全对应。以 data_upload_controller.go 为例,当 CR 阶段为空或为New且未被取消时:
if r.vgdpCounter != nil && r.vgdpCounter.IsConstrained(ctx, r.logger) { log.Debug("Data path initiation is constrained, requeue later") return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil }RequeueAfter: time.Second * 5正是设计文档中的 5 秒重试延迟。相同的判断逻辑同时存在于 data_download_controller.go、pod_volume_backup_controller.go 与 pod_volume_restore_controller.go 中,形成四条统一的约束链路。
VgdpCounter:基于客户端缓存的计数与缓存
设计文档强调,计数发生在所有节点的所有 Controller 上,为避免拖垮 API server,计数基于Controller 客户端缓存(client caches)完成,且计数结果会被缓存,只在必要时才重新统计。
这一设计在 pkg/exposer/vgdp_counter.go 中得到完整实现,核心结构如下:
VgdpCounter为四类 CR(DataUpload / DataDownload / PodVolumeBackup / PodVolumeRestore)各维护一对状态:duState/ddState/pvbState/pvrState(原子变化的changeID)与对应的缓存duCacheState等(缓存的队列长度);initListeners(L58-L164)为每类 CR 注册 Informer 的UpdateFunc事件处理器,监听阶段变化;IsConstrained(L166-L223)通过changeID判断是否需要重新计数:仅当 ID 与缓存 ID 不一致时才用client.List按标签velero.io/expose-on-going=true(常量 ExposeOnGoingLabel)重新统计处于暴露中的 CR 数量,否则直接复用缓存值;最后将四类 CR 的队列长度求和,当existing >= allowedQueueLength时返回“已受限”。
触发重新计数的四种阶段变化
设计文档列出了判断“是否需要重新计数”的四个事件,与initListeners中注册的 UpdateFunc 逻辑一一对应(以 DataUpload 为例,L66-L79):
- 一个或多个 CR 的阶段变为
Accepted(newDu.Status.Phase == DataUploadPhaseAccepted); - 一个或多个 CR 的阶段从
Accepted变为某个终态(oldDu.Status.Phase == Accepted && newDu.Status.Phase != Prepared,即非转入 Prepared 的离开 Accepted 情况); - 一个或多个 CR 的阶段从
Prepared变为某个终态; - 一个或多个 CR 的阶段从
Prepared变为InProgress。
任一事件发生时都会atomic.AddUint64(&w.duState.changeID, 1),使下一次IsConstrained调用感知到状态变化并重新计数;而阶段未变化(oldDu.Status.Phase == newDu.Status.Phase)的事件则直接忽略。DataDownload、PodVolumeBackup、PodVolumeRestore 的处理逻辑完全对称,且均使用了原子操作与uint64changeID,避免并发访问冲突。
单元测试 pkg/exposer/vgdp_counter_test.go 验证了上述行为:例如在允许队列长度为 1 时,仅存在一个带ExposeOnGoingLabel标签的 DataUpload 即为受限;而当四类 CR 混合、总数达到配额时也会被判定受限,印证了“四类 CR 合并计数”的设计。
为什么是“尽力而为”:跨节点同步的取舍
设计文档特别解释了为什么该机制不是精确控制,并给出了理论上的边界分析:
- 跨节点无法精确同步:不同节点的客户端缓存并不一致(coherent),因此不同节点上的 Controller 之间无法精确同步计数;
- 同节点同步代价过高:虽然同一节点内的 Controller 之间理论上可以同步,但步骤 2~3(计数与暴露)本就属于 expose 工作流的一部分,强加同步会损害既有工作流的性能与稳定性;
- 最终一致性仍可保证:即使不做同步,舒缓机制最终依然生效——当 Controller 看到所有已放行的负载(包括预期内的和超额放行的)之后,就会停止创建新负载,直到配额重新可用;
- 窗口极短:需要同步的步骤 2~3 完成得非常快,实际触发最坏情况的概率很低。
基于以上分析,理论上可能出现的超额放行上限为:
最大等待负载数 = prepareQueueLength 数值 + 集群节点数这是指多个同类型 Controller(如不同节点上的 DataUpload Controller)在重叠时间窗口内同时完成计数与放行时的情形。另一种情形是混合负载并发计数(如数据移动备份、数据移动恢复、PodVolume 备份、PodVolume 恢复混跑),超额放行的数量取决于并发混合负载的数目。两种情形下,由于步骤 2~3 耗时极短,达到理论最坏结果的可能性很小——这正是设计文档将其定性为“舒缓(soothing)”而非“精确节流”的根本原因。
与其他设计的关联
prepareQueueLength机制并非孤立存在,它与以下设计文档构成完整的数据移动控制体系:
- Node-agent Concurrency 设计:定义
loadConcurrency与 VGDP 节流,本机制是其前置补充; - Volume Snapshot Data Movement 设计:CSI 快照数据移动,DataUpload/DataDownload 的触发来源;
- VGDP Micro Service 设计:VGDP 微服务的整体架构;
- VGDP Micro Service for fs-backup 设计:fs-backup(PodVolumeBackup/PodVolumeRestore)场景的 VGDP 应用。
使用建议与注意事项
- 按需启用:如果你的环境对每节点/整集群的 Pod 数量、节点系统盘空间或卷快照数量有严格限制,且并行备份/恢复卷数远超可用节点的并行度,建议根据
loadConcurrency的并发总量设置prepareQueueLength;反之,若环境无此类约束,保留默认(不设置)以充分利用并行准备带来的吞吐优势。 - 设置正数:负值会被忽略;值为 0 或不设置均表示不启用舒缓机制。
- 修改后重启:配置只在 node-agent 启动时读取,修改 ConfigMap 后必须
kubectl rollout restart -n velero daemonset/node-agent才能生效(参考 官方文档)。 - 接受“尽力而为”:由于跨节点缓存不一致且无显式同步,实际放行的等待负载数可能短暂超过
prepareQueueLength(理论上限为prepareQueueLength + 节点数),这是设计上的有意取舍,不应视为缺陷。 - 中间对象随之受限:开启后,不仅数据移动 Pod 的数量受到约束,随 Pod 一并创建的中间资源(PVC、VolumeSnapshot、VolumeSnapshotContent 等)也会同步受限,从而缓解 API server 与存储侧压力(参考 官方文档 与 node-agent-configmap 文档)。
总结
prepareQueueLength是 Velero node-agent 在“启动后节流”的精确 VGDP 并发控制之外,补充的一层前置的、尽力而为的负载舒缓机制。它通过让 DataUpload、DataDownload、PodVolumeBackup、PodVolumeRestore 四类 Controller 在将 CR 从New推进到Accepted之前,基于客户端缓存统计全集群“准备中”的负载总量,并在超出配额时以 5 秒间隔延迟重新入队,从而把数据移动 Pod 与卷快照的创建规模约束在可控范围。该机制以pkg/exposer/vgdp_counter.go的VgdpCounter为核心实现,以pkg/cmd/cli/nodeagent/server.go的启动时初始化为入口,并以pkg/controller下四个 Controller 的IsConstrained检查为执行点,构成了一个完整、可测试、可运维的负载控制闭环。
【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考