Kubernetes 恢复顺序与依赖图
一次跨集群恢复中,所有 YAML 在十分钟内创建完毕,业务却比逐项手工恢复更晚可用。数据库自定义资源先于 CRD 到达,显示 no matches for kind;Operator 随后启动,却因 workload identity 未建立而读不到 KMS;PVC 因 CSI driver 未就绪一直 Pending;Ingress 最早恢复,外部流量进入尚未校验的数据副本。自动化没有停下来等待依赖,只是更快地制造了一批互相遮蔽的错误。
恢复顺序不是文件名排序,也不是一次 kubectl apply -R。每一阶段都要产生下一阶段可消费的证据,并允许操作者在证据不足时暂停、修复、重跑而不放大副作用。
先区分两条不能混跑的恢复路径
自管集群的 etcd 整体恢复,会把 API 存储恢复到一个历史 revision。正确动作是停止所有 API Server,恢复全部 etcd 成员,再启动 API Server,并重启依赖旧状态的 scheduler、controller-manager 和 kubelet。它不是在一个正常运行的新集群里导入若干对象。
Velero、Kasten、托管备份或 GitOps 采用对象级恢复:目标集群已经有健康的 API Server,再创建 CRD、Namespace、RBAC、工作负载和卷。不要先恢复 etcd,又把同一恢复点的对象归档覆盖一遍;两条路径会在 UID、resourceVersion、ownerReference、云资产和卷绑定上产生不可预测冲突。以下命令用于对象级恢复到新集群。
八层依赖机制图
箭头表达的是消费关系:PVC 消费 CSI 与身份,应用消费 CRD/Operator、配置和数据,入口消费已通过业务校验的应用。某个对象 YAML 已经存在,不等于依赖已经可用。例如 CRD Established=True 只证明 API 接受该 kind,不证明 conversion/admission webhook 可访问;PVC Bound 只证明卷绑定,不证明文件或数据库一致。
恢复前先冻结会与恢复竞争的写入者:暂停 GitOps 自动同步与 prune、HPA/KEDA、备份 Schedule、数据库迁移任务和会消费生产队列的控制器;来源仓库改为只读。暂停动作要有 owner、恢复条件和最大持续时间,不能靠“大家记得别打开”。
建立可暂停的批次合同
把恢复资产放进受控目录或制品库,每个 manifest 标注阶段,而不是依赖文件名碰巧有序:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: orders
namespace: recovery-lab
labels:
recovery.example.io/stage: "7"
recovery.example.io/point: "rp-alpha"
recovery.example.io/owner: "orders-team"
spec:
serviceName: orders
replicas: 0
selector:
matchLabels:
app: orders
template:
metadata:
labels:
app: orders
spec:
serviceAccountName: orders
containers:
- name: app
image: registry.example.invalid/orders@sha256:<approved-digest>
volumeMounts:
- name: data
mountPath: /var/lib/orders
volumes:
- name: data
persistentVolumeClaim:
claimName: orders-data示例使用 .invalid 域名和占位 digest,不能直接部署。真实恢复包还应保存源 GVK、目标 GVK、命名空间映射、StorageClass 映射、恢复点 ID、对象 digest、镜像 digest、KMS key 引用、外部依赖引用和业务校验命令。Secret 只保存受控密钥系统中的引用,不保存明文。
一个简单的阶段执行器可以用 server-side dry-run 预检,再应用当前批次并暂停:
#!/usr/bin/env bash
set -euo pipefail
stage="${1:?stage 1..8 required}"
bundle="${2:?manifest directory required}"
mapfile -t files < <(grep -rl "recovery.example.io/stage: [\"']\?${stage}[\"']\?" "$bundle" | sort)
if ((${#files[@]} == 0)); then
echo "no manifests for stage ${stage}" >&2
exit 2
fi
apply_args=()
for file in "${files[@]}"; do
apply_args+=(-f "$file")
done
kubectl apply --server-side --dry-run=server "${apply_args[@]}"
read -r -p "stage ${stage} dry-run passed; type APPLY to continue: " answer
[[ "$answer" == APPLY ]] || { echo "paused"; exit 3; }
kubectl apply --server-side --field-manager=dr-restore "${apply_args[@]}"
echo "stage ${stage} applied; run its evidence gate before the next stage"dry-run=server 能发现未知 GVK、schema 与部分 admission 错误,但不会证明 controller、云 API、KMS、卷数据或业务可用,所以每批之后仍要执行专属门禁。生产自动化可把人工输入改为审批系统,但“暂停”语义必须保留。
第一阶段:重建基础设施而不是等待备份工具创建
基础设施包括目标账号/项目、区域、配额、VPC/VNet、子网、路由、DNS 私有区、Firewall/Security Group、Kubernetes 集群、节点池、OIDC issuer、KMS、镜像仓库访问和对象仓库网络。托管 Kubernetes 备份通常不会重建全部这些对象。
最低门禁可以这样采集:
kubectl version
kubectl get --raw='/readyz?verbose'
kubectl get nodes -o wide
kubectl auth can-i get namespaces --as="${RESTORE_PRINCIPAL:-system:anonymous}"预期证据是 API readyz 各检查通过,目标 Kubernetes minor 位于备份产品和插件支持矩阵内,节点覆盖计划恢复卷的 topology,恢复主体只能执行批准动作。节点 Ready 但区域、CPU/内存、Pod/卷配额不足时仍应暂停;后面的批次只会把容量问题伪装成调度和挂载故障。
若目标集群由 IaC 重建,先保存 plan 与 apply 输出,再冻结 IaC 自动变更。恢复期间让 IaC、GitOps 和恢复工具同时写同一对象,会形成字段所有权争用。
第二阶段:让 CNI、CSI、快照和镜像路径真正工作
CNI 先为系统 Pod、DNS、webhook 和 Operator 提供网络;CSI 负责 StorageClass、动态卷、attach/mount,快照路径还需要 snapshot CRD、snapshot-controller、CSI snapshotter 与 driver 支持。只看到 DaemonSet Ready 不足以放行。
kubectl -n kube-system get pods -o wide
kubectl get storageclass,volumesnapshotclass
kubectl get csidrivers
kubectl create namespace recovery-smoke
kubectl -n recovery-smoke run dns-check --image=busybox:1.36 --restart=Never \
--command -- nslookup kubernetes.default.svc
kubectl -n recovery-smoke wait --for=condition=Ready pod/dns-check --timeout=90s
kubectl -n recovery-smoke logs dns-check再创建一块最小测试 PVC 并挂载写入,验证目标 StorageClass、拓扑和 KMS 权限。若使用 VolumeSnapshot,额外创建一次测试快照并从它生成新 PVC。通过条件是 DNS 可解析、跨节点策略符合预期、PVC Bound、Pod 能写读、后端卷出现在正确账号/区域且使用批准的加密 key。
网络插件尚未收敛时,webhook 可能表现为 context deadline exceeded;CSI 身份错误常表现为 ProvisioningFailed、Unauthorized 或 KMS 拒绝。先修本层,不要通过关闭 webhook、改用未加密 StorageClass 或授予全局云管理员绕过。
第三阶段:先建立 CRD 与 API 契约
恢复 CRD、APIService 和必要的 conversion/admission 配置,再恢复自定义资源。跨 Kubernetes 版本时检查源 API 是否已移除、目标 CRD 的 served/storage 版本、status.storedVersions 和 conversion webhook。
kubectl apply --server-side -f recovery-bundle/stage-3-crds/
kubectl wait --for=condition=Established crd --all --timeout=120s
kubectl get crd -o custom-columns='NAME:.metadata.name,STORED:.status.storedVersions,SERVED:.spec.versions[?(@.served==true)].name'
kubectl api-resources --api-group='<application-api-group>'Established=True 后,再用一个不产生副作用的最小 CR 执行 --dry-run=server。若 conversion webhook 由下一阶段 Operator 提供,先只建立能被当前 API Server 正确存储的 CRD,等 webhook 服务就绪后再执行版本转换和 CR 恢复。把 webhook 的 failurePolicy 临时改成 Ignore 会扩大无校验对象的写入面,必须作为有审批、有时限、有回滚的事故动作,而非常规步骤。
第四阶段:让 Operator、webhook 和调谐循环就绪
Operator 不只是一个 Deployment。它通常需要 ServiceAccount、ClusterRole、webhook Service/证书、leader election、镜像、云 API、CRD 和自身状态存储。先恢复平台级 Operator,再恢复应用 CR。
kubectl -n operators get deploy,pod,svc,endpoints,secret
kubectl -n operators rollout status deploy/<operator-name> --timeout=180s
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations
kubectl auth can-i --as=system:serviceaccount:operators:<operator-sa> \
patch '<custom-resource-plural>.<application-api-group>'创建最小测试 CR 后观察 metadata.generation 与 status.observedGeneration。只有后者追上前者、Condition 进入预期状态、webhook endpoint 有后端且日志没有持续重试,调谐器才算可消费后续对象。
kubectl get <custom-resource> -n recovery-smoke <smoke-name> \
-o jsonpath='{.metadata.generation}{" "}{.status.observedGeneration}{"\n"}'
kubectl -n operators logs deploy/<operator-name> --since=10mOperator 容量也进入 RTO:恢复数千 CR 时,work queue、API QPS/burst、leader 数量、云 API 限额和 webhook 并发会形成长尾。演练要记录队列深度、reconcile 失败率与最老等待年龄,而不只看 Pod CPU。
第五阶段:恢复身份、配置与外部依赖
先恢复 Namespace、ResourceQuota、LimitRange,再恢复 RBAC、ServiceAccount、ConfigMap、Secret 引用和 workload identity 绑定。Kubernetes Secret 返回了,不表示外部 KMS、Vault、云数据库、对象存储和消息队列仍接受它。
身份门禁至少验证四件事:
恢复主体能读取备份但不能删除源恢复点。Operator 和应用 ServiceAccount 只能访问各自资源。KMS 解密、证书链、OIDC issuer/audience 与目标集群一致。
校验环境的外部 endpoint 指向沙箱或只读副本,不能连接生产写入口。
kubectl auth can-i --as=system:serviceaccount:recovery-lab:orders get secret/orders-runtime -n recovery-lab
kubectl auth can-i --as=system:serviceaccount:recovery-lab:orders delete backups -A
kubectl -n recovery-lab describe serviceaccount orders
kubectl -n recovery-lab get events --sort-by=.lastTimestamp第一条按应用需要应为 yes 或改成由 External Secrets 注入,第二条必须为 no。临时恢复权限使用独立 RoleBinding 和短期云会话;本阶段结束就撤销高权限恢复主体,不让它变成日常运行身份。
第六阶段:恢复 PVC 与应用数据
现在才创建或恢复 PVC。先核对目标 StorageClass 的 provisioner、parameters、volumeBindingMode、allowedTopologies、accessModes、volumeMode、文件系统、KMS 和容量;名称相同不代表语义相同。
kubectl -n recovery-lab get pvc -o wide
kubectl -n recovery-lab describe pvc orders-data
kubectl get pv "$(kubectl -n recovery-lab get pvc orders-data -o jsonpath='{.spec.volumeName}')" -o yaml原生 snapshot handle 可能只在源账号或区域有效;文件/data mover 仓库更便于跨后端,但受网络、缓存和目标写入吞吐限制。恢复工具状态完成后,使用只读校验 Pod 检查文件属主、容量、checksum,再由数据库工具执行 checkpoint/WAL 回放、schema 和逻辑查询。
kubectl -n recovery-lab run volume-check --image=busybox:1.36 --restart=Never \
--overrides='{"spec":{"containers":[{"name":"volume-check","image":"busybox:1.36","command":["sh","-c","id; df -h /data; sha256sum /data/probe.txt"],"volumeMounts":[{"name":"data","mountPath":"/data","readOnly":true}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"orders-data"}}]}}'
kubectl -n recovery-lab wait --for=condition=Ready pod/volume-check --timeout=120s
kubectl -n recovery-lab logs volume-check数据库校验通过前保持业务副本数为零,避免 readiness probe 触发写流量。多卷应用还要验证各卷属于同一恢复点;两个 PVC 都 Bound 不能证明事务一致。
第七阶段:先有状态、后无状态,最后开放副作用
应用恢复从数据库、队列状态持有者和有状态控制器开始,再启动无状态 API、worker 和定时任务。CronJob、consumer、mailer、支付回调等副作用组件默认保持 suspend 或副本数为零。
kubectl -n recovery-lab scale statefulset/orders-db --replicas=1
kubectl -n recovery-lab rollout status statefulset/orders-db --timeout=300s
kubectl -n recovery-lab scale deployment/orders-api --replicas=1
kubectl -n recovery-lab rollout status deployment/orders-api --timeout=180s
kubectl -n recovery-lab get pods -o wideReady 只是探针结论。放行前运行版本、身份、读写和业务不变量检查:镜像 digest 与恢复点匹配;数据库 schema 与应用版本兼容;只读查询返回预期水位;隔离写入可读回;错误身份被拒绝;队列 consumer 未连接生产;积压不会在扩容后压垮下游。
应用通过后逐步增加副本,同时观察 API 延迟、数据库连接、队列 lag、错误率和 Operator reconcile。恢复容量按“数据写回 + controller 重算 + 镜像拉取 + 冷缓存 + 积压处理”预算,平时稳态容量不能直接代表恢复峰值。
第八阶段:入口最后恢复,并保留回切开关
Service 可以较早用于集群内校验,面向用户的 Gateway/Ingress、LoadBalancer、DNS、全局流量和外部 webhook 必须最后放行。先使用内部地址或受控测试 Host 验证 TLS、身份、路由和写入,再按权重切流。
kubectl -n recovery-lab get svc,endpoints,endpointslices
kubectl -n recovery-lab get ingress,gateway,httproute 2>/dev/null || true
curl --fail --resolve '<test-host>:443:<test-address>' \
--cacert '<approved-ca-file>' https://<test-host>/ready真实域名和地址从受控配置取得,不写进恢复包示例。切流前记录旧、新入口权重和 TTL;切流后观测业务 SLI、错误率、身份失败、数据库写入与队列重复消费。若指标越界,回切入口不能撤销新集群已经产生的数据,因此还需要单写者协议和数据回并计划。
正向实验:逐批放行并在 PVC 前暂停
在隔离集群准备一个包含 CRD、测试 Operator、ServiceAccount、PVC、应用和测试 Ingress 的恢复包。执行阶段 1 到 5,每阶段保存 kubectl get、Condition、Event 和业务探针输出。在阶段 6 前主动暂停:
./restore-stage.sh 5 recovery-bundle
kubectl -n recovery-lab get deploy,pod,sa,rolebinding
kubectl -n recovery-lab get pvc预期是 Operator 与身份检查通过,但业务 StatefulSet 仍为零副本,PVC 尚不存在或没有挂载,入口没有外部地址。暂停十分钟后再次执行阶段 6;一个设计正确的恢复包不会因为暂停而启动消费、创建重复云资产或修改生产 DNS。
继续恢复数据和应用:
./restore-stage.sh 6 recovery-bundle
kubectl -n recovery-lab wait --for=jsonpath='{.status.phase}'=Bound pvc/orders-data --timeout=300s
kubectl -n recovery-lab logs pod/volume-check
./restore-stage.sh 7 recovery-bundle
kubectl -n recovery-lab get pods完成业务读写、错误身份拒绝与数据水位校验后才执行阶段 8。正向实验通过的不变量是:暂停期间没有自动越级;每阶段可重复执行;observedGeneration 收敛;PVC 数据校验一致;入口开放前业务 SLI 已通过;所有阶段耗时能汇总为完整 RTO。
反向实验:提前恢复应用和入口
反向实验在同一隔离环境先应用阶段 7 和 8,不应用 CRD/Operator、身份与 PVC:
kubectl apply --server-side --dry-run=server -f recovery-bundle/stage-7-apps/ || true
kubectl apply --server-side -f recovery-bundle/stage-8-entry/
kubectl -n recovery-lab get pods,pvc,svc,endpoints
kubectl -n recovery-lab get events --sort-by=.lastTimestamp应该稳定观察到至少一类因果证据:未知 CR 报 no matches for kind;admission webhook 无 endpoint;Pod 因 ServiceAccount/Secret 不存在被拒绝;PVC 未绑定导致 FailedScheduling;镜像或 KMS 身份错误导致 ImagePullBackOff/挂载失败;Service 存在但 EndpointSlice 为空;测试请求返回 503。记录第一条错误及其上游依赖,而不是把所有事件都归为“恢复慢”。
随后按阶段 1 到 7 补齐依赖,但保持入口隔离。若错误按依赖图依次消失,说明门禁能解释故障;若某个对象仍失败,就在其层内检查版本、字段所有权、finalizer、webhook、拓扑或身份。反例结束后删除测试入口,避免它在应用恢复时意外开始接流量。
故障现象应回到上游层
| 现象 | 第一检查层 | 关键证据 | 不应采用的绕过 |
|---|---|---|---|
no matches for kind | CRD/API | api-resources、CRD served/storage、conversion | 删除 CR 或改成错误旧 apiVersion |
| webhook timeout | CNI、CRD、Operator | Service endpoint、证书、网络策略、日志 | 长期 failurePolicy: Ignore |
PVC Pending | CSI、身份、存储映射 | Event、CSIDriver、SC、拓扑、KMS | 换成未批准的默认 StorageClass |
Pod CreateContainerConfigError | 身份与配置 | Secret/ConfigMap/SA 引用、RBAC | 把生产 Secret 手工复制到测试集群 |
| Pod Ready 但数据库报错 | PVC 数据与一致性 | checkpoint、schema、checksum、WAL | 重启直到偶然通过 |
| Service 有地址但请求 503 | 应用与入口 | EndpointSlice、readiness、route、TLS | 直接把健康检查改成常绿 |
| GitOps 恢复后反复回滚字段 | 自动化冻结/所有权 | managedFields、sync event、desired state | 多个工具持续强制 apply |
清理与回滚顺序同样受依赖约束
演练环境清理从入口向基础设施反向进行:先撤销 DNS/流量和外部回调,再停止应用与 consumer,导出验证证据,删除恢复出的工作负载,处理 PVC/快照/仓库保留,撤销临时身份,最后卸载 Operator、CRD、CNI/CSI 测试资源和集群。
kubectl -n recovery-lab delete ingress,gateway,httproute --all --ignore-not-found
kubectl -n recovery-lab scale deployment,statefulset --all --replicas=0
kubectl -n recovery-lab get all,pvc,configmap,secret -o yaml > recovery-final-inventory.yaml
kubectl delete namespace recovery-lab --wait=true
kubectl delete namespace recovery-smoke --wait=trueNamespace 删除前检查 finalizer;CRD 删除前必须确认所有 CR、外部云资产和恢复点目录已交接。删除 CRD 会让 CR 从 API 消失,却不保证 Operator 创建的 bucket、LoadBalancer、snapshot 或数据库实例被回收。PVC 删除还受 PV reclaim policy 和 snapshot deletionPolicy 影响。对象存储的不可变版本不能提前删除时,保留记录直到生命周期释放。
权限与恢复职责
恢复执行器需要跨层能力,但不应永久持有所有能力。基础设施 owner 管账号、网络、集群与配额;平台 owner 管 CNI/CSI、CRD/Operator 和恢复工具;安全 owner 签发短期恢复身份、KMS 与仓库只读权限;应用 owner 提供一致性和业务校验;业务 owner 决定切流与回切。
阶段审批应与权限绑定:执行器在进入某阶段时获得最小临时权限,门禁完成后撤销。尤其要分离仓库读取与删除、生产 DNS 修改与应用恢复、KMS policy 管理与 Secret 读取。恢复日志不得打印 kubeconfig、云令牌、repository password、receive string 或 Secret 内容。
审计记录至少保存恢复点 ID、审批人、执行身份、应用的 manifest digest、每阶段开始/结束、失败事件、临时授权、流量变化和清理结果。它既用于复盘,也是证明 RTO 没有从“点击恢复”才开始计算的依据。
容量决定顺序能否按时完成
八个阶段不是均匀耗时。集群和节点池创建、镜像冷拉取、data mover、卷动态供应、Operator 大规模 reconcile、数据库回放与队列积压常构成长尾。容量计划至少测量:
API Server 请求速率、限流和 admission 延迟;CNI 地址容量、DNS QPS、NetworkPolicy 收敛;CSI attach/provision 并发、目标卷 IOPS、快照读取和 KMS 限额;
data mover CPU/内存/缓存、仓库读取和网络带宽;Operator work queue 深度、最老对象年龄、reconcile 失败与重试;Registry 拉取、节点临时盘、镜像解压和签名验证;
数据库回放速率、连接池、队列积压和下游限流。
提高并发只能在上游容量与限额允许时缩短 RTO。若第六阶段占据大部分时间,应优化数据布局、预热副本或恢复吞吐;若第七阶段被 reconcile 队列拖慢,应分批恢复 CR、调整受测 QPS/worker 并扩充 API/云配额。把入口提前开放不会缩短恢复,只会把未完成工作暴露给用户。
升级、迁移与退出仍要保持这张图
恢复包必须与目标 Kubernetes、备份工具、CNI、CSI、snapshot CRD、Operator CRD 和应用镜像共同版本化。升级前在隔离集群恢复一个旧点,逐层检查:API 是否仍 served、storedVersion 能否转换、webhook/证书是否兼容、旧 snapshot/repository 能否读取、StorageClass 与 KMS 是否可用、业务查询是否通过。
迁移到新工具时,新旧工具可以并行生成恢复点,但同一对象不能被两个恢复执行器同时写入。用相同八阶段门禁分别恢复旧、新恢复点,比较对象差异、数据水位和总 RTO。只有新链连续通过且旧点按保留策略完成交接,才停止旧调度。
退出时反向走图:先移除入口和外部副作用,再归还应用、PVC、身份与 Operator 的所有权;导出 CR 与恢复点索引后才删除 CRD;确认 CSI/CNI 不再承载工作负载后才卸载;最后撤销集群、仓库、KMS、Registry 和云账号中的临时身份与费用资源。退出证据是依赖和资产都归零,而不是 Helm release 消失。
恢复完成的判定
恢复只有同时满足以下事实才结束:目标基础设施位于批准故障域;CNI/CSI 与快照路径通过真实读写;CRD、webhook 和 Operator 收敛;身份最小化且 KMS/外部依赖可用;每个 PVC 数据与恢复点一致;应用版本、schema、读写和副作用符合合同;入口切换可观察、可回切;临时权限、测试端点和恢复容量已经清理。
Backup Completed、Restore Completed、PVC Bound、Pod Ready、Ingress 获得地址都只是中间状态。八阶段图的价值,是让任何一个中间状态都不能越过下一道证据门禁。
执行恢复时查阅的入口
Kubernetes etcd 备份与恢复。Kubernetes VolumeSnapshot。Velero Restore Reference
