CSI VolumeSnapshot 与卷恢复
凌晨恢复演练里,值班同学看到 VolumeSnapshot.status.readyToUse: true,便删除损坏的 PVC,准备用快照回滚。新 PVC 却一直 Pending:目标 StorageClass 换了 CSI driver,快照只存在原存储账号,创建时使用的 KMS 密钥也已撤销。Kubernetes 对象说“可用”,真正的恢复链却在后端创建卷时断了。
另一次事故更隐蔽。团队删除测试 namespace 时顺手删掉了 VolumeSnapshot,以为云盘快照还会按备份保留期存在;对应 VolumeSnapshotClass 的 deletionPolicy 实际是 Delete,snapshot-controller、external-snapshotter 和 CSI driver 按设计把 VolumeSnapshotContent 与后端快照一起清掉。这里没有控制器误删,只有生命周期配置与恢复预期互相矛盾。
先把三种成功拆开
VolumeSnapshot 创建成功,只表示 API Server 接受了一个自定义资源。readyToUse=true 表示 CSI 路径报告后端快照已经可供后续操作使用。真正的恢复成功还要看到新 PVC Bound、新 PV 指向正确 driver 与后端卷、隔离 Pod 能读取预期数据,并且应用自身的校验通过。
稳定 API 使用 snapshot.storage.k8s.io/v1。VolumeSnapshot 是 namespace 内的请求,VolumeSnapshotContent 是集群级的后端快照记录,VolumeSnapshotClass 是集群级的 driver 参数与删除策略。它们是 CRD,不是安装 Kubernetes 后必然可用的核心对象;Kubernetes 官方的 Volume Snapshot 概念页也把 CRD、snapshot-controller 与 CSI driver 支持列为共同依赖。
对象与数据应分成四层观察:
| 层次 | 关键对象 | 它能证明什么 | 它不能证明什么 |
|---|---|---|---|
| API 请求 | VolumeSnapshot | 谁请求了哪个 PVC 的快照、当前状态 | 后端副本是否脱离源故障域 |
| 绑定记录 | VolumeSnapshotContent | driver、snapshot handle、删除策略和绑定关系 | 应用事务是否一致 |
| 存储副本 | 后端 snapshot | 存储系统保存了时间点数据 | 新集群能否找到密钥、拓扑和账号 |
| 恢复结果 | 新 PVC、新 PV、隔离 Pod | 数据能被重新供应、挂载和读取 | 业务依赖是否整体恢复 |
在隔离 namespace 核对安装链
实验需要一个可销毁的 Kubernetes 集群、kubectl、支持快照的 CSI StorageClass,以及创建 namespace、PVC、VolumeSnapshot 的权限。安装 CRD、集群级 controller、class 与 RBAC 通常需要临时 storage admin。生产集群优先使用发行版、云厂商或 CSI driver 厂商给出的兼容组合;手工安装上游组件时固定 release 与镜像摘要,不要直接引用 master 清单。external-snapshotter 的发行与兼容信息应从官方 releases核对。
先盘点,不要先创建对象:
kubectl version
kubectl config current-context
kubectl api-resources | grep -E 'volumesnapshot(class|content)?'
kubectl get crd \
volumesnapshots.snapshot.storage.k8s.io \
volumesnapshotcontents.snapshot.storage.k8s.io \
volumesnapshotclasses.snapshot.storage.k8s.io
kubectl get csidriver,storageclass
kubectl -n kube-system get deploy,pod,lease | grep -E 'snapshot|csi'预期看到三个 snapshot.storage.k8s.io CRD、集群级 snapshot-controller、目标 CSI controller 中的 external-snapshotter sidecar,以及 driver 匹配的 StorageClass。只有 CRD 而没有 controller,VolumeSnapshot 会被 API 接受却长期没有绑定;有 common controller 而没有 driver sidecar 或 driver capability,Event 会停在创建后端 snapshot 的步骤。
还要确认操作身份:
kubectl auth can-i create volumesnapshots.snapshot.storage.k8s.io -n snapshot-lab
kubectl auth can-i get persistentvolumeclaims -n snapshot-lab
kubectl auth can-i create volumesnapshotcontents.snapshot.storage.k8s.io
kubectl auth can-i get secrets -n <snapshot-secret-namespace> \
--as=system:serviceaccount:<csi-namespace>:<snapshotter-service-account>应用团队通常只需在自己的 namespace 管理 VolumeSnapshot;storage admin 管理 class/content 和后端凭证。授予创建快照的权限等于允许复制 PVC 数据,不能仅按“只读存储操作”授权。external-snapshotter 若通过 VolumeSnapshotClass.parameters 获取 Secret,只给专用 Secret 的 get 权限;相关参数名与模板变量应按 CSI 官方的 snapshot credentials 说明和 driver 文档核对。
配置一条可保留的实验链
先让 storage admin 创建实验 class。把占位符替换成集群实际 driver;parameters 必须来自该 driver 文档,不能从另一家 CSI 实现照搬。
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: lab-retain
driver: <csi-driver-name>
deletionPolicy: RetaindeletionPolicy 只有 Retain 与 Delete 两种语义,详见 Kubernetes 官方的 VolumeSnapshotClass 文档。它会进入绑定的 VolumeSnapshotContent.spec.deletionPolicy。这与 PV 的 persistentVolumeReclaimPolicy 是两套开关:前者处理快照,后者处理源卷。
创建一个小 PVC,并写入容易核对的文件:
apiVersion: v1
kind: Namespace
metadata:
name: snapshot-lab
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: source-data
namespace: snapshot-lab
spec:
accessModes: [ReadWriteOnce]
storageClassName: <snapshot-capable-storage-class>
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: writer
namespace: snapshot-lab
spec:
restartPolicy: Never
containers:
- name: writer
image: busybox:1.37
command: ["sh", "-c", "printf 'order-001\norder-002\n' > /data/orders && sha256sum /data/orders > /data/orders.sha256 && sync && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: source-data保存清单为 snapshot-lab.yaml 后执行:
kubectl apply -f snapshot-lab.yaml
kubectl -n snapshot-lab wait --for=condition=Ready pod/writer --timeout=180s
kubectl -n snapshot-lab exec writer -- sha256sum -c /data/orders.sha256预期输出包含 orders: OK。这个 sync 只让文件系统实验更稳定,不代表数据库事务一致;数据库需要自己的 checkpoint、WAL 与停写协议。
现在创建快照:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: source-data-rp1
namespace: snapshot-lab
spec:
volumeSnapshotClassName: lab-retain
source:
persistentVolumeClaimName: source-datakubectl apply -f snapshot-rp1.yaml
kubectl -n snapshot-lab wait \
--for=jsonpath='{.status.readyToUse}'=true \
volumesnapshot/source-data-rp1 --timeout=300s
kubectl -n snapshot-lab get volumesnapshot source-data-rp1 -o yaml
CONTENT=$(kubectl -n snapshot-lab get volumesnapshot source-data-rp1 -o jsonpath='{.status.boundVolumeSnapshotContentName}')
kubectl get volumesnapshotcontent "$CONTENT" -o yaml
kubectl -n snapshot-lab get events --sort-by=.lastTimestamp证据中应同时出现 readyToUse: true、restoreSize、绑定的 content 名称,以及 content 中与 class 相同的 driver、deletionPolicy: Retain 和后端 snapshotHandle。handle 属于敏感基础设施标识,排障材料要脱敏。
恢复不是覆盖旧 PVC,而是供应新卷
从快照创建 PVC 时,dataSource 指向同 namespace 的 VolumeSnapshot。请求容量不得小于 restoreSize,StorageClass、driver、volumeMode、access mode、拓扑、配额和加密密钥都必须在目标环境成立。Kubernetes 的 PersistentVolume 文档给出了 snapshot data source 的标准形式。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restored-data
namespace: snapshot-lab
spec:
storageClassName: <snapshot-capable-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: restore-reader
namespace: snapshot-lab
spec:
restartPolicy: Never
containers:
- name: reader
image: busybox:1.37
command: ["sh", "-c", "sha256sum -c /restore/orders.sha256 && cat /restore/orders && sleep 3600"]
volumeMounts:
- name: restored
mountPath: /restore
volumes:
- name: restored
persistentVolumeClaim:
claimName: restored-datakubectl apply -f restore-rp1.yaml
kubectl -n snapshot-lab wait --for=condition=Ready pod/restore-reader --timeout=300s
kubectl -n snapshot-lab logs restore-reader
kubectl -n snapshot-lab get pvc restored-data -o wide
kubectl get pv "$(kubectl -n snapshot-lab get pvc restored-data -o jsonpath='{.spec.volumeName}')" -o yaml预期日志再次出现 orders: OK 和两条订单,PVC 为 Bound,新 PV 有新的 volumeHandle。若 PVC Pending,先看 PVC Event 与 external-provisioner 日志,再比较 restoreSize、StorageClass driver、volumeMode、拓扑和 quota;不要反复删建快照掩盖供应失败。
删除反例揭示真正的生命周期
先记录 Retain 实验的 content 与后端 snapshot ID,再删除 namespaced 请求:
kubectl -n snapshot-lab delete volumesnapshot source-data-rp1
kubectl get volumesnapshotcontent "$CONTENT" -o yaml预期 VolumeSnapshot 消失,content 与后端 snapshot 保留。它们不会自动重新出现在 namespace;管理员需要用预配置 content 的导入流程重新绑定,或按资产台账清理。Retain 延长误删后的恢复窗口,也把孤儿盘点和持续计费责任交给平台团队。
再新建 deletionPolicy: Delete 的 class 与快照,等待 Ready 后删除该快照:
DELETE_CONTENT=$(kubectl -n snapshot-lab get volumesnapshot source-data-delete -o jsonpath='{.status.boundVolumeSnapshotContentName}')
kubectl -n snapshot-lab delete volumesnapshot source-data-delete
kubectl get volumesnapshotcontent "$DELETE_CONTENT"预期 content 最终返回 NotFound,CSI driver 后端也不再列出对应 snapshot。若对象长期 Terminating,检查 finalizer、Event、snapshot-controller、external-snapshotter、CSI driver 和删除凭证。删除 Secret 后再删快照是一个很有价值的隔离反例:Kubernetes 记录可能继续变化,但需要该 Secret 的 DeleteSnapshot 可能失败并留下后端孤儿。修复凭证和 driver 协调后再观察收敛,不能把批量移除 finalizer 当作修复。
controller、sidecar 与 driver 怎样接力
snapshot-controller 负责 VolumeSnapshot 与 VolumeSnapshotContent 的绑定、对象生命周期、状态传播和保护 finalizer;它不直接调用存储设备。每个 CSI driver 旁的 external-snapshotter 只处理 driver 匹配的 content,把请求转换成 CSI CreateSnapshot、DeleteSnapshot、ListSnapshots。driver 再把 CSI RPC 映射到存储阵列或云盘 API。
恢复走另一条 RPC:external-provisioner 解析 PVC 的 snapshot data source,让 driver 以 snapshot 作为 volume_content_source 创建新卷。于是 readyToUse 与恢复 PVC Bound 之间还隔着容量、拓扑、配额、密钥、凭证和后端供应能力。这个中间状态正是大多数“快照明明成功却恢复不了”的根因所在。
快照和仓库数据不是同一种资产
CSI snapshot 通常留在同一个存储系统、账号或区域,创建快、恢复快,也可能依赖源存储的元数据、KMS 和配额。备份仓库里的文件块或对象副本经过读取、分块、校验、加密与上传,可以进入独立账号和故障域,代价是更长的数据移动窗口、扫描开销、网络费用、仓库维护和更慢的全量恢复。
因此选型不能写成“支持快照就不需要备份”。低 RTO 可以保留近端 CSI snapshot;需要跨账号、跨存储、长期保留或抵御源存储误删时,还要把数据移动到独立仓库。相反,只有仓库副本而没有近端快照,海量 PVC 的恢复可能被下载吞吐和重新供应速度拖慢。两者组合时必须记录同一个恢复点身份,不能让 API 对象、后端 snapshot 和仓库副本各自使用无法关联的名字。
容量、费用与故障域一起算
快照成本不只看“增量占用”。后端可能以 copy-on-write 保存变化块,源卷持续写入会让保留快照逐渐变大;恢复会临时同时占用 snapshot、恢复卷和源卷。并发恢复峰值可按“恢复卷总申请量 + 快照变化块 + 临时克隆 + driver 缓存”估算,再叠加目标 StorageClass 配额、区域容量和 API 限流。
至少观测快照创建/删除延迟、Pending 年龄、Ready 数、失败数、后端字节、孤儿 handle、恢复 PVC 供应时间、CSI RPC 错误与限流。阈值来自业务 RPO/RTO、基线吞吐和存储配额,不用脱离负载的固定分钟数冒充通用标准。费用则按快照保留字节、跨区域复制、API 调用、KMS、恢复卷并存时间和出口流量分别核算,价格从目标存储服务页面动态确认。
升级、迁移与退出要保住 handle
升级前导出 CRD 的 served、storage 与 storedVersions,保存 controller/sidecar/driver 镜像摘要、class、content、snapshot handle 和一组可恢复 fixture。先在隔离集群用目标组合创建、删除、导入并恢复,再升级一小部分非关键 class。只改 YAML 的 apiVersion 不能完成 CRD 存储版本迁移;直接删除 CRD 会删除 Kubernetes 对象索引,后端 snapshot 可能变成无法归属的资产。
迁移 CSI driver 或存储平台时,旧 snapshot handle 通常不能由新 driver 直接解释。先把关键恢复点恢复成卷,再用目标平台重新快照或移动到独立仓库;双轨期对同一 fixture 比较校验值、恢复时间、加密身份和删除结果。回滚窗口内保留旧 controller 与读取凭证,但禁止两套组件同时写同一组 content。
实验清理按证据反向执行:停止 writer 与 restore-reader,确认恢复数据不再需要,删除恢复 PVC 并核对新后端卷,再按 Delete 或人工处置 Retain 快照,最后删除源 PVC、namespace、临时 RoleBinding、Secret 和实验 class。退出完成不是 kubectl delete 返回成功,而是 Kubernetes 中没有残留 finalizer,后端没有未登记 snapshot/volume,旧 ServiceAccount 与存储凭证已撤销,监控与账单也不再出现该链路的资产。
