Kubernetes 选择性恢复与冲突处理
一次配置误删只影响 orders 命名空间里的一个租户。值班人员却直接执行整份备份恢复:旧 Deployment 覆盖了刚发布的镜像,历史 Service 尝试占用当前 clusterIP,PVC 因同名对象已经存在而被跳过,恢复任务最终显示 Completed,业务数据却没有回到期望水位。真正的问题不是工具不会恢复,而是团队没有先回答三个问题:要取回哪些事实,目标集群里哪些事实必须保留,发生冲突时谁拥有最终决定权。
选择性恢复不是给全量恢复命令追加几个 --include-* 参数。它是一场受控合并:备份对象、目标对象和目标平台默认值共同竞争同一个名字、字段和外部资产。安全链路应先生成恢复清单,在隔离命名空间证明数据可用,再按冲突类型决定跳过、改名、转换、替换或人工接管。
先把恢复请求翻译成对象集合
事故请求经常只有一句“恢复订单服务”。Kubernetes 里并不存在一个天然的“服务备份单元”:Deployment 依赖 ServiceAccount、ConfigMap、Secret 和 CRD;PVC 依赖 StorageClass、CSI driver、快照或数据仓库;Ingress 还依赖证书、DNS 和外部负载均衡器。只按 app=orders 标签筛选,可能漏掉没有统一标签的 RBAC、ExternalSecret、Operator CR 和跨命名空间依赖。
恢复前先建立集合清单,至少记录 GVK、命名空间、名称、源 UID、备份点、数据角色和处理动作:
recoveryPoint: rp-orders-alpha
targetCluster: recovery-cluster
items:
- gvk: apps/v1/Deployment
namespace: orders
name: orders-api
action: transform-and-create
- gvk: v1/Service
namespace: orders
name: orders-api
action: preserve-target-network-fields
- gvk: v1/PersistentVolumeClaim
namespace: orders
name: orders-data
action: restore-as-new-pvc
- gvk: external-secrets.io/v1/ExternalSecret
namespace: orders
name: orders-db
action: recreate-reference-only
excluded:
- kind: Secret
reason: target identity system recreates credentials
- kind: Ingress
reason: traffic remains isolated until business validation这个清单是恢复合同,不应包含 Secret 明文、对象存储密钥或 KMS key material。源 UID 用来追踪对象身份,但不能复制成新对象的身份;Kubernetes 在对象重新创建时会分配新 UID,同名不等于同一个对象。
五类冲突决定五种合并机制
第一类是名称冲突。目标集群已有同 GVK、同命名空间、同名称对象。默认跳过最保守,改名或映射命名空间最适合隔离验证,覆盖只适合已经证明目标对象可以丢弃的场景。
第二类是不可变字段冲突。Service 的 clusterIP、Deployment/StatefulSet 的 selector、PVC 的大部分绑定属性、Job 模板等字段不能靠普通 patch 改回历史值。处理方式通常是保留目标字段、恢复为新名字,或在明确停机窗口删除重建,而不是不断重试。
第三类是字段所有权冲突。GitOps、Operator、HPA、平台 webhook 和恢复工具可能同时管理副本数、镜像、标签或策略。managedFields 能显示 server-side apply 的字段经理;强制夺取字段会让原控制器下一轮再次写回,形成振荡。
第四类是引用冲突。ownerReference 指向旧 UID,RoleBinding 指向不存在的 ServiceAccount,Webhook 指向未就绪 Service,PV 的 claimRef 仍指向源命名空间。对象能创建并不代表引用闭合。
第五类是外部资产冲突。同名 PVC 可能绑定了不同云盘,LoadBalancer Service 可能创建新地址,ExternalSecret 可能从目标账户读到另一份凭证。Kubernetes YAML 无法单独判断哪块盘、哪把 key 或哪个 DNS 记录才正确。
预检目标集群,不让恢复任务替你做决定
示例以 Velero v1.18 稳定文档描述的 Restore API 为口径。实际 CLI、server、插件与 Kubernetes 版本要落在各自支持矩阵内。执行者需要读取 Backup、Restore、目标对象和 Event;创建恢复时只授予批准的资源与命名空间权限。仓库身份保持只读,删除备份和修改保留策略使用另一条审批链。
export BACKUP=orders-rp-alpha
export SOURCE_NS=orders
export STAGE_NS=restore-orders-alpha
kubectl config current-context
kubectl version
velero version
velero backup describe "$BACKUP" --details
velero backup logs "$BACKUP" > /tmp/${BACKUP}.log
kubectl get ns "$STAGE_NS" --ignore-not-found
kubectl api-resources | sort > /tmp/target-api-resources.txt
kubectl get crd,storageclass,volumesnapshotclass
kubectl auth can-i create restores.velero.io -n velero
kubectl auth can-i create deployments.apps -n "$STAGE_NS"
kubectl auth can-i get secrets -n "$SOURCE_NS"最后一条应按职责设计为 no:恢复操作者不必读取源业务 Secret 明文。若备份详情显示 PartiallyFailed、卷保护方法不明、目标缺少 CRD/StorageClass,或者仓库无法只读访问,应停止恢复并补齐证据。
把目标现状导出为冲突基线。导出文件含敏感配置时必须进入加密事故制品库,不要提交 Git:
kubectl get deploy,sts,svc,sa,role,rolebinding,pvc \
-n "$SOURCE_NS" -o yaml > /secure-incident/target-before.yaml
kubectl get deploy orders-api -n "$SOURCE_NS" -o json \
| jq '{uid:.metadata.uid,rv:.metadata.resourceVersion,managed:.metadata.managedFields,
image:.spec.template.spec.containers[].image,replicas:.spec.replicas}' \
> /secure-incident/orders-api-before.jsonresourceVersion 只用于并发判断,不应从备份原样写回;status、uid、creationTimestamp 和多数 server-populated 字段也不属于可移植期望状态。
正向实验:恢复到隔离命名空间
先创建没有入口、默认拒绝出站的隔离命名空间。真实 NetworkPolicy 还要按所用 CNI 能力放行 DNS、对象仓库、KMS 和测试依赖;如果 CNI 不实施 NetworkPolicy,YAML 存在也没有隔离效果。
kubectl create namespace "$STAGE_NS"
kubectl label namespace "$STAGE_NS" recovery.example.io/point="$BACKUP"
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: restore-orders-alpha
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
EOF只选择订单命名空间和已核对的资源类型,并映射到隔离命名空间:
velero restore create orders-selective-alpha \
--from-backup "$BACKUP" \
--include-namespaces "$SOURCE_NS" \
--include-resources deployments.apps,statefulsets.apps,services,serviceaccounts,configmaps,persistentvolumeclaims \
--namespace-mappings "${SOURCE_NS}:${STAGE_NS}" \
--existing-resource-policy none \
--wait
velero restore describe orders-selective-alpha --details
velero restore logs orders-selective-alpha > /tmp/orders-selective-alpha.log
kubectl get all,pvc -n "$STAGE_NS"
kubectl get events -n "$STAGE_NS" --sort-by=.metadata.creationTimestampVelero 默认的现有资源策略是非破坏性的 none:同名对象存在时跳过,而不是覆盖。ServiceAccount 是特殊情况,Velero 会尝试合并部分字段。--existing-resource-policy=update 是 best effort;PVC 的卷数据不会因为对象 spec 被更新而覆盖,更新失败也可能只记 warning 后继续。因此 Restore 显示完成,只能证明控制器处理完队列。
隔离恢复的通过证据应同时出现:Restore 无未解释 warning/error;PVC 绑定到新 PV 或预期恢复卷;恢复后的文件校验和、数据库水位或业务记录数符合恢复点;应用只连接隔离依赖;没有 Ingress、Gateway、生产队列消费者和计划任务被意外启用。
kubectl get pvc -n "$STAGE_NS" -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,VOLUME:.spec.volumeName,SC:.spec.storageClassName'
kubectl rollout status deploy/orders-api -n "$STAGE_NS" --timeout=180s
kubectl exec -n "$STAGE_NS" deploy/orders-api -- /opt/verify/recovery-check.sh
kubectl get ingress,httproute,cronjob -n "$STAGE_NS"业务校验脚本应返回恢复点 ID、数据库提交水位、关键记录校验和和只读查询结果,不打印用户数据或凭证。测试完成前不要把 replicas、定时任务和入口同时放开。
用资源转换解决平台差异
跨集群恢复常需要改 StorageClass、镜像仓库、ServiceAccount、节点亲和性和入口域名。Velero 的 resource modifier 在资源写入 API Server 前应用 JSON Patch,适合可审计、可重复的转换。下面的规则把 Deployment 缩到零副本,并标记恢复点;精确 schema 应按 v1.18 的资源修补文档核对:
version: v1
resourceModifierRules:
- conditions:
groupResource: deployments.apps
namespaces: [orders]
patches:
- operation: replace
path: /spec/replicas
value: 0
- operation: add
path: /metadata/labels/recovery.example.io~1isolated
value: "true"kubectl -n velero create configmap orders-restore-modifiers \
--from-file=resource-modifiers.yaml
velero restore create orders-transformed-alpha \
--from-backup "$BACKUP" \
--include-namespaces "$SOURCE_NS" \
--namespace-mappings "${SOURCE_NS}:${STAGE_NS}" \
--resource-modifier-configmap orders-restore-modifiersJSON Pointer 中标签键的 / 必须转义为 ~1。replace 要求路径已经存在;可选字段不存在时应使用带条件的 add 或先在副本资源上验证。modifier ConfigMap 本身是恢复制品,需要代码评审、digest 和最小写权限,不能让事故现场临时修改后直接作用于生产。
StorageClass 映射还会改变卷类型、拓扑、加密 key、性能和成本。完成映射后必须读取新 PV 的 CSI driver、volumeHandle、节点拓扑与后端加密配置,不能只看 PVC Bound。
反向实验一:默认跳过掩盖了旧对象
在隔离命名空间先创建一个同名 Deployment,使用故意不同的镜像,再执行 existing-resource-policy=none:
kubectl -n "$STAGE_NS" create deployment orders-api \
--image=registry.example.invalid/orders:conflict
velero restore create orders-conflict-skip \
--from-backup "$BACKUP" \
--include-namespaces "$SOURCE_NS" \
--include-resources deployments.apps \
--namespace-mappings "${SOURCE_NS}:${STAGE_NS}" \
--existing-resource-policy none --wait
kubectl -n "$STAGE_NS" get deploy orders-api \
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
velero restore describe orders-conflict-skip --details预期镜像仍是 :conflict,Restore 详情出现跳过或相关信息。这个实验稳定证明“任务完成”不等于“备份版本已经生效”。安全动作是回到冲突清单决定保留、改名或删除重建,而不是立刻切换到 update。
反向实验二:强行更新撞上不可变字段
为目标 Service 预先分配不同网络字段,再使用 update。Velero 会尽力 patch,但不可变字段、admission 或字段类型可能使 patch 失败;控制器会记录 warning/error 并继续其他资源:
velero restore create orders-conflict-update \
--from-backup "$BACKUP" \
--include-namespaces "$SOURCE_NS" \
--include-resources services,deployments.apps,persistentvolumeclaims \
--namespace-mappings "${SOURCE_NS}:${STAGE_NS}" \
--existing-resource-policy update --wait
velero restore describe orders-conflict-update --details
velero restore logs orders-conflict-update | grep -Ei 'warning|error|immutable|already exists|forbidden'
kubectl get svc,pvc -n "$STAGE_NS" -o yaml看到 warning 后不要删除整个目标命名空间。先按对象分类:Service 保留目标 clusterIP 并恢复 selector/port;PVC 恢复为新名字并验证数据,再让 StatefulSet 指向新 claim;Deployment selector 冲突则以新名字创建并通过 Service 切流。这样每个不可逆动作都有独立回滚点。
从隔离副本合并回生产
受控合并前暂停 GitOps 自动同步、Operator 的相关 reconcile、HPA 和业务写入者,并记录恢复后如何重新启用。不要长期删除控制器;暂停应有 owner、超时和可观察状态。
先对候选 YAML 去掉运行时字段并执行 server-side dry-run:
kubectl get deploy orders-api -n "$STAGE_NS" -o yaml \
| yq 'del(.metadata.uid,.metadata.resourceVersion,.metadata.creationTimestamp,.metadata.managedFields,.status)' \
> /secure-incident/orders-api-candidate.yaml
kubectl apply --server-side --dry-run=server \
--field-manager=dr-restore \
-f /secure-incident/orders-api-candidate.yaml
kubectl diff --server-side --field-manager=dr-restore \
-f /secure-incident/orders-api-candidate.yaml--force-conflicts 会让恢复经理夺取字段所有权,只能在已确认原字段经理暂停、差异经过审批且有回滚制品时使用。Kubernetes apply 参考明确区分 server dry-run、field manager 与强制冲突;这些参数不是“恢复成功开关”。
数据合并优先使用应用原生协议:数据库按表或时间点恢复、对象存储按前缀复制、消息系统按业务补偿。直接把历史 PVC 替换到正在写入的 Pod,既没有字段级冲突检测,也没有事务级回滚。完成数据导入后先运行只读不变量,再开放一小部分写入,最后逐级恢复入口和自动控制器。
清理和回滚不能依赖删除 Restore
删除 Velero Restore 对象不会自动删除它创建的全部 Kubernetes 对象,更不会回收已经创建的云盘、快照副本和外部凭证。清理应使用恢复清单和标签逐项核对:
kubectl get all,pvc,configmap,serviceaccount -n "$STAGE_NS" \
-l velero.io/restore-name=orders-selective-alpha
kubectl scale deploy/orders-api -n "$STAGE_NS" --replicas=0 --timeout=90s
kubectl delete namespace "$STAGE_NS" --wait=false
kubectl get namespace "$STAGE_NS" -o jsonpath='{.spec.finalizers}'
kubectl get pv,volumesnapshotcontentNamespace 卡在 Terminating 时先查 APIService、webhook、finalizer owner 和外部资产,不能直接清空 finalizer。若恢复 PVC 使用 Retain 回收策略或快照 Content 使用 Retain,Namespace 消失后后端资产仍可能计费。清理凭证的顺序是先停止任务、确认无运行引用、撤销临时角色与 token、再删除隔离仓库前缀;不要先删 KMS 权限,让剩余副本变成不可验证密文。
若合并后的生产变更需要回滚,应回到合并前导出的目标基线、Git/IaC 期望状态和数据库原生恢复点。回滚不是再次运行同一个 Restore;同名对象、外部卷和字段所有权已经发生变化,必须重新生成冲突基线。
把偶发救火变成团队能力
生产团队应为每类应用维护机器可读的保护集合:必需 GVK、标签规则、外部依赖、Secret 重建者、StorageClass 映射、隔离网络、数据校验命令、入口开关和清理 owner。每次演练记录跳过数、warning/error 分类、孤儿 PV/快照数量、字段冲突和业务水位差,而不是只保存 Restore 的绿色状态。
容量预算至少包含隔离集群配额、恢复卷的完整逻辑容量、快照克隆限制、对象仓库读请求、跨区域传输、临时节点和验证时间。选择性恢复节省的是受影响对象数量,不一定节省底层卷恢复容量。
长期治理的判断标准很直接:同一个恢复请求由不同值班人员执行,生成的对象集合和冲突决策应一致;默认策略不能改写目标对象;所有覆盖动作都能追溯审批、字段 owner 和回滚制品;隔离副本在接入流量前必须通过数据水位和业务不变量;演练结束后孤儿卷、快照、临时权限和仓库前缀回到基线。只有这些证据同时成立,选择性恢复才是可重复工程能力。
Runbook 的版本与行为依据
恢复工具和 Kubernetes 对象语义都会随版本演进。团队应把下列页面固定到 Runbook 的兼容矩阵,并在升级后重新执行冲突与所有权实验:
Velero v1.18 Restore 参考。Velero v1.18 Restore API。Velero v1.18 故障排查
