Kubernetes 备份、恢复与灾备工具
凌晨的恢复任务显示 Completed,新集群里的 Deployment 也全部 Ready,登录接口却持续返回 500。排查到最后,团队发现 Kubernetes 对象回来了,数据库 PVC 也从快照创建成功,但加密所需的 KMS 授权没有迁移,消息消费者又在隔离校验完成前接入了生产队列。一个绿色状态同时掩盖了“数据不可解密”和“恢复副本产生外部副作用”两种故障。
更麻烦的是,值班记录只有备份任务名和一张成功截图。没人能回答快照对应哪个 PVC UID、后端 snapshot handle 是否仍存在、Secret 来自哪个版本、恢复使用了哪条 StorageClass、业务校验跑了哪些查询。此时问题已经不是“再执行一次恢复”,而是团队从未建立可以证明恢复点完整、恢复动作正确、业务可以接管的证据链。
先把“集群备份”拆成五类资产
Kubernetes 没有一个快照能够同时冻结所有状态。恢复计划至少要识别五类资产:API 对象与 CRD、Kubernetes 状态库、PVC 背后的持久数据、应用自己的日志或检查点,以及云 IAM、KMS、DNS、负载均衡器、外部数据库和对象存储等集群外依赖。镜像也必须以 digest 或不可变版本重新取得,不能假设某个浮动 Tag 永远指向同一制品。
Git 和 IaC 很擅长重建 Namespace、Deployment、网络策略与基础设施,却通常不包含运行中的 PVC 数据、动态生成的 Secret 和数据库事务位置。etcd snapshot 能保存 Kubernetes API 状态,却不会复制云磁盘内容,也不会让外部 DNS 和 IAM 回到同一时刻。CSI snapshot 能复制卷,却不理解数据库是否已 checkpoint,更不能替业务决定何时接入流量。
因此,“备份成功”只能表示某个组件完成了自己的动作。完整恢复要沿下面的链路逐层收集证据:
第一次操作先确认谁真正提供能力
开始安装工具前,先在目标集群读取真实版本、API 和驱动能力。普通 VolumeSnapshot 使用 snapshot.storage.k8s.io/v1;VolumeSnapshot、VolumeSnapshotContent 与 VolumeSnapshotClass 是 CRD,集群发行方需要提供匹配的 CRD、snapshot-controller 和 validating webhook,CSI 驱动侧还要运行 csi-snapshotter sidecar。只有 API 对象而没有驱动能力,快照会一直等待。
kubectl config current-context
kubectl version
kubectl api-resources | grep -E 'volumesnapshot|volumegroupsnapshot'
kubectl get crd | grep snapshot.storage.k8s.io
kubectl get csidriver
kubectl get volumesnapshotclass -o yaml
kubectl auth can-i create volumesnapshots.snapshot.storage.k8s.io -n backup-lab
kubectl auth can-i get volumesnapshotcontents.snapshot.storage.k8s.io预期能看到稳定 API、实际 CSI driver 名称和至少一条由存储管理员批准的 snapshot class。api-resources 有输出而 VolumeSnapshotClass 为空,表示扩展 API 已安装,但没有可选后端;Class 存在而 driver 不支持 CreateSnapshot,则会在 sidecar 或 CSI RPC 层失败。Kubernetes Volume Snapshots明确给出了 controller、sidecar、CSI driver 与发行方的职责关系,安装时应以集群发行版和驱动的兼容矩阵选择版本,而不是直接套用开发分支清单。
自建 kubeadm 集群还要区分 stacked etcd 与 external etcd。前者的本地数据目录通常是 /var/lib/etcd,后者由独立成员和独立证书管理;两者的停止顺序、成员拓扑和恢复动作不同。托管 Kubernetes 通常不向租户开放内部 etcd,恢复入口应使用云厂商公开的工作负载备份产品或自建数据保护工具,不能去工作节点寻找状态库文件。
建一个不会碰生产数据的快照实验
下面的实验使用已有 CSI snapshot 能力。先把实际的 StorageClass 与 VolumeSnapshotClass 填入环境变量;实验 Namespace、PVC 和后端快照都要使用专用名称。执行者需要在实验 Namespace 创建 Pod、PVC 和 VolumeSnapshot,并能读取绑定的 PV、VolumeSnapshotContent 与 Event。生产中,读取集群级 Content 的审计身份应与应用提交身份分离。
export LAB_NS=backup-lab
export STORAGE_CLASS='<approved-storage-class>'
export SNAPSHOT_CLASS='<approved-retain-snapshot-class>'
kubectl create namespace "$LAB_NS"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: source-data
namespace: ${LAB_NS}
spec:
storageClassName: ${STORAGE_CLASS}
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: writer
namespace: ${LAB_NS}
spec:
restartPolicy: Never
containers:
- name: writer
image: busybox:1.36
command: [sh, -c, "printf 'order=alpha\\n' > /data/orders.txt; sha256sum /data/orders.txt > /data/orders.sha256; sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: source-data
EOF
kubectl wait -n "$LAB_NS" --for=condition=Ready pod/writer --timeout=120s
kubectl exec -n "$LAB_NS" writer -- sh -c 'cat /data/orders.txt; cat /data/orders.sha256'记录源 PVC UID、PV、driver 与 volume handle 后再创建快照。这些字段把 Kubernetes 请求与后端资产绑在一起,仅保存快照名称不够。
kubectl get pvc -n "$LAB_NS" source-data -o jsonpath='{.metadata.uid}{"\n"}{.spec.volumeName}{"\n"}'
kubectl get pv "$(kubectl get pvc -n "$LAB_NS" source-data -o jsonpath='{.spec.volumeName}')" \
-o jsonpath='{.spec.csi.driver}{"\n"}{.spec.csi.volumeHandle}{"\n"}'
cat <<EOF | kubectl apply -f -
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: source-data-rp1
namespace: ${LAB_NS}
spec:
volumeSnapshotClassName: ${SNAPSHOT_CLASS}
source:
persistentVolumeClaimName: source-data
EOF
kubectl wait -n "$LAB_NS" --for=jsonpath='{.status.readyToUse}'=true \
volumesnapshot/source-data-rp1 --timeout=300s
kubectl get volumesnapshot -n "$LAB_NS" source-data-rp1 -o yamlreadyToUse: true 只说明 CSI 可以使用该存储快照。继续从它创建一块新卷并只挂给隔离 Pod,才开始验证“能恢复”。恢复 PVC 的请求容量不能小于快照的 restoreSize,StorageClass、访问模式、拓扑和加密配置也要与目标集群能力相容。
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restored-data
namespace: ${LAB_NS}
spec:
storageClassName: ${STORAGE_CLASS}
dataSource:
name: source-data-rp1
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: verifier
namespace: ${LAB_NS}
spec:
restartPolicy: Never
containers:
- name: verifier
image: busybox:1.36
command: [sh, -c, "sha256sum -c /data/orders.sha256; grep -qx 'order=alpha' /data/orders.txt"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: restored-data
EOF
kubectl wait -n "$LAB_NS" --for=jsonpath='{.status.phase}'=Succeeded pod/verifier --timeout=300s
kubectl logs -n "$LAB_NS" verifier预期日志包含 orders.txt: OK,恢复 PVC 为 Bound,并生成不同于源卷的新 PV 和 volume handle。若 Pod Pending,先看 PVC Event、CSI provisioner 日志、restoreSize、StorageClass、拓扑和配额;若文件校验失败,问题已经越过 Kubernetes 对象层,需检查应用写入、文件系统 flush 与快照时点。
故意制造一次“绿色但不可用”
保持源 Pod 写入时直接创建另一个快照,然后把校验规则改成必须存在第二个业务文件。CSI 仍可能把快照标为 Ready,但恢复校验应失败:
kubectl exec -n "$LAB_NS" writer -- sh -c "printf 'pending=true\\n' > /data/inflight.tmp"
# 按前面的方式创建 source-data-rp2,并从它创建 restored-data-rp2。
# verifier 的命令改为同时检查 orders.txt 与 committed.marker:
test -f /data/committed.marker这个反例模拟数据库事务尚未 checkpoint、WAL 与 data 分卷却只保护一个卷,或应用 hook 部分成功。readyToUse 不会理解 committed.marker 的业务语义,因此应用必须使用自己支持的 quiesce、checkpoint、flush WAL 或原生备份协议。多卷应用在驱动支持时可使用 Volume Group Snapshot;Kubernetes 1.36 将 groupsnapshot.storage.k8s.io/v1 提升为稳定 API,但它提供的是同一时间点的崩溃一致性,不等于数据库事务一致性,且驱动必须实现对应 group snapshot RPC 与 capability。Volume Group Snapshot GA 说明列出了对象、恢复方式和驱动要求。
从请求到后端快照到底经过什么
用户创建 VolumeSnapshot 后,集群级 snapshot-controller 负责请求与 VolumeSnapshotContent 的绑定和生命周期;CSI 驱动旁的 csi-snapshotter 观察 Content,通过 CSI endpoint 调用 CreateSnapshot 或 DeleteSnapshot;驱动再把请求翻译成存储后端资产。恢复不是把旧卷原地倒回,而是 PVC provisioner 根据 dataSource 创建新卷。
VolumeSnapshotContent.spec.deletionPolicy 决定删除 namespaced Snapshot 后是否删除后端资产。Retain 给误删保留恢复窗口,也会留下需要盘点和付费的孤儿;Delete 自动回收,但错误删除请求会穿透到后端。它与 PV.spec.persistentVolumeReclaimPolicy 是两套独立策略,必须分别记录。对象卡在 Terminating 时,先查 finalizer、controller、sidecar、Secret、driver 和后端状态;直接移除 finalizer 可能让唯一 handle 丢失,也可能遗留持续计费资产。
etcd 恢复不是复制一个 db 文件
自建集群需要使用与运行版本匹配的 etcd 工具和 TLS 凭证创建快照,校验 hash,并把加密副本送到独立故障域。etcd 3.6 及后续版本把 snapshot status 与 restore 等离线动作迁到 etcdutl;在线 endpoint health、member 和 snapshot save 仍由 etcdctl 处理。恢复 Kubernetes 状态库时还要停止 API Server、隔离旧成员、创建新的 data directory,并按 etcd disaster recovery 与 Kubernetes etcd 操作指南处理 revision 回退、bump revision 和 mark compacted,避免 informer 继续相信旧 resourceVersion。
恢复后的 Kubernetes 对象可能指向已经不存在的云磁盘、负载均衡器或 KMS Key,所以状态库恢复后必须与云资产清单对账。kubeadm 升级产生的 /etc/kubernetes/tmp/kubeadm-backup-* 是升级回滚材料,不是异地、加密、定期校验的长期备份;升级完成后还要按保留策略清理。
权限和凭证要跟数据分开治理
备份控制器常需要读取多 Namespace 对象、创建 VolumeSnapshot、访问对象仓库,并调用 KMS 或云快照 API。把这些能力都交给一个长期 cluster-admin 和一把永久 Access Key,会让任何控制器漏洞同时获得集群与备份删除权。更稳妥的拆分是:
调度身份只能创建 Backup、Restore 或 Snapshot 请求,不能修改 controller Deployment。controller 的 Kubernetes RBAC 只覆盖已批准资源和 Namespace;集群级 CRD、RBAC 与 webhook 另行审批。仓库写入身份与保留/删除身份分离,优先使用 workload identity 和短期凭证。
KMS Key、对象仓库与源集群放在独立故障域;恢复身份预先验证解密权限,但不长期驻留源集群。Secret 不进入日志、命令历史、海报和普通导出文件;审计记录只保存 Secret 名称、UID、版本或指纹。
删除备份前需要双重条件:保留期或销毁审批已经满足,并且没有恢复演练、法律保留或导入对象引用该恢复点。勒索场景还要让源集群管理员无法单独缩短不可变保留期或删除仓库。
工具怎么组合,而不是怎么排榜
工具选择从灾难动作反推。只需保护单个 CSI 卷时,原生 VolumeSnapshot 最短,但它不归档完整 API 对象,也不保证快照离开存储故障域。需要跨 Namespace 选择对象、调度备份、迁移资源和组合卷保护时,可评估 Velero 一类平台;需要应用感知工作流、租户治理、图形化策略、合规报告或商业支持时,再比较 Kasten K10、K8up、Kanister、VolSync 及云厂商服务的真实能力。
不要用产品数量代替设计。一个常见组合是:Git/IaC 重建期望状态,平台工具归档运行对象,CSI 快照或 data mover 保护卷,数据库原生备份提供事务级恢复,跨账号对象仓库存放副本,演练系统负责隔离恢复和业务校验。每层都要有 owner、恢复顺序和失败后的降级动作。
选择时依次问:灾难会删除哪些故障域;RPO 需要快照、持续复制还是日志回放;目标集群能否访问原 StorageClass、KMS 和镜像;恢复是否跨区域、账号、发行版或 CSI driver;工具能否导出可移植数据;删除与许可到期后还能否读取历史恢复点。云服务的地区、配额、支持对象和价格会变化,应在目标账号查看官方能力页和价格计算器,不写死套餐数字。
容量、时间与成本要在第一次全量恢复前计算
快照并不等于零成本副本。增量快照会随着数据变更率增长;跨区域复制产生存储与传输费用;文件级 data mover 还消耗节点 CPU、内存、临时空间、API QPS 和对象仓库请求。可以先用下面的预算模型估算,再用演练数据替换假设:
daily_changed_bytes = protected_bytes × daily_change_rate
repository_bytes ≈ first_full + daily_changed_bytes × retained_points × compression_factor
backup_window ≈ bytes_to_move / sustained_backup_throughput
restore_window ≈ bytes_to_read / sustained_restore_throughput + provision + replay + validation生产阈值不能照抄演示值。至少持续观察每个恢复点的逻辑大小与实际占用、快照创建和删除延迟、失败与重试、data mover 吞吐、待处理队列年龄、仓库对象数、孤儿 handle、API 限流和演练实测 RPO/RTO。恢复带宽经常低于备份带宽,KMS、对象仓库、CSI provisioner 与 API Server 也可能在灾难时共同成为瓶颈。
升级、迁移和退出都要保留恢复窗口
升级前先冻结 CRD、controller、sidecar、CSI driver、备份格式和 Kubernetes 版本的兼容矩阵,在隔离集群恢复旧恢复点。CRD 的 served/storage version、conversion webhook 与存量对象必须一起检查;只升级 controller 镜像,可能让旧对象仍以已弃用版本存储。Volume Group Snapshot 从 Beta 迁到稳定 API 时,还要核对 driver capability、CRD storedVersions、controller/sidecar 镜像与发行方声明,不能只替换 YAML 的 apiVersion。
迁移工具采用双轨期:旧工具继续生成可恢复副本,新工具独立写入新仓库;同一恢复点分别恢复,对比对象、卷、应用查询、RPO/RTO 和删除语义。退出前导出恢复点索引、对象清单、后端 handle、加密与 KMS 依赖、保留策略和审计记录;确认历史数据有受支持的读取路径,再撤销工作负载身份、仓库权限和云角色。
卸载顺序是先停止新调度,再等待运行任务结束,处理 finalizer 与保留对象,核对后端资产,最后删除 controller、webhook、RBAC 和 CRD。直接删 CRD 会让后端快照失去 Kubernetes 索引。清理实验环境时也遵循同样原则:
kubectl delete pod -n "$LAB_NS" verifier --ignore-not-found
kubectl delete pvc -n "$LAB_NS" restored-data --ignore-not-found
kubectl delete volumesnapshot -n "$LAB_NS" source-data-rp1 --ignore-not-found
kubectl get volumesnapshotcontents
# Retain 策略下先核对并按存储后端流程处理保留资产,再删除 Namespace。
kubectl delete namespace "$LAB_NS"真正可以接管的恢复点,不是一个绿色 Backup,也不是一块 Ready 快照,而是一组能把资产身份、恢复点、后端副本、恢复动作、业务不变量和接管决策互相指向的证据。下一步可以用备份恢复证据模型与工具选型把这组证据变成团队可执行的保护合同。
