设计解析:为 Volume Snapshot Data Movement 备份精确约束 VGDP 运行节点)
Velero node-agent 节点亲和性loadAffinity设计解析为 Volume Snapshot Data Movement 备份精确约束 VGDP 运行节点【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero导读在 Kubernetes 环境下Velero 通过 node-agentDaemonSet承载数据面VGDP实例来完成卷快照数据移动Volume Snapshot Data Movement备份而数据面实例可以调度到任意允许调度的节点上运行。本文基于仓库中的《Node-agent Load Affinity Design》设计文档design/Implemented/node-agent-affinity.md完整解析其提出的loadAffinity配置机制如何通过一个 ConfigMap 为卷快照数据移动备份的 VGDP 实例指定亲和性affinity与反亲和性anti-affinity规则并结合源码验证从配置解析、按 StorageClass 匹配到翻译为 Pod 亲和性的完整实现链路。读完本文你将掌握 node-agent 节点亲和性的设计动机、配置语法、创建方法以及调度失败时的排障思路。背景为什么 VGDP 实例需要节点亲和性相关概念GlossaryVelero Generic Data PathVGDPVGDP 是 Unified Repository 设计 中引入的一组模块集合。Velero 使用这些模块完成各类数据搬运任务如 PodVolume 备份/恢复、Volume Snapshot Data Movement包括 uploader 以及备份存储库backup repository。ExposerExposer 是 Volume Snapshot Data Movement 设计 中引入的模块。Velero 使用它将卷快照暴露给 node-agent Pod或其关联 Pod以便从快照完成数据移动。node-agent 与 VGDP 的运行模型Velero node-agent 是一个以 DaemonSet 方式运行的组件承载控制器与 VGDP 模块完成备份/恢复的具体工作包括 PodVolume 备份/恢复和 Volume Snapshot Data Movement 备份/恢复。具体来说node-agent 在每个节点上各运行一个 DataUpload 控制器实例监听 DataUpload CR某个控制器实例接管一个 DataUpload CR 后会在本节点启动一个 VGDP 实例该实例初始化 uploader 与备份存储库连接完成数据搬运VGDP 实例运行在 node-agent Pod 内部或运行在与该节点上 node-agent Pod 关联的 PodbackupPod中。受数据量、数据复杂度、可用资源等因素影响VGDP 可能耗时较长并消耗可观的 CPU、内存、网络带宽等资源。从技术上来说VGDP 实例可以运行在任何允许 Pod 调度的节点上但用户可能出于多种原因希望约束其运行节点某些节点上运行着更关键的业务负载希望 VGDP 实例不要抢占这些节点某些节点资源更充裕希望将 VGDP 实例集中调度到这些节点存储仅在特定节点上支持卷/快照的供给希望 VGDP 实例只在这些节点运行。因此为了让数据面适应更广泛的部署拓扑有必要为 VGDP 实例配置节点亲和性——尤其是对于 VGDP 实例运行频繁且集中的备份场景。设计目标与边界Goals / Non-Goals设计文档明确了如下目标Goal 1定义 node-agent 中卷快照数据移动备份场景下 VGDP 实例的节点亲和性行为Goal 2建立一套让用户为卷快照数据移动备份指定 VGDP 实例节点亲和性的机制。同时设计文档也明确划定了不做Non-Goals的边界避免功能被盲目扩展不支持 PodVolume 备份/恢复的亲和性因为 PodVolume 备份/恢复的 VGDP 实例必须运行在源/目标 Pod 所在的节点无法迁移到其他节点不支持数据移动恢复restore的亲和性例如当 StorageClass 的volumeBindingMode为WaitForFirstConsumer时恢复卷必须挂载到目标 Pod 被调度的节点VGDP 实例必须与目标 Pod 同节点同时考虑到恢复场景通常不会频繁、集中地运行因此暂不支持当前实现仅考虑 Exposer 的 backupPod 暴露方式如 Volume Snapshot Data Movement 设计 所述Exposer 可能以不同方式暴露快照当前只支持通过 backupPod 这一种方式。未来若引入新的暴露方式亲和性配置的定义与行为仍然有效但可能需要新的实现。配置载体node-agent ConfigMap通过--node-agent-configmap指定亲和性配置存放在由velero node-agentCLI 参数--node-agent-configmap指定的 ConfigMap 中。该参数在 pkg/cmd/cli/nodeagent/server.go 中注册command.Flags().StringVar(config.nodeAgentConfig, node-agent-configmap, config.nodeAgentConfig, The name of ConfigMap containing node-agent configurations.)需要说明的约束条件该 ConfigMap不是 Velero 自动创建的用户需要按需手动创建ConfigMap 必须位于 Velero 安装的同一命名空间若在不同命名空间中安装了多个 Velero 实例则每个命名空间各有一个 ConfigMap且只作用于该命名空间内的 node-agentnode-agent 服务端在启动时检查这些配置并用于初始化相关 VGDP 模块。用户随时可以编辑该 ConfigMap但要让修改生效需要重启 node-agent 服务端。从源码看node-agent 服务端通过 getDataPathConfigs 在启动阶段读取该 ConfigMap并将结果保存到s.dataPathConfigs中供后续初始化 VGDP 相关模块使用。ConfigMap 中的数据结构ConfigMap 内部新增一种名为loadAffinity的配置。设计文档给出了如下数据结构type Configs struct { // LoadConcurrency is the config for load concurrency per node. LoadConcurrency *LoadConcurrency json:loadConcurrency,omitempty // LoadAffinity is the config for data path load affinity. LoadAffinity []*LoadAffinity json:loadAffinity,omitempty } type LoadAffinity struct { // NodeSelector specifies the label selector to match nodes NodeSelector metav1.LabelSelector json:nodeSelector }这一结构在仓库中已落地为真实类型。在 pkg/types/node_agent.go 中NodeAgentConfigs是 node-agent 配置的完整载体包含LoadConcurrency、LoadAffinity、BackupPVCConfig、RestorePVCConfig、PodResources、PriorityClassName、PodAnnotations、PodLabels等多项配置LoadAffinity类型定义于 pkg/types/node_agent.go。而实际承载亲和性配置的kube.LoadAffinity结构体定义在 pkg/util/kube/pod.go它在设计文档的基础上增加了一个关键字段type LoadAffinity struct { // NodeSelector specifies the label selector to match nodes NodeSelector metav1.LabelSelector json:nodeSelector // StorageClass specifies the VGDPs the LoadAffinity applied to. If the StorageClass doesnt have value, it applies to all. If not, it applies to only the VGDPs that use this StorageClass. StorageClass string json:storageClass }也就是说实际实现支持按 StorageClass 细化亲和性storageClass为空时配置对全局生效非空时仅对使用该 StorageClass 的 VGDP 生效详见下文“按 StorageClass 匹配亲和性”一节。为什么loadAffinity是数组用户可能希望根据不同的条件如不同的 StorageClass、CSI 驱动所代表的存储设置不同的 LoadAffinity 配置因此设计文档将loadAffinity定义为数组。这是出于可扩展性考虑——当前版本并不支持多配置因此如果数组中存在多条配置实现上总是取数组中的第一条。而在实际的代码演进中配合StorageClass字段GetLoadAffinityByStorageClass 已经实现了按 StorageClass 精确匹配、未匹配时回退到全局配置StorageClass为空的那条的查找逻辑。亲和性与反亲和性的语义loadAffinity内的nodeSelector使用 Kubernetes 标准的metav1.LabelSelector通过两种方式表达亲和/反亲和规则Affinity亲和性允许 VGDP 实例在指定节点运行MatchLabelsMatchLabels中的标签默认表示LabelSelectorOpIn操作因此在本上下文中会被当作亲和规则处理——只有带这些标签的节点才允许运行 VGDP 实例MatchExpressions标签定义在MatchExpressions的Key和Values中Operator必须设置为LabelSelectorOpIn或LabelSelectorOpExists。Anti-affinity反亲和性禁止 VGDP 实例在指定节点运行通过MatchExpressions定义Operator必须设置为LabelSelectorOpNotIn或LabelSelectorOpDoesNotExist。需要提醒的是亲和性与反亲和性规则位于同一个nodeSelector内且相互独立地作用于调度过程——最终的调度结果是“同时满足所有亲和条件、且不落入任何反亲和条件”的节点集合。完整示例与创建方法设计文档给出了一个完整的 ConfigMap 数据示例同时涵盖了亲和性与反亲和性{ loadAffinity: [ { nodeSelector: { matchLabels: { beta.kubernetes.io/instance-type: Standard_B4ms }, matchExpressions: [ { key: kubernetes.io/hostname, values: [ node-1, node-2, node-3 ], operator: In }, { key: xxx/critial-workload, operator: DoesNotExist } ] } } ] }该示例包含两条亲和性配置和一条反亲和性配置规则类型配置内容语义亲和性matchLabelsbeta.kubernetes.io/instance-typeStandard_B4msVGDP 实例只会在带该标签的节点上运行亲和性matchExpressionskubernetes.io/hostnameIn[node-1, node-2, node-3]VGDP 实例只会运行在 node-1、node-2、node-3 上反亲和性matchExpressionsxxx/critial-workloadDoesNotExistVGDP 实例不会运行在带有xxx/critial-workload标签的节点上创建 ConfigMap用户需要先把类似上述示例的内容保存为一个 JSON 文件然后执行以下命令创建 ConfigMapkubectl create cm ConfigMap name -n velero --from-filejson file name例如将示例保存为load-affinity.json后kubectl create cm node-agent-config -n velero --from-fileload-affinity.json创建完成后需要确保 node-agent 的启动参数--node-agent-configmap指向该 ConfigMap 名称并重启 node-agent 使配置生效。实现原理loadAffinity 如何作用于备份 Pod从 LabelSelector 到 corev1.Affinity 的翻译如 Volume Snapshot Data Movement 设计 所述Exposer 决定 VGDP 实例在哪里启动。目前对于卷快照数据移动备份Exposer 会创建 backupPodVGDP 实例将随 backupPod 的调度结果在对应节点上启动。因此loadAffinity会被翻译从metav1.LabelSelector翻译为corev1.Affinity并设置到 backupPod 上。翻译逻辑实现在 pkg/util/kube/pod.go 的ToSystemAffinity函数中将MatchLabels中的每个标签键值对转换为NodeSelectorOpIn的NodeSelectorRequirement将MatchExpressions中的每条表达式原样转换为NodeSelectorRequirement保留其Operator从而保留In/NotIn/Exists/DoesNotExist等语义将所有 requirement 汇总到NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution的NodeSelectorTerms中若调用方提供了卷拓扑volumeTopology即存储拓扑还会将卷拓扑的NodeSelectorTerms一并合并进来确保备份 Pod 既能满足用户亲和性又能访问快照卷。在 CSI 快照 Exposer 的实现 pkg/exposer/csi_snapshot.go 中先通过kube.GetLoadAffinityByStorageClass按备份 PVC 的 StorageClass 取出匹配的亲和性配置随后在 csi_snapshot.go 处调用kube.ToSystemAffinity(affinity, volumeTopology)生成 Pod 亲和性并在创建 backupPod 时通过Affinity: podAffinitycsi_snapshot.go将其注入 Pod 规格。创建成功后日志会记录Backup pod is created及其携带的 affinitycsi_snapshot.go。按 StorageClass 匹配亲和性GetLoadAffinityByStorageClass 的查找逻辑如下遍历loadAffinity数组优先查找StorageClass与备份 PVC 的 StorageClass 完全匹配的配置命中即返回深拷贝避免污染共享配置若未找到匹配项则回退到StorageClass为空全局的那条配置两者都没有时返回空即不施加亲和性约束。该函数返回的是配置的深拷贝deepCopy见 pkg/util/kube/pod.go确保调用方修改结果时不会破坏 node-agent 共享的配置对象。与 node-agent DaemonSet 调度的配合设计文档特别强调了一个容易踩坑的点node-agent 作为 DaemonSet并不一定在每个工作节点上都运行 Pod用户可以通过给 node-agent DaemonSet 指定nodeSelector或nodeAffinity来限制其分布而当前实现中VGDP 实例必须被调度到有 node-agent Pod 运行的节点上因此如果 node-agent 本身有节点选择配置用户必须将其一并考虑进 loadAffinity 配置中以保证 VGDP 实例始终被调度到存在 node-agent 的节点上这一责任在用户侧Velero 不会继承 node-agent DaemonSet 的任何节点选择配置——因为设计文档认为 DaemonSet 调度器与普通 Pod 调度器的工作方式不同简单地全量继承可能引发 backupPod 调度出现意外结果。如果 backupPod 被调度到没有 node-agent Pod 的节点对应的 DataUpload CR 将停留在Accepted阶段直到 prepare 超时默认 30 分钟。调度失败的排查与处理快照卷不可达导致的调度失败作为 expose 操作的一部分Exposer 会从快照创建一个卷由 backupPVC 表示。backupPVC 与源卷使用相同的 StorageClass若该 StorageClass 的volumeBindingMode为Immediate卷会在等待 backupPod 之前立即从底层存储分配此时如果 backupPod 被调度到快照卷不可访问的节点例如因存储拓扑限制backupPod 将无法进入 Running 状态数据移动无法完成。设计文档给出了该问题发生时 backupPod 的状态示例status: conditions: - lastProbeTime: null message: 0/2 nodes are available: 1 node(s) didnt match Pods node affinity/selector, 1 node(s) had volume node affinity conflict. preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.. reason: Unschedulable status: False type: PodScheduled phase: Pending从消息中可以清晰地看到两类原因节点不匹配 Pod 的 node affinity/selector即用户配置的亲和性排除了所有可用节点以及卷节点亲和性冲突存储拓扑约束与调度冲突。Prepare 超时与诊断机制问题发生后backupPod 停留在Pending阶段对应 DataUpload CR 停留在Accepted阶段直到 prepare 超时默认 30 分钟后 backupPod 被删除超时时间由 node-agent 的--data-mover-prepare-timeout参数控制默认值为 30 分钟见 pkg/cmd/cli/nodeagent/server.go超时逻辑位于 DataUpload 控制器的 onPrepareTimeout当time.Since(du.Status.AcceptedTimestamp.Time)超过preparingTimeout时触发data_upload_controller.go将 DataUpload 状态消息置为timeout on preparing data upload。由于 backupPod 在 prepare 超时后即被删除用户无法再据此判断失败原因究竟是上述哪一类。为此设计文档提出可以增加诊断机制在因 prepare 超时删除 backupPod 之前先收集同一节点上 backupPod 与 node-agent 的状态信息用于排障。仓库中已有对应的诊断工具函数 DiagnosePod它会汇总 Pod 的 phase、node name、status message、各 condition 以及相关 Warning 事件可用于生成上述场景的诊断报告。小结loadAffinity机制为 Velero 卷快照数据移动备份提供了将 VGDP 实例约束到指定节点的能力使用户能够避开运行关键业务的节点、将备份集中到资源充裕的节点、或贴合存储拓扑限制。配置以数组形式存于--node-agent-configmap指定的 ConfigMap 中支持基于metav1.LabelSelector的亲和/反亲和表达实现上由 Exposer 在创建 backupPod 时经ToSystemAffinity翻译后注入 Pod 调度约束并支持按 StorageClass 精确匹配。使用时需注意 node-agent DaemonSet 自身的节点选择必须同步纳入亲和性配置同时要警惕存储拓扑冲突导致的Unschedulable与 prepare 超时问题。参考链接设计文档design/Implemented/node-agent-affinity.mdVGDP 上游设计design/Implemented/unified-repo-and-kopia-integration/unified-repo-and-kopia-integration.mdVolume Snapshot Data Movement 设计design/Implemented/volume-snapshot-data-movement/volume-snapshot-data-movement.mdnode-agent 服务端配置加载pkg/cmd/cli/nodeagent/server.gonode-agent 配置结构定义pkg/types/node_agent.go亲和性翻译与按 StorageClass 匹配pkg/util/kube/pod.goCSI 快照 Exposer 对亲和性的应用pkg/exposer/csi_snapshot.goDataUpload 控制器 prepare 超时逻辑pkg/controller/data_upload_controller.go【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考