K8up PVC 与应用感知备份
一次恢复演练卡在最容易被忽略的地方:K8up 的 Check 成功,restic 仓库也能列出快照,Restore Job 却把“最新快照”恢复到了空 PVC。日志没有网络错误,文件系统也可写;真正的原因是仓库里每个 PVC 和应用命令输出都有独立快照,默认 latest 恰好属于另一个路径。仓库完整不等于选中了正确数据,Restore 完成也不等于应用可启动。
另一个现场更隐蔽。数据库 Pod 使用应用感知命令把逻辑 dump 写到标准输出,备份成功后,值班人员按普通 PVC Restore 创建目标卷,结果什么也没有。K8up 官方恢复语义明确区分文件路径快照与 stdin 快照:应用命令和 PreBackupPod 的输出需要用 restic dump 或 mount 取回,不能假装成原 PVC 文件树。
K8up 保护的是数据路径,不是整套集群对象
K8up 是集群级 Kubernetes Operator。长驻的 k8up operator 观察 k8up.io/v1 的 Schedule、Backup、Check、Prune、Restore 等对象,并在对象所在 namespace 创建一次性 Job;Job 中的 k8up restic 负责扫描 PVC、写入仓库、检查、清理或恢复,结束后退出。
Schedule 把 backup、check、prune 等周期动作和 backend 放在一起;Backup 表示一次备份请求;Check 验证 restic repository 的结构和数据可读性;Prune 按保留规则执行 forget/prune;Restore 把选定快照恢复到已有 PVC 或另一个对象存储位置。Snapshot 是仓库快照在 Kubernetes 中的投影,能提供 ID、路径和 repository,但它不是 PVC,也不代表业务已经恢复。
K8up 不备份 Deployment、StatefulSet、Service、RBAC、CRD 等 Kubernetes 对象。灾难恢复时,这些声明要由 GitOps、IaC 或另一条对象备份链重建,再把与其版本匹配的 PVC 数据接回。把 K8up 单独称为“整集群备份”会在空集群恢复时留下无法填补的缺口。
安装先锁定 CRD、Chart 和平台组合
K8up 需要真实 Kubernetes 调度、PVC 挂载、Job、RBAC 和 Secret,单机二进制、Docker 或 Compose 只能帮助阅读 CLI,不能证明备份路径。实验集群至少要有 Linux worker、动态 StorageClass、能从 Job 访问的 S3-compatible 或其他受支持 backend,以及可创建 ClusterRole/ClusterRoleBinding 的安装身份。
K8up 的发布由 operator/app 版本、Helm chart 和 CRD 共同组成。先从 版本化文档与 官方 release选择相互匹配的发行线,再查 系统要求。不要把 master 文档或浮动 chart 当作生产保证。
export K8UP_CHART_VERSION='<target-chart-version>'
export K8UP_RELEASE_TAG='<matching-release-tag>'
helm repo add k8up-io https://k8up-io.github.io/k8up
helm repo update
helm search repo k8up-io/k8up --versions | head
kubectl apply --server-side \
-f "https://github.com/k8up-io/k8up/releases/download/${K8UP_RELEASE_TAG}/k8up-crd.yaml"
helm upgrade --install k8up k8up-io/k8up \
--version "$K8UP_CHART_VERSION" \
--namespace k8up-system \
--create-namespace \
--wait --timeout 10m
kubectl -n k8up-system get deploy,pods
kubectl api-resources | grep k8up.io
k8up --version升级时 CRD 必须先于 Helm release,且不能靠 --reuse-values 跨越 breaking changes。若没有安装本地 CLI,最后一条可省略,以 operator 镜像 digest、chart metadata 和 Pod 日志中的版本为证据。
新发行线可能改变 Kubernetes API streaming、CPU 架构或 Pod exec 的支持边界。应用感知命令依赖 pods/exec,因此 Kubernetes 版本不兼容时,普通 PVC 备份也许能运行,而应用命令稳定失败。安装验收必须分别覆盖 PVC 和 exec 两条路径。
先准备隔离仓库与恢复密钥
下面实验使用一个 S3-compatible endpoint。先创建 k8up-lab,再从受控密钥系统注入两个 Secret:对象存储身份与 restic repository password。示例值只是占位符,不能提交真实凭证。
kubectl create namespace k8up-lab
kubectl -n k8up-lab create secret generic backup-credentials \
--from-literal=username='<access-key>' \
--from-literal=password='<secret-key>'
kubectl -n k8up-lab create secret generic backup-repo \
--from-literal=password='<repository-password>'repository password 是解密恢复所必需的材料,丢失后对象还在也无法读取。它必须在集群外保存受控副本,并与对象存储访问密钥分开授权、轮换和撤销。能够读取两组 Secret 的用户可绕过 Operator 直接用 restic 枚举仓库,所以 Kubernetes namespace 并不是共享 repository 的密码学隔离边界。
backend 只配置一种存储类型。S3、Azure、GCS、B2、local、Swift、REST 的字段不同,应从目标版本 API Reference生成。自签 CA 或 mTLS 使用 tlsOptions 与 Secret volume,不要长期关闭 TLS 校验。
用最小 PVC 跑通 Backup
创建一个写入可验证序列的 PVC 和 Pod,并显式标记它参与备份:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: subject-data
namespace: k8up-lab
labels:
backup.example.com/set: subject
annotations:
k8up.io/backup: "true"
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: subject-writer
namespace: k8up-lab
spec:
containers:
- name: writer
image: busybox:1.37
command: ["sh", "-c", "i=0; while true; do i=$((i+1)); printf '%s\\n' \"$i\" >> /data/sequence; sleep 2; done"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: subject-datakubectl apply -f subject.yaml
kubectl -n k8up-lab wait pod/subject-writer --for=condition=Ready --timeout=180s
kubectl -n k8up-lab exec subject-writer -- tail -n 3 /data/sequence再创建一次性 Backup。endpoint 与 bucket 使用隔离测试值:
apiVersion: k8up.io/v1
kind: Backup
metadata:
name: subject-backup
namespace: k8up-lab
spec:
failedJobsHistoryLimit: 2
successfulJobsHistoryLimit: 2
labelSelectors:
- matchLabels:
backup.example.com/set: subject
backend:
repoPasswordSecretRef:
name: backup-repo
key: password
s3:
endpoint: https://s3.example.com
bucket: k8up-lab
accessKeyIDSecretRef:
name: backup-credentials
key: username
secretAccessKeySecretRef:
name: backup-credentials
key: passwordkubectl apply --server-side --dry-run=server -f backup.yaml
kubectl apply -f backup.yaml
kubectl -n k8up-lab get backup subject-backup -w
kubectl -n k8up-lab get jobs,pods
kubectl -n k8up-lab get snapshots预期出现 Backup 对应的 Job/Pod,Backup conditions 结束,Snapshot 的 spec.paths 包含 /data/subject-data。保存完整 snapshot ID,不只保存短名称。若 Job Pending,先看调度 Event、PVC access mode、node affinity、taint/toleration、PodSecurity 和镜像拉取;若 Pod Running 后失败,再看 backend DNS、CA、凭证、repository lock 和 restic 日志。
PVC 调度为什么是常见故障源
K8up 要让备份 Job 读取 PVC。ReadWriteOnce 不等于“只能一个 Pod”,而是受节点与 CSI 挂载规则约束;现有工作负载、PV node affinity、拓扑和调度器共同决定 Job 能否落到可挂载节点。local PV 或 WaitForFirstConsumer 场景尤其容易出现 Job 被调到错误拓扑、卷无法 attach 或长时间 Pending。
用下面的证据链定位,不要先删 Pod 重试:
kubectl -n k8up-lab describe job <backup-job>
kubectl -n k8up-lab describe pod <backup-pod>
kubectl -n k8up-lab get pvc subject-data -o yaml
kubectl get pv <bound-pv> -o yaml
kubectl get volumeattachments.storage.k8s.io需要固定调度时,用 PodConfig 设置 nodeSelector、affinity、tolerations 和 securityContext,并引用到 Schedule 或具体动作。动作级 PodConfig 优先于 Schedule 级配置。它不能覆盖 runner 的 image、command、container name 和 args,因此不应用 PodConfig 偷换备份程序。
apiVersion: k8up.io/v1
kind: PodConfig
metadata:
name: backup-placement
namespace: k8up-lab
spec:
template:
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: k8up
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]真实 node affinity 必须从 PV 拓扑推导,不能把某个节点名写死进共享模板。Restore Job 同样需要能挂载目标 PVC,恢复时还要核对 runAsUser、fsGroup 与原文件属主。
正向恢复:用 snapshot ID 和 paths 双重约束
先停止写入并记录源水位,然后创建一个不参与后续备份的目标 PVC:
kubectl -n k8up-lab exec subject-writer -- tail -n 1 /data/sequence
kubectl -n k8up-lab delete pod subject-writer --wait=trueapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: subject-restore
namespace: k8up-lab
annotations:
k8up.io/backup: "false"
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi从 kubectl -n k8up-lab get snapshot <name> -o yaml 取得属于 /data/subject-data 的完整 ID,然后创建 Restore。目标 claim 与源路径不同,因此 paths 应验证仓库中确实存在源 PVC 路径:
apiVersion: k8up.io/v1
kind: Restore
metadata:
name: subject-restore
namespace: k8up-lab
spec:
snapshot: <full-snapshot-id>
paths:
- /data/subject-data
restoreMethod:
folder:
claimName: subject-restore
backend:
repoPasswordSecretRef:
name: backup-repo
key: password
s3:
endpoint: https://s3.example.com
bucket: k8up-lab
accessKeyIDSecretRef:
name: backup-credentials
key: username
secretAccessKeySecretRef:
name: backup-credentials
key: passwordkubectl apply -f restore.yaml
kubectl -n k8up-lab get restore subject-restore -w
kubectl -n k8up-lab get jobs,pods恢复结果会保留源路径层级。挂载目标 PVC 后,用 find 确认位置,再验证文件非空、数字严格递增、末尾水位不超过备份时源端水位。若应用要求固定 UID/GID,在 Restore 中设置经过验证的 podSecurityContext.fsGroup/runAsUser,不要恢复后用递归 chmod 777 掩盖权限模型。
反向实验:默认 latest 恢复错数据
再创建第二个 PVC,写入 other-dataset,执行一次相同 backend 的 Backup。随后创建不带 snapshot 和 paths 的 Restore。K8up 会选择 latest,但默认不会证明该快照包含目标 PVC;恢复可能得到第二个 PVC 的路径或与预期无关的数据。
稳定证据是:Restore Job 成功,目标卷可挂载,但 find 显示路径并非 /data/subject-data,业务校验失败。修复后删除错误目标 PVC,重新创建空 PVC,指定完整 snapshot ID 与 paths 再恢复。不要在装有部分数据的 PVC 上反复覆盖,因为旧文件可能保留并污染结果。
时间过滤也不能代替精确 ID。若过滤条件没有匹配项,行为可能回退到 latest;恢复审批应记录 snapshot ID、repository、paths、源 namespace/PVC、备份水位和校验脚本。
应用感知命令解决一致性,也带来 exec 权限
直接扫描在线数据库 PVC 通常只能得到 crash-consistent 文件集合。K8up 可以在现有 Pod 中执行 annotation 指定的备份命令并收集 stdout,也可以用 PreBackupPod 运行受审镜像产生 dump。前者能访问应用本地上下文,但要求 Operator 的执行身份具有 pods/exec;后者隔离更清晰,却不能自动继承另一个 Pod 的本地 socket、文件和身份。
应用命令必须只把备份数据写到 stdout,把诊断写到 stderr,失败返回非零;不要把密码放在命令字符串。数据库凭证通过受限 Secret 或工作负载身份提供,命令先执行一致性协议,再输出可独立导入的 dump。具体 annotation 名称与 JSON/命令格式以目标发行线的 Application-Aware Backups为准,并先在隔离 namespace 做 server-side dry run。
这类输出在 restic 中属于 stdin 快照,不能用前面的 PVC Restore。恢复时在受控 Linux 环境配置 repository password 和 backend 凭证,用明确 snapshot ID 执行 restic dump 到文件,再校验格式、导入隔离数据库并运行关键查询。临时凭证只进入当前进程环境,结束后清除 shell 历史、卸载 mount、删除 dump 和撤销访问密钥。
平台若禁止跨租户 pods/exec,就不要为“方便备份”放宽整个集群。禁用该路径,改用受审 PreBackupPod、数据库 Operator 或数据库原生备份任务,并把生成物写进租户独立 repository。
Schedule、Check 与 Prune 组成长期维护环
验证一次 Backup 后再引入 Schedule,避免错误配置按周期放大:
apiVersion: k8up.io/v1
kind: Schedule
metadata:
name: subject-schedule
namespace: k8up-lab
spec:
backend:
repoPasswordSecretRef:
name: backup-repo
key: password
s3:
endpoint: https://s3.example.com
bucket: k8up-lab
accessKeyIDSecretRef:
name: backup-credentials
key: username
secretAccessKeySecretRef:
name: backup-credentials
key: password
backup:
schedule: "@daily-random"
failedJobsHistoryLimit: 3
successfulJobsHistoryLimit: 3
check:
schedule: "@weekly-random"
prune:
schedule: "@monthly-random"
retention:
keepLast: 5
keepDaily: 14随机表达式会被 Operator 转换并保存在 EffectiveSchedule,用于错峰。示例保留值只适合实验,生产值由业务 RPO、法规、仓库变化率和恢复演练决定。
Check 只证明 repository integrity,不能证明 PVC 能挂载、文件权限正确、数据库能启动或业务不变量成立。长期门禁应同时要求最近 Check 成功、最近抽样 Restore 成功、业务断言成功和 repository password 的离线恢复副本可用。
Prune 是破坏性仓库维护,必须独占 repository。手工 Prune 与 Schedule、另一个 K8up 安装或外部 restic prune 并发时会争用 lock,整个 Pod 可能失败。keepTags 与 tags 语义不同,修改保留策略前先在隔离 repository 演练并列出将失去的 snapshot;清理 Job/Pod 历史不会释放备份空间。
namespace 权限与真正的租户隔离
K8up Operator 默认是 cluster-wide:manager 会观察 PVC/PV/Pod,并在业务 namespace 创建 Job、ServiceAccount 和 RoleBinding;executor 为应用命令使用 pods/exec。Chart 可把 K8up 资源权限聚合给 Kubernetes admin/edit/view,但谁能在某 namespace 创建 Schedule、Backup、Restore、PodConfig 和 PreBackupPod,仍要由 RoleBinding 明确决定。
不要让普通租户同时拥有 K8up CR 写权限、任意 Secret 读取、任意 PodConfig/PreBackupPod 构造和广泛网络出口。严格租户隔离至少需要独立 bucket/prefix、独立 repository password、独立对象存储身份、受限 egress 和 namespace RoleBinding;更高要求使用独立集群或独立 K8up 安装。共享同一 repository/password 时,任何拿到凭证的人都可能枚举其他租户快照。
恢复权限比备份权限更敏感,因为 Restore 能把历史敏感数据带回可读 PVC。备份 owner、恢复审批者、Secret 管理者和 Prune 管理者应分离;审计关联 CR UID、Job、ServiceAccount、snapshot ID、repository path 和变更单。凭证轮换后分别验证 Backup、Check 和 Restore,不能只看 Secret resourceVersion 改变。
容量、费用与性能从 restic 行为估算
文件级备份要遍历目录、读取内容、分块、加密、去重并上传。大量小文件消耗元数据与对象请求,变化率高的大文件会增加读取和传输,Prune 又会扫描与重写仓库索引。容量模型至少记录 PVC 逻辑大小、每日变化量、文件数量、备份扫描时间、上传字节、repository 实际增长、Prune 时长与临时空间。
并发 Job 会争抢节点 CPU/内存、PVC IOPS、网络和对象存储限流。用 PodConfig 设置 requests/limits 和调度约束,用随机 Schedule 错峰;观察 Job 排队年龄、restic throughput、失败与 lock、节点负载和业务 I/O 长尾。对象存储容量、请求、出口与恢复演练都会产生费用,价格从实际 backend 官方页面按区域与用量核算,不在团队模板里固定单价。
K8up 适合以 namespace 为组织单位、需要 restic 文件级 PVC 备份与应用输出、愿意自行补齐 Kubernetes 对象恢复的团队。若需要整应用资源目录、跨集群应用迁移和商业支持,应评估平台型产品;若数据规模大且 CSI 快照成熟,快照加数据移动可能比反复全目录扫描更合适;数据库恢复目标严格时,数据库原生备份和日志归档通常仍是主链。
升级、回滚与安全退出
升级前导出 CRD、Schedule/Backend 脱敏 YAML、chart values、镜像 digest、repository 位置与 Secret key 映射;在测试 namespace 依次回归 PVC Backup、应用命令、Check、Prune 锁冲突和指定 snapshot Restore。老集群还要检查旧 API group、annotation、镜像名称和已弃用 Job history 字段,不能只换镜像。
CRD 不应在普通 Helm 卸载中顺手删除,否则 Schedule、Snapshot 投影和运行记录会消失,而外部 repository 仍然存在。回滚需要兼容的旧 CRD/controller、旧 values 和可读取现有 repository 的 restic 版本;至少保留一个由升级前后都恢复验证过的 snapshot ID。
安全退出时先暂停 Schedule,等待运行中 Job 结束,执行最后一次 Check 与隔离 Restore,导出 snapshot 清单和恢复说明;再决定哪些 snapshot 迁移、依法保留或通过受控 Prune 删除。随后撤销 backend 身份,删除 namespace RoleBinding、临时 ServiceAccount、PodConfig 和应用 exec 权限,最后卸载 Operator。helm uninstall 不会删除外部 repository、离线 repository password、已恢复 PVC 或对象存储费用。
实验清理也不要用删除 namespace 代替仓库清理:先删除校验 Pod 和恢复 PVC,再按保留决策处理测试 snapshot,确认 Prune 完成和仓库趋势,删除 Backup/Schedule/Restore/PodConfig 与测试 Secret,最后删除 k8up-lab。最终证据应同时回答:选中了哪个 PVC 或 stdin 快照、由哪个 Job 写入哪个 repository、哪个 snapshot ID 被恢复、恢复文件在哪里、业务如何验证、凭证与仓库如何撤销。少一个答案,备份就还没有变成可接管的恢复能力。
