Kubernetes 备份恢复证据模型与工具选型
一家团队每晚都备份 Namespace 里的对象和 PVC,演练时却发现恢复后的 StatefulSet 永远起不来。对象清单里有 StatefulSet、Service 和 PVC,仓库里也有卷快照;缺失的是创建这些对象所需的 CRD、webhook CA、镜像 digest、KMS 授权和数据库 checkpoint。备份任务没有报错,因为没有任何工具被告知“一个可接管应用到底由哪些资产组成”。
另一次事故中,平台恢复了全部 Secret,并把测试副本连回生产消息队列。数据校验尚未结束,副本已经重复发送通知。保护得越完整,越需要明确隔离、身份与副作用责任;否则“全部恢复”本身也会成为故障。
用保护集合代替备份对象清单
把一个应用的保护集合记为 P = K ∪ V ∪ A ∪ X ∪ I。这不是五个产品类别,而是五类必须能被唯一识别、恢复和校验的资产:
| 集合 | 保存什么 | 稳定身份 | 恢复证据 |
|---|---|---|---|
K Kubernetes 对象 | Namespace、CRD/CR、RBAC、Service、Workload、ConfigMap | cluster、GVK、namespace、name、UID、resourceVersion | 对象 diff、ownerReference、finalizer、webhook 与 API 可用性 |
V 持久数据 | PVC、PV、后端卷、snapshot、data mover 副本 | PVC UID、PV、CSI driver、volumeHandle、snapshotHandle | 新 PVC/PV、后端资产、checksum、容量与拓扑 |
A 应用状态 | checkpoint、WAL/LSN、schema、队列 offset、应用版本 | workload UID、镜像 digest、数据库/分片身份、position | replay 日志、完整性检查、关键查询与业务不变量 |
X 外部依赖 | DNS、LB、云数据库、对象存储、队列、SaaS 回调 | provider resource ID、account、region、endpoint | 资产对账、连通性、路由与副作用隔离 |
I 身份与密钥 | ServiceAccount、workload identity、KMS、证书、仓库凭证 | issuer、subject、key ID、Secret UID/版本、证书指纹 | 授权测试、解密、轮换状态与审计事件 |
一个恢复点 R 只有在每个必需资产都满足 captured -> retained -> readable -> restorable -> validated 时才可用。任何一项只有“已创建”而没有“可读取和可恢复”,都不能进入接管候选集。恢复合同可以写成下面的最小结构,真实值存入受控清单系统,而不是散落在聊天记录里:
application: checkout
recoveryPoint: rp-semantic-id
protectionSet:
kubernetes:
inventoryDigest: sha256:...
requiredKinds: [CustomResourceDefinition, Namespace, ServiceAccount, Secret, StatefulSet, Service]
volumes:
- pvcUID: '<uid>'
role: data
driver: '<csi-driver>'
snapshotHandleRef: '<vault-reference>'
application:
imageDigest: sha256:...
consistencyMode: application
checkpointRef: '<checkpoint-reference>'
external:
- role: orders-queue
resourceIdRef: '<inventory-reference>'
isolatedDuringValidation: true
identity:
kmsKeyIdRef: '<kms-reference>'
restorePrincipalRef: '<identity-reference>'
validation:
- id: checksum-data
commandRef: '<runbook-command>'
- id: invariant-order-count
queryRef: '<approved-query>'清单保存引用和指纹,不保存明文 Secret、Token 或私钥。recoveryPoint 也不能只是一串时间:它要能关联对象清单 digest、卷快照 handle、应用 checkpoint、工具版本、仓库位置、加密 Key、保留策略和每次恢复结果。
责任图先于工具安装
备份平台通常横跨 Kubernetes API、CSI driver、对象仓库和云 IAM。任何一条边没有 owner,故障时就会出现“控制器说后端失败,存储说请求从未到达”的责任空洞。
应用 owner 决定哪些卷属于同一事务、怎样冻结与解冻、什么查询证明数据可用;平台 owner 编排对象、卷和恢复环境;存储 owner 保证 driver 能力、后端资产与删除语义;安全 owner 管理仓库身份、KMS 和勒索隔离;业务 owner 依据证据决定接管与回切。工具可以自动化边,但不能替代这些决定。
托管集群要把云厂商加入责任图。厂商维护内部 API Server 和 etcd,不表示租户可以下载或恢复其内部快照。租户可执行的动作应落到云厂商公开的备份产品、CSI 能力和 IAM 接口,例如 AWS Backup for EKS、Backup for GKE或 AKS backup and recovery。支持资源、区域、配额和费用都以目标账号的官方页面为准。
选择算法从灾难动作反推
先写灾难,再选工具。输入至少包含:故障域 F、允许丢失量 RPO、允许恢复时长 RTO、一致性等级 C、迁移跨度 M、保留与不可变要求 T、租户隔离 S、可用人员与预算 B。算法的输出不是一个产品名,而是一组相互补足的能力。
choose(F, RPO, RTO, C, M, T, S, B):
1. enumerate P = K ∪ V ∪ A ∪ X ∪ I
2. remove every copy that shares a failure domain with F
3. if K includes cluster-wide state:
select etcd disaster recovery for self-managed clusters
and/or portable resource archive for workload-level recovery
4. for each V:
if target keeps the same CSI backend and fast local restore is enough:
require CSI snapshot
if snapshot may disappear with account, region, backend or driver:
require data mover, storage replication or application-native copy
5. if C is transaction/application consistent:
require application protocol and checkpoint evidence
group snapshot alone is insufficient
6. if RPO is below scheduled-copy interval:
require log shipping, continuous replication or database-native PITR
7. if M crosses cluster, region, account, distribution or CSI driver:
require portable object export, data format and identity remapping
8. if T requires ransomware resistance:
separate write, delete and retention authorities; add immutable copy
9. reject candidates that cannot restore old points after upgrade or license exit
10. rank remaining combinations by measured restore time, evidence completeness,
operator load, capacity cost and exit cost这套算法会自然产生组合方案。CSI snapshot 恢复快,但可能与源存储共享故障域;文件级 data mover 可跨后端,却增加传输时间和节点负载;数据库原生备份能提供事务语义,却不重建 Kubernetes 对象;etcd snapshot 能恢复 API 状态,却不包含卷与云资源;商业平台可能提供策略、报告和多租户工作流,但还要验证格式、许可和退出读取路径。
初筛时可以用“能力是否闭环”淘汰候选,而不是比较功能数量:
| 决策信号 | 必需能力 | 淘汰条件 |
|---|---|---|
| 源集群和存储账号同时丢失 | 跨账号/区域副本、独立 KMS、可导入数据 | 快照仅存在源账号或同一后端 |
| 多卷数据库要求事务级恢复 | 官方应用协议、checkpoint/position、完整恢复查询 | 只有 crash-consistent group snapshot |
| 跨 CSI driver 或发行版迁移 | 文件/块数据移动、对象转换、StorageClass 映射 | 只能引用原 snapshotHandle |
| 多租户自助恢复 | Namespace 隔离、策略委派、审计、资源配额 | controller 长期 cluster-admin,租户可读他人恢复点 |
| 长期保留与勒索防护 | 不可变保留、删除权分离、独立资产盘点 | 源集群身份可单独删除全部副本 |
| 供应商或工具退出 | 开放格式、导出索引、历史恢复点读取验证 | 许可失效后无法恢复或导出 |
正向实验:从保护集合恢复一个可判定副本
实验使用支持 snapshot.storage.k8s.io/v1 的隔离集群。先创建 evidence-lab Namespace、一块数据 PVC、一个带 ConfigMap 与 Secret 的校验 Job。Secret 仅放实验值,不能换成生产凭证。StorageClass 和 Retain 型 VolumeSnapshotClass 使用环境中已经批准的名称。
export LAB_NS=evidence-lab
export STORAGE_CLASS='<approved-storage-class>'
export SNAPSHOT_CLASS='<approved-retain-snapshot-class>'
kubectl create namespace "$LAB_NS"
kubectl -n "$LAB_NS" create configmap app-contract \
--from-literal=schema='orders-v1' \
--from-literal=expected='order=alpha'
kubectl -n "$LAB_NS" create secret generic app-key \
--from-literal=key='lab-key'按家族入口的快照实验创建 source-data、writer 与 source-data-rp1。在 snapshot Ready 后,先保存保护清单,再删除源应用对象,模拟工作负载级事故:
kubectl get -n "$LAB_NS" configmap/app-contract secret/app-key pvc/source-data \
-o json > protection-set.json
kubectl get -n "$LAB_NS" volumesnapshot/source-data-rp1 -o yaml > recovery-point.yaml
kubectl delete pod -n "$LAB_NS" writer
kubectl delete configmap -n "$LAB_NS" app-contract
kubectl delete secret -n "$LAB_NS" app-key从受控清单恢复 ConfigMap 和实验 Secret,再从 Snapshot 创建 restored-data:
kubectl -n "$LAB_NS" create configmap app-contract \
--from-literal=schema='orders-v1' \
--from-literal=expected='order=alpha'
kubectl -n "$LAB_NS" create secret generic app-key \
--from-literal=key='lab-key'
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
EOF
kubectl wait -n "$LAB_NS" --for=jsonpath='{.status.phase}'=Bound \
pvc/restored-data --timeout=300s校验 Job 必须同时验证对象、数据和隔离标志。用 heredoc 生成实验清单,避免手工排版改变 YAML 层级:
cat > recovery-verifier.yaml <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: recovery-verifier
namespace: evidence-lab
spec:
template:
spec:
restartPolicy: Never
containers:
- name: verify
image: busybox:1.36
command:
- sh
- -ceu
- |
test "$SCHEMA" = "orders-v1"
test "$APP_KEY" = "lab-key"
test "$EGRESS_MODE" = "isolated"
sha256sum -c /data/orders.sha256
grep -qx "$EXPECTED" /data/orders.txt
env:
- name: SCHEMA
valueFrom: {configMapKeyRef: {name: app-contract, key: schema}}
- name: EXPECTED
valueFrom: {configMapKeyRef: {name: app-contract, key: expected}}
- name: APP_KEY
valueFrom: {secretKeyRef: {name: app-key, key: key}}
- name: EGRESS_MODE
value: isolated
volumeMounts:
- {name: data, mountPath: /data, readOnly: true}
volumes:
- name: data
persistentVolumeClaim: {claimName: restored-data}
EOFkubectl apply -f recovery-verifier.yaml
kubectl wait -n "$LAB_NS" --for=condition=Complete job/recovery-verifier --timeout=300s
kubectl logs -n "$LAB_NS" job/recovery-verifier
kubectl get -n "$LAB_NS" pvc/restored-data volumesnapshot/source-data-rp1 -o wide通过证据不是 Job 名字,而是:Snapshot 与 Content 能关联后端 handle;恢复 PVC 使用新 PV;Job 日志显示 checksum 通过;ConfigMap 和 Secret 引用都可解析;副本保持隔离;原始对象清单、恢复后清单和工具版本已归档。若任一必需项失败,恢复点状态应是 rejected 或 partial,不能人工改成成功。
反向实验一:Ready 快照缺少应用状态
在源卷持续写入时创建 rp-crash,故意不生成应用提交标记,再从该快照恢复。把校验 Job 增加下面的命令:
test -f /data/committed.marker
grep -qx 'checkpoint=complete' /data/committed.marker预期 VolumeSnapshot.status.readyToUse=true,恢复 PVC 也能 Bound,但 Job 以非零状态退出并显示 committed.marker 不存在。第一证据依次是 Snapshot YAML、恢复 PVC/PV、Job termination message 与日志。这个实验稳定证明:存储层成功不能替代 A 集合中的 checkpoint 和业务不变量。
对真实数据库,反例应改成受支持的测试实例:持续写入,将 data 与 WAL 分放两个 PVC,只保护 data 卷,恢复后执行数据库自己的完整性检查与关键查询。不要用生产数据制造损坏,也不要把任意 shell hook 当成数据库协议。驱动支持 Volume Group Snapshot 时,可以比较普通单卷、group crash consistency 与应用 quiesce 后 group snapshot 的 replay 时间和查询结果。
反向实验二:完整数据使用了错误身份
恢复数据和 ConfigMap,但故意不恢复实验 Secret,或者创建同名 Secret、写入另一值。校验 Job 会在 Pod 创建或运行阶段失败:Secret 缺失时常见的是 CreateContainerConfigError,同名错误值则进入容器后由 test "$APP_KEY" = "lab-key" 拒绝。
kubectl delete secret -n "$LAB_NS" app-key
kubectl describe pod -n "$LAB_NS" -l job-name=recovery-verifier
kubectl -n "$LAB_NS" create secret generic app-key \
--from-literal=key='wrong-key'
kubectl delete job -n "$LAB_NS" recovery-verifier
kubectl apply -f recovery-verifier.yaml
kubectl logs -n "$LAB_NS" job/recovery-verifier这个反例把两种失败分开:缺失凭证是依赖解析失败,错误凭证是身份语义失败。真实 KMS 场景还要区分 Key 不存在、恢复身份无 Decrypt 权限、Key 被禁用、密文属于另一加密上下文。Secret 名称相同不能证明身份相同,证据应保存 issuer、subject、Key ID、Secret UID/版本或证书指纹。
反向实验三:删除策略制造资产孤儿
用 Retain 型 VolumeSnapshotClass 删除 namespaced Snapshot,预期 VolumeSnapshotContent 与后端 snapshot 仍存在;再使用 Delete 型 Class 重做,预期 controller 经 CSI DeleteSnapshot 删除 Content 和后端资产。随后暂时撤销 sidecar 使用的 snapshot Secret 权限,在隔离环境发起删除,观察 finalizer、Event、sidecar 日志和后端清单。
这组实验要得到三种可区分结果:按 Retain 设计保留、按 Delete 成功回收、因权限或 driver 失败而意外遗留。三者在账单上都可能表现为“快照仍存在”,但责任和处理动作完全不同。恢复 Secret 权限、等待协调完成并核对后端后再清理;不要把批量移除 finalizer 当成修复。
把实验输出变成证据记录
每次恢复生成一份不可变记录,至少包含以下字段:
{
"recoveryPoint": "rp-semantic-id",
"protectionSetDigest": "sha256:...",
"objectInventoryDigest": "sha256:...",
"volumeHandles": ["vault-ref:..."],
"applicationCheckpoint": "checkpoint-ref:...",
"toolchain": {
"kubernetes": "vX.Y.Z",
"snapshotApi": "snapshot.storage.k8s.io/v1",
"driver": "name@version",
"backupController": "name@version"
},
"restoreTarget": "isolated-cluster-ref",
"results": [
{"check": "object-diff", "status": "pass"},
{"check": "volume-checksum", "status": "pass"},
{"check": "business-invariant", "status": "pass"},
{"check": "external-side-effects", "status": "blocked"}
],
"measured": {"rpoSeconds": 0, "rtoSeconds": 0},
"decision": "candidate-for-cutover"
}示例中的 0 是待采集占位值,不是生产目标。RPO 从最后一个被证明存在的业务位置算到故障点,RTO 从恢复动作开始算到业务校验和接管就绪,不能用 Backup duration 或 Pod Ready 时间替代。团队应观察连续多轮趋势:恢复时长是否随数据量可解释地增长、孤儿资产是否回到稳定基线、旧恢复点是否仍能读取、失败重试是否在容量预算内。
容量和成本进入评分模型
候选工具的总成本不只是许可证。评估至少包含源快照占用、跨区域副本、对象仓库存储与请求、数据传输、data mover 节点资源、恢复演练环境、KMS、日志保留、值班工时和退出迁移。商业产品价格与云服务费用应从目标区域的官方价格页或计算器读取,不把某次报价写成固定事实。
评分可以采用门禁加权,而不是把不可接受的能力缺口用低价抵消:
hard_gate = restore_pass
&& failure_domain_separated
&& identity_restorable
&& delete_authority_separated
&& old_points_readable_after_upgrade
score = restore_time_fit
+ evidence_completeness
+ portability
+ operator_load_fit
+ measured_total_cost_fithard_gate 任一项为假就淘汰。通过后再比较恢复吞吐、并发、API 限流、增量效率、仓库增长、跨区成本与团队操作负担。这样可以避免“功能很多、演示很快,但灾难模型不成立”的工具胜出。
升级和退出也要跑同一套算法
升级候选版本时,用旧工具创建的恢复点作为输入,恢复到隔离目标,重新执行对象 diff、卷 checksum、应用查询、身份授权和删除实验。CRD 的 served/storage version、conversion webhook、controller、sidecar 与 CSI driver 必须作为一个兼容单元。只看到 controller Ready 或 CRD Established,不能证明旧恢复点仍可读。
退出工具时先导出保护集合、恢复点索引、对象归档、后端 handle 引用、加密依赖和审计记录,再用替代工具恢复同一业务。双轨期间禁止两个控制器同时管理同一 Snapshot、finalizer 或仓库保留策略。历史恢复点要么由旧读取路径持续支持,要么经过可验证迁移;许可到期、云账号关闭和 KMS 销毁都必须排在最后。
实验清理遵循“引用先解除、后端再核对、身份最后撤销”:删除 verifier Job 和恢复 PVC,确认没有恢复引用;按 Retain/Delete 语义处理 Snapshot 与 Content;核对后端和仓库孤儿;最后删除 Namespace、RoleBinding、workload identity 与实验 Key。清理后重新运行资产盘点,只有 Kubernetes 对象、后端快照、仓库对象和云身份都回到预期基线,实验才真正结束。
