文件系统备份与数据移动器
恢复窗口已经过半,备份平台仍显示 Completed。新 Pod 启动后却只找到一部分上传文件:备份时 node agent 直接扫描正在写入的 live volume,目录遍历先读到索引,稍后才读到数据块,得到的是跨越多个时刻的文件集合。另一个 PVC 完全没进仓库,因为它当时没有被 Pod 挂载,文件系统移动器根本没有读入口。
紧接着,第二批恢复 Pod 被驱逐。仓库下载没有坏,目标卷也有空间,真正耗尽的是节点临时盘上的数据移动缓存;并发从每节点一条调到四条后,四个恢复任务同时解密、去重和写卷,kubelet 先触发 ephemeral storage pressure。此时增加对象存储带宽无济于事,故障发生在 node agent 所在节点。
三条路径读到的不是同一个时刻
live volume 文件备份由 node agent 从 Pod 已挂载卷的节点路径读取文件。它适合没有快照能力的 NFS、文件存储、local volume 或需要跨存储恢复的场景,但扫描期间应用仍可能写入,目录项、文件内容和多个文件之间不天然处于同一时刻。没有 Pod 挂载的孤立 PVC 也可能没有可读路径。
snapshot clone 数据移动先创建 CSI snapshot,再从 snapshot 供应一个临时克隆 PVC,把只读时间点副本挂到数据移动 Pod,最后上传到仓库。它减少 live write 对扫描的一致性干扰,也把快照从源存储搬到独立故障域;代价是依赖 snapshot API、CSI driver、临时卷供应、节点调度和额外容量。
仅保留存储快照不做文件级上传,速度通常最快,但副本可能仍绑定原账号、区域、driver、KMS 与存储后端。仓库中的 Kopia/restic 类数据则是经过分块、去重、压缩、加密和索引的数据集合,可以脱离源卷保存,却需要仓库密码、对象存储权限、维护任务和完整下载路径。
这三条路可以共同存在,但同一个卷的一次备份必须明确选择哪条数据路径。以 Velero 为例,File System Backup 与卷快照对同一卷互斥;选择 FSB 后不会再为该卷重复创建 snapshot。具体行为、限制和 restic 迁移状态应从已安装 release 对应的 Velero File System Backup 文档核对。
先锁定 release、节点与仓库
下面用 Velero 的 node-agent、PodVolumeBackup/PodVolumeRestore 和 DataUpload/DataDownload 作为可观察实现。执行前从 Velero 文档版本选择器进入与服务端相同的 release,锁定 CLI、server、对象存储插件与镜像摘要;main 是开发线,不能直接拿来承诺稳定环境行为。
实验需要 kubectl、velero CLI、Helm、一个可销毁集群、可创建 namespace/PVC/Pod 的账号,以及一套隔离对象存储 bucket。安装者还需要 DaemonSet、CRD、ClusterRole 和 Secret 权限。先保存事实:
velero version
kubectl version
kubectl get nodes -o custom-columns='NODE:.metadata.name,EPHEMERAL:.status.allocatable.ephemeral-storage'
kubectl -n velero get deploy,ds,pod
kubectl -n velero get backupstoragelocations.velero.io -o yaml
kubectl -n velero get secret velero-repo-credentials -o jsonpath='{.metadata.resourceVersion}'node agent 以 DaemonSet 覆盖数据所在节点。live-volume FSB 通常需要访问 kubelet 的 Pod volume 路径,因而会引入 hostPath、root 或特定平台的 privileged/SCC 权限;snapshot clone 路径若只挂临时 PVC,可按目标 release 支持关闭不必要的 hostPath。安装参数与平台修正以 Velero 安装文档和 CSI/storage vendor 文档为准。
代表性的安装骨架如下,真实插件名、版本、bucket、endpoint 和 credential file 必须来自隔离环境:
velero install \
--namespace velero \
--use-node-agent \
--features=EnableCSI \
--plugins=<object-storage-plugin>@<immutable-image-digest> \
--bucket=<backup-bucket> \
--backup-location-config region=<region>,s3Url=<endpoint> \
--secret-file=<temporary-credentials-file>
kubectl -n velero rollout status deployment/velero --timeout=300s
kubectl -n velero rollout status daemonset/node-agent --timeout=300s
velero backup-location get
velero repo get预期 server 与每个目标节点的 node-agent Ready,BackupStorageLocation 为 Available。首次仓库操作前设置专用 repository password;仓库创建后再改密码会让旧数据无法连接。对象存储身份只授予目标 prefix 所需的 list/get/put/delete,读取生产 PVC 的节点身份与写仓库的云身份分开审计。Secret、repository password、KMS key 和 bucket policy 必须纳入轮换与灾难恢复,不能只备份密文对象而丢掉解密入口。
正向实验一:从 live volume 进入仓库
创建隔离 namespace、PVC 和写入 Pod。文件名、内容与校验文件构成恢复断言:
apiVersion: v1
kind: Namespace
metadata:
name: mover-lab
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: live-data
namespace: mover-lab
spec:
accessModes: [ReadWriteOnce]
storageClassName: <storage-class>
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: live-writer
namespace: mover-lab
annotations:
backup.velero.io/backup-volumes: data
spec:
containers:
- name: writer
image: busybox:1.37
command: ["sh", "-c", "mkdir -p /data/inbox && printf 'alpha\nbeta\n' > /data/inbox/items && sha256sum /data/inbox/items > /data/inbox/items.sha256 && sync && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: live-datakubectl apply -f mover-live.yaml
kubectl -n mover-lab wait --for=condition=Ready pod/live-writer --timeout=180s
kubectl -n mover-lab exec live-writer -- sha256sum -c /data/inbox/items.sha256
velero backup create mover-live-rp1 --include-namespaces mover-lab --wait
velero backup describe mover-live-rp1 --details
kubectl -n velero get podvolumebackups \
-l velero.io/backup-name=mover-live-rp1 -o yaml预期源校验为 items: OK,Backup 完成,目标 PodVolumeBackup 进入终态并记录 node、PVC、路径、snapshot ID/仓库引用和完成字节。若 Backup 完成却没有 PodVolumeBackup,优先检查注解使用的是 Pod volume 名 data,而不是 PVC 名 live-data;还要确认 node-agent 覆盖 Pod 所在节点。
恢复时不要覆盖源 PVC。把 namespace 映射到隔离目标,并阻断外部消息、邮件和生产入口:
velero restore create mover-live-restore \
--from-backup mover-live-rp1 \
--namespace-mappings mover-lab:mover-restore \
--wait
velero restore describe mover-live-restore --details
kubectl -n velero get podvolumerestores \
-l velero.io/restore-name=mover-live-restore -o yaml
kubectl -n mover-restore get pod,pvc
kubectl -n mover-restore exec live-writer -- sha256sum -c /data/inbox/items.sha256预期 PodVolumeRestore 完成,目标 PVC Bound,恢复 Pod 的校验再次通过。仅看到 Velero Restore Completed 不够;若工作负载被修改器暂停,使用独立 reader Pod 挂载恢复 PVC完成校验,再决定是否启动应用。
live volume 为什么会得到混合时点
node-agent controller 为选中的 Pod volume 创建 PodVolumeBackup,在 Pod 所在节点读取 kubelet 暴露的卷目录,并交给 uploader 遍历、分块和上传。文件 A 在 T0 被读取、文件 B 在 T+N 被读取;应用若在两次读取之间提交事务,仓库可能同时包含旧 A 与新 B。单文件在读取期间被改写,也可能产生应用无法解释的内容。
可重复的反例是在 source Pod 中循环替换 manifest 和两个数据文件,备份完成后恢复并检查 manifest 声明的哈希。至少一轮会出现 manifest 与文件不匹配;这证明扫描过程不是原子快照,而不是证明 uploader 随机损坏。修复方法是使用应用支持的停写/checkpoint/hook,或改用 snapshot clone;数据库仍应优先采用原生备份协议。
孤立 PVC 也要单独处理。FSB 从 Pod 挂载路径读取,不会因为 PVC 存在就自动扫描。需要保护时创建受控 staging Pod 挂载它,并记录 owner、镜像、网络隔离和删除时点;长期保留一个无限 sleep 的高权限 Pod 会扩大攻击面,也容易把已经废弃的 PVC继续计入备份。
正向实验二:从 snapshot clone 移到仓库
当 CSI 支持 snapshot.storage.k8s.io/v1 时,可先冻结时间点,再把克隆卷交给数据移动器。Velero 的稳定发行文档把这条链称为 CSI Snapshot Data Movement:Backup 创建 VolumeSnapshot/VolumeSnapshotContent,随后创建 DataUpload;node-agent 选择节点,准备临时 BackupPVC/PV,数据移动 Pod 从克隆读取并写入仓库。上传结束后临时对象与近端快照按流程清理,仓库副本继续保留。
velero backup create mover-snapshot-rp1 \
--include-namespaces mover-lab \
--snapshot-move-data \
--wait
kubectl -n velero get datauploads \
-l velero.io/backup-name=mover-snapshot-rp1 -o wide
kubectl -n velero get datauploads \
-l velero.io/backup-name=mover-snapshot-rp1 -o yaml
kubectl get volumesnapshot,volumesnapshotcontent -A
velero repo getDataUpload 应经历 Accepted、Prepared、InProgress 一类中间状态并最终进入 Completed;准确 phase 名以目标 release CRD 为准。证据至少保留源 PVC UID、VolumeSnapshot/Content、临时 PVC/PV、处理节点、上传字节、仓库 snapshot ID 和清理后的后端资产。找不到临时对象不一定是漏做,它们可能已经被成功清理;Event、DataUpload status 与仓库引用必须能串起全过程。
恢复会创建 DataDownload,准备目标 RestorePVC,调度数据移动 Pod 下载并写入,再把恢复卷交给工作负载:
velero restore create mover-snapshot-restore \
--from-backup mover-snapshot-rp1 \
--namespace-mappings mover-lab:mover-snapshot-restore \
--wait
kubectl -n velero get datadownloads \
-l velero.io/restore-name=mover-snapshot-restore -o yaml
kubectl -n mover-snapshot-restore exec live-writer -- sha256sum -c /data/inbox/items.sha256若 DataUpload 长期停在 Prepare,先查临时 PVC Event、StorageClass、access mode、拓扑与 quota;若 InProgress 停滞,再查 data mover Pod、节点磁盘、网络、仓库和缓存。把两类故障混在“上传慢”里会错过真正的修复点。
Kopia 与 restic 是仓库引擎,不是 CSI 快照
Kopia 类引擎把文件切块、内容寻址、去重、加密并写入 repository,读取时依赖索引、缓存和 repository password。删除一个 Kubernetes Backup 往往只是让部分快照不再被引用,真正释放对象需要维护与安全窗口;Kopia 官方的 repository maintenance明确区分快速维护与完整维护,也提醒关闭安全保护会有并发损坏风险。
restic 同样维护加密仓库、snapshot、index、pack,并通过 check/prune 管理完整性和空间。它在一些旧环境里仍承载历史备份,但 Velero 已把 restic 路径放入弃用迁移过程;是否可选、何时移除以及怎样迁到 Kopia,必须查询目标 release 的 FSB 页面与 Velero deprecation policy。不要直接把 uploader 字段从 restic 改成 Kopia 后就删除旧仓库,旧恢复点仍需要原引擎、密码和索引。
两类引擎的“snapshot”是仓库中的逻辑文件集合,与 VolumeSnapshot 不是同一个 API 或生命周期。排障记录应使用 csi_snapshot_handle、repository_snapshot_id、backup_uid 三个独立字段,避免一句“快照已删除”无法判断删的是存储副本还是仓库索引。
反向实验:让容量故障在隔离仓库暴露
最安全的容量实验不是填满工作节点系统盘,而是给测试对象存储一个很小、不可自动扩容的后端卷,再上传大于它的不可压缩数据。可以用隔离 MinIO 实例承载 <capacity-test-bucket>,将后端 PVC 固定为演示容量,并确认它与任何真实备份 bucket、KMS 和账号无关。随后在 mover-capacity-lab 的源卷写入明显大于仓库可用空间的随机数据;下面的 count 是演示值,必须小于源 PVC、同时大于测试仓库的实际剩余空间:
apiVersion: v1
kind: Namespace
metadata:
name: mover-capacity-lab
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: capacity-source
namespace: mover-capacity-lab
spec:
accessModes: [ReadWriteOnce]
storageClassName: <storage-class>
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: capacity-writer
namespace: mover-capacity-lab
annotations:
backup.velero.io/backup-volumes: data
spec:
containers:
- name: writer
image: busybox:1.37
command: ["sh", "-c", "dd if=/dev/urandom of=/data/incompressible.bin bs=1M count=256 && sync && sha256sum /data/incompressible.bin > /data/incompressible.sha256 && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: capacity-sourcekubectl apply -f mover-capacity-source.yaml
kubectl -n mover-capacity-lab wait --for=condition=Ready pod/capacity-writer --timeout=300s
velero backup create mover-capacity-fail \
--include-namespaces mover-capacity-lab \
--storage-location <capacity-test-bsl> \
--wait
kubectl -n velero get podvolumebackups \
-l velero.io/backup-name=mover-capacity-fail -o yaml预期 PodVolumeBackup 或 DataUpload 进入 Failed/Cancelled,Event、data mover 日志和对象存储日志出现 quota、no space left、HTTP 507/5xx 或写入失败;Backup 不应被团队当作完整恢复点。扩容测试仓库或删除无关测试对象后重试,新的上传应完成且恢复校验通过。
同一实验再把每节点并发从基线逐级上调,观察 node-agent/data mover Pod 的 CPU、内存、ephemeral storage、缓存卷、网络与队列年龄。并发增加但吞吐不再增长、驱逐数上升或等待年龄恶化,就是该节点池的容量边界。不要在生产直接把并发翻倍;Velero 提供 node-agent concurrency与数据移动缓存 PVC配置入口,字段和默认行为按目标 release 核对。
容量模型必须包含临时副本
一次 snapshot clone 上传的空间峰值近似为“源卷 + 后端 snapshot 变化块 + 临时克隆卷 + 节点/缓存 PVC + 仓库新增块”;恢复峰值则是“仓库读取缓存 + RestorePVC + 目标卷 + 应用恢复临时文件”。live-volume 路径少了克隆卷,却把扫描 I/O 施加在正在服务的卷和节点上。大数据库文件即使只改少量字节,也可能需要完整扫描才能发现去重块,CPU 与读 I/O 不会按增量字节同比下降。
容量看板至少分开:待处理对象数与最老等待时间、每节点并发、扫描/上传/下载字节率、去重前后字节、缓存 used/limit、ephemeral storage、临时 PVC 申请量、仓库剩余空间、对象存储限流、失败重试和维护年龄。成功率不能吞掉分母;某个 node-agent 未覆盖导致任务根本没创建时,0 failures 是假平静。
费用也按路径拆开。live FSB 消耗节点 I/O、CPU、出口流量和仓库存储;snapshot clone 再增加快照、临时卷与供应 API;跨区域恢复增加下载与出口,长保留增加仓库维护和对象请求。云价格、免费额度和跨区规则从目标服务的价格页与账单导出动态计算,不在配置里写死金额。
权限、网络与多租户隔离
node-agent 能读取节点上的 Pod 卷,权限接近数据面备份管理员。限制它可调度的节点、hostPath、capability、SELinux/SCC、ServiceAccount 和网络出口;安装后保存 DaemonSet 与 RBAC diff。Velero server 管理 Backup、Restore、DataUpload、DataDownload 与仓库 CR,数据移动 Pod 只获得当前任务所需的 PVC、Secret 和 endpoint 访问。
kubectl auth can-i --list \
--as=system:serviceaccount:velero:velero
kubectl -n velero get daemonset node-agent -o yaml
kubectl get clusterrole,clusterrolebinding \
-l app.kubernetes.io/name=velero -o yaml多租户不要共享一个可浏览全部前缀的长期云密钥。按环境、租户或数据等级拆 bucket/prefix、KMS 与恢复角色;备份写入身份和灾难恢复读取身份可以分离,并在隔离账号定期验证后者。代理、私有 CA 和 S3-compatible endpoint 要同时进入 server、node-agent 与 data mover Pod 的信任链;BSL Available 只证明探测成功,不证明大对象上传、分段重试和恢复下载都能通过。
升级时先保住旧恢复点
升级前导出 Velero CRD 存储版本、BackupRepository、BSL、node-agent ConfigMap、repository password Secret 的备份责任、插件/镜像摘要,以及一组 live FSB 和 snapshot movement fixture。新旧版本在隔离 bucket prefix 双跑,比较对象选择、PVB/PVR、DataUpload/DataDownload、仓库格式、恢复校验、峰值资源和清理结果。
从 restic 迁向 Kopia 时建立“双读、单写”的阶段:新备份只写新仓库,旧引擎与凭证保持只读恢复能力,抽样恢复每个保留层级;直到旧恢复点自然过期或完成受审计的重备份,才执行 check/prune 和销毁。回滚条件要包含 CRD schema、仓库格式和插件兼容,不能只看 Helm revision。
退出一套移动器时先暂停新 Schedule,等待活动上传/下载进入终态,导出仍需保留的恢复点与仓库身份,再删除测试恢复、临时 PVC、data mover Pod、PVB/PVR、DataUpload/DataDownload。随后撤销对象存储与 KMS 身份,按引擎流程维护或销毁 repository,卸载 node-agent/server,检查 CRD、ClusterRoleBinding、hostPath、缓存 PVC、bucket prefix、生命周期规则和账单。旧仓库不能恢复、旧节点不再读取卷、旧凭证不可使用、旧资产不再产生费用,这四项同时成立才算退出完成。
实验结束时先删除两个恢复 namespace,再删除 Backup/Restore,让平台按自己的引用关系清理仓库 snapshot;确认对象存储与后端快照资产后,删除源 Pod/PVC、测试 BSL、临时 Secret、容量测试 MinIO 与 namespace。直接删 bucket 会把“清理成功”变成不可逆的数据丢失,也会跳过 repository maintenance 与审计证据。
