Pod 重建数据还在,PVC 一删却可能没了?Kubernetes 存储这 5 个坑要弄明白
Pod 重建后,服务起来了,昨天上传的文件却不见了。碰到这种场面,光看一个绿色的 Running,心里可踏实不了。
页面能访问,数据找不着,恢复工作往往比修一个报错还麻烦。我会把注意力从“容器有没有启动”,挪到“文件写到了哪块盘,这块盘跟谁绑定”。
这篇就顺着 PV、PVC、StorageClass 往下拆:配置怎么落地,为什么能保留数据,卡在 Pending 怎么查,删资源之前又该确认什么。
我觉得这件事最容易让人着急的地方,是 YAML 看着都对,数据也确实写成功了,可真到重建、迁移、删资源时,心里又没底。面试背得出三个缩写,值班时却不敢碰删除按钮,这种落差很常见。
这里还有一个容易忽略的区别。容器可写层会随容器替换而丢失,emptyDir 能跨容器重启保留,但跟着 Pod 生命周期走;PVC 提供的持久卷,才把数据从 Pod 的生死里分出来。只重启一次进程,或者只让容器重启一次,证明不了 Pod 重建以后还安全。
我还会看应用的实际写入路径。上传目录配置在 /app/uploads,卷却挂在 /data,两个目录都能写,日志也不报错,可业务文件依然落在容器里。挂了卷却没用上,比完全没挂更容易给人一种安心的错觉。
持久化存储的关键,是把数据放对卷,再管好这块卷的生命周期。
三个名字混在一起,配置就容易写歪
我习惯把 PVC 看成“申请单”:应用需要多大空间、怎样访问,写在这里。
PV 是 Kubernetes 对实际存储的登记,背后可能是云盘,也可能是 NFS。
StorageClass 则是供应规则,告诉系统用哪个驱动、什么参数去准备存储。
PVC、Pod 属于命名空间,Pod 要使用同命名空间的 PVC;PV 和 StorageClass 是集群级资源。遇到“明明有 PVC,Pod 却说找不到”,别急着怀疑磁盘,先看 namespace。
不少教程把“创建 StorageClass”和“手工创建 PV”排成连续步骤,这里得停一下。
动态供给通常是:创建 StorageClass,提交 PVC,存储供应器准备云盘并创建 PV,再把 PVC 和 PV 绑定起来。
这条路不需要自己再手写一个 PV。
静态供给适合已经有存储的情况,比如已有 NFS 导出目录。管理员写一个指向实际后端的 PV,应用提交能匹配它的 PVC。容量、访问模式、存储类别、volumeMode 都要能对上。
要固定使用某个无类别 PV,可以在 PV、PVC 中明确写 storageClassName: "",并在 PVC 用 volumeName 指定 PV 名称,避免默认 StorageClass 把申请带去动态供给。
绑定也不是把一块 PV 切碎分给好几张申请单。一个 PV 与一个 PVC 独占绑定。PVC 申请 10Gi,静态 PV 提供 20Gi,可以满足容量条件;反过来就不行。
至于多个应用共享文件,那是访问模式和后端存储能力的事,不能靠多个 PVC 重复指定同一个 PV 解决。
顺带说一句,NFS 的 PV 写了 10Gi,不等于服务端已经给这个目录设好了 10Gi 硬配额。这个坑得去存储端落实。
动手前,我会把这几件事查清
下面限定普通 EKS 集群、Linux EC2 工作节点,已经安装 EBS CSI 驱动并配置好它的 IAM 权限。命令在能访问集群的 Bash 终端执行。换成其他云或 Ceph,供应器名称和参数要换。
一些教程仍使用旧的 in-tree 供应器 kubernetes.io/aws-ebs,我这里采用 ebs.csi.aws.com。
gp2 仍是有效卷类型,本例选择 gp3,拓扑通过 WaitForFirstConsumer 配合调度决定,不照搬旧的 zones 参数。
EKS Auto Mode 使用的是 ebs.csi.eks.amazonaws.com,不能直接套这份配置;Fargate Pod 也不能挂 EBS。
这组检查只读,重点是确认自己连到了哪里、驱动是否存在、节点有哪些可用区:
kubectl config current-context
kubectl version -o yaml
kubectl get sc
kubectl get csidriver ebs.csi.aws.com
kubectl get nodes -L topology.kubernetes.io/zone
kubectl -n kube-system get deployment ebs-csi-controller
kubectl -n kube-system get daemonset ebs-csi-node版本记下来,驱动也按部署方式确认。组件名不同,就用实际名称查询。
找不到 CSI 驱动,或控制器、节点组件没就绪,先处理依赖。部署都正常,也只说明组件在运行,不能证明它有创建云盘的权限。
我会在独立的 storage-lab 命名空间做下面的验证,它会创建一块收费云盘。
碰现有业务之前,保留 Git 中的配置、当前 PVC/PV YAML、云盘 ID,并做好应用一致性备份。数据库还要考虑写入、日志和恢复点,不能把“做过快照”当成“肯定能恢复”。
这套配置,让 PVC 自己找盘
把下面保存为 storageclass.yaml:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3-retain-demo
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true我在示例里主动选择 Retain:删除 PVC 后,不自动删除对应 PV 和云盘,方便人工确认数据。
保留的卷仍占资源、仍会收费。若要换一种回收策略,新建另一个 StorageClass;已有 PV 不会跟着变化,删除前看 PV 自身的策略。
WaitForFirstConsumer 让系统等到有 Pod 使用 PVC,再结合调度约束选择创建卷的位置。EBS 有可用区限制,这能减少“盘在一个区,Pod 想去另一个区”的尴尬。
示例没有指定自管 KMS 密钥。若业务要求使用自己的密钥,不能只补一个密钥 ID 就上线,还要验证身份和密钥策略。
allowVolumeExpansion 只是允许提出扩容申请,不表示容量、文件系统一定立刻扩好,也不是提高吞吐量的开关。
保存 pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-demo
namespace: storage-lab
spec:
storageClassName: ebs-gp3-retain-demo
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi再保存 pod.yaml,用一个小容器验证挂载:
apiVersion: v1
kind: Pod
metadata:
name: data-demo
namespace: storage-lab
spec:
containers:
- name: check
image: busybox:1.37.0
command: ["sh", "-c", "sleep 86400"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-demo这里的 claimName 对应 PVC 名称,volumeMounts 和 volumes 里的 name 要一致,mountPath 是容器里真正使用的路径。
创建资源属于变更,只在前面确认过的测试环境执行:
kubectl create namespace storage-lab
kubectl apply -f storageclass.yaml
kubectl apply -f pvc.yaml
kubectl -n storage-lab get pvc
kubectl apply -f pod.yaml
kubectl -n storage-lab wait --for=condition=Ready pod/data-demo --timeout=180s
kubectl -n storage-lab get pvc,pod -o wide命名空间若已存在,确认它专用于本次测试、没有冲突资源后,再决定是否复用。
Pod 出现之前,PVC 显示 Pending 可以是正常等待。有了消费者,还得等供应、绑定、挂载完成。预期看到 PVC 为 Bound、Pod 为 Running 且就绪。
180 秒只是本次检查的等待窗口,不是存储系统的性能承诺。超时就看事件,别反复删 PVC“再试一次”。镜像拉取失败也会阻止 Pod 就绪,要跟存储问题分开判断。
文件写进去,再验证一次重建
我会先看 PVC 绑定了哪个 PV,再确认它背后的云盘:
PV=$(kubectl -n storage-lab get pvc data-demo -o jsonpath='{.spec.volumeName}')
kubectl get pv "$PV" -o yaml
kubectl -n storage-lab describe pod data-demo
kubectl -n storage-lab exec data-demo -c check -- sh -c 'printf "storage-proof\n" > /data/proof.txt && sync && cat /data/proof.txt'PV 的 spec.csi.driver 应是本例的 EBS CSI 名称,spec.csi.volumeHandle 是后端卷标识。Pod 事件不应持续出现挂载失败,文件预期输出 storage-proof。
这些都是预期结果,用来对照验证。
空间也要从挂载点看:
kubectl -n storage-lab exec data-demo -c check -- df -h /data
kubectl -n storage-lab exec data-demo -c check -- df -i /data文件系统可用空间不一定等于 PVC 的申请数字,格式化和保留空间都会占一部分。
容量还有剩余却写不进去,要看 inode、权限和具体报错,不要见到一次写入失败就扩容。这里的测试容器以默认用户运行,业务用了非 root 用户,目录权限也需要单独验证。
接着只删除测试 Pod,保留原来的 PVC 和 PV:
kubectl -n storage-lab delete pod data-demo
kubectl apply -f pod.yaml
kubectl -n storage-lab wait --for=condition=Ready pod/data-demo --timeout=180s
kubectl -n storage-lab exec data-demo -c check -- cat /data/proof.txt
kubectl -n storage-lab get pvc data-demo -o wide预期仍能读到 storage-proof,PVC 的 VOLUME 也应保持一致。这才验证了“重建后接回原卷”,而不只是目录可写。
原理其实不玄乎。
Kubernetes 的存储控制器协调 PVC/PV 绑定,CSI 供应器调用云 API 创建卷并登记 PV,调度器考虑节点与卷的拓扑。
涉及挂接时,由 CSI 控制器侧组件协调云盘与节点;节点上的 CSI 插件再把卷准备好、挂到 Pod 的路径,容器在 /data 读写后端文件系统。
这也解释了为什么“驱动 Pod 都 Running”还不够。控制器无法调用云 API,盘可能建不出来;节点插件或节点挂载出了问题,PV 已经 Bound,容器仍可能停在 ContainerCreating。
看 PVC 事件判断供应阶段,看 Pod 事件判断调度、挂接和挂载,排查才不会绕圈。
Pod 被删除,不等于这块卷也被删除。新 Pod 使用原 PVC,就能继续访问原来的数据。本例用单个测试 Pod 演示,生产应用还得设计副本、身份和卷的关系,不能把这份验证清单直接当高可用方案。
还有个现实问题:EBS 卷属于特定可用区。卷所在区没有合适节点,新 Pod 仍可能起不来。
持久化把数据和 Pod 的生命周期分开,跨区容灾还得另外设计。
Pending 不一定坏了,但这次确实是权限
下面用一个脱敏后的典型场景说明。时间、现象和输出均为示例,不对应某次真实事故。
09:20,新建的验证实例一直没就绪,旧实例仍在服务,业务错误率没有明显变化。近期变更是调整了 CSI 控制器的身份配置。
我的初判会在存储类别、调度条件和云 API 权限之间排,而不是立即重启节点。
先保留现场:
kubectl -n storage-lab describe pvc data-demo
kubectl -n storage-lab get events --sort-by=.metadata.creationTimestamp
kubectl -n storage-lab describe pod data-demo
kubectl -n kube-system logs deployment/ebs-csi-controller -c ebs-plugin --since=15m --tail=100如果只有 WaitForFirstConsumer 提示,同时还没创建 Pod,那是在等消费者。
若 Pod 事件说节点可用区、资源或亲和性不满足,就沿调度方向查。
这个示例已经有消费者,StorageClass 名称也对,PVC 事件却反复出现:
ProvisioningFailed
could not create volume in EC2: UnauthorizedOperation09:24,控制器日志在同一时间窗也出现相同的授权失败。
此时有证据指向“创建云盘的请求被拒绝”。组件在运行,也不能排除 IAM 问题。
临时止损是停止放量,让旧实例继续承担流量,并保留新 PVC 等待修复。不要为了赶进度,给控制器塞一份管理员权限。
09:30,我会读取控制器实际使用的 ServiceAccount,而不是猜一个角色名称:
SA=$(kubectl -n kube-system get deployment ebs-csi-controller -o jsonpath='{.spec.template.spec.serviceAccountName}')
kubectl -n kube-system get serviceaccount "$SA" -o yamlIRSA 要对照 eks.amazonaws.com/role-arn 注解和角色信任关系。
Pod Identity 则到 EKS 的实际关联里核对命名空间、ServiceAccount 和角色,不能因为没有注解就判断没配置身份。
再对照同时间窗 CloudTrail 中失败的 CreateVolume 记录及其 userIdentity,确认被拒绝的请求到底用了哪个身份。
接着检查这个身份的有效授权,包含角色策略和其他可能的限制。示例中核查发现,身份调整后关联到了不具备所需 EBS 授权的角色,这才把根因从“授权被拒”收窄到具体绑定问题。
按现有 IaC 配置修正绑定与驱动所需的最小权限;若用自管 KMS 密钥,还要核对密钥策略。
修复后保留原 PVC,让供应器重试。预期供应事件成功、PVC 变成 Bound、Pod 就绪,再做前面的写入和重建读取验证。
恢复后控制器不再反复报错、应用错误率和耗时符合基线,我才继续小范围验证。观测窗口按业务发布规则定。
任何一项恶化就停止放量,切回保留的旧实例和旧 PVC。已经产生的新数据,要按数据回退方案处理,回滚 YAML 不能把它凭空还原。
涉及正式写入迁移时,切回之前要暂停或隔离新路径的写入,核对恢复点和新增数据,按业务方案对齐后再恢复服务。
旧 PVC 留着,只能证明旧数据还在,不能证明它包含迁移后的全部写入。没有验证过数据回退,就别把“流量能切回来”当成迁移可回滚。
防复发也得做实:把 CSI 身份变更纳入发布前验证,在独立命名空间创建小 PVC 和消费者,验证真实云 API 授权;持续监控重复的 ProvisioningFailed,不能只看组件是否 Running。
这 5 个坑,建议贴在变更单上
- 不要把删 Pod 和删 PVC 当成一回事。 删 Pod 通常不会触发卷回收;删除 PVC 则要看 PV 的策略。Delete 通常会连同底层云盘清理,Retain 会留下 PV 和卷。下手前看实际 PV,别只看 StorageClass。
- 不要把 RWO 理解成“只准一个 Pod”。 ReadWriteOnce 是单节点读写,同节点可能有多个 Pod 使用。需要严格限制单 Pod,可评估 CSI 的 ReadWriteOncePod,并确认集群和驱动组件支持。RWX 也得看后端能力,普通 EBS 卷不能照着 NFS 的方式共享写。
- 不要为了让 Pending 消失就删申请。 等待消费者、权限失败、配额不足、拓扑冲突都可能卡住,事件才能帮你分路。删掉重建会丢现场,若原卷已有数据还可能扩大风险。
- 不要把挂载成功当成备份成功。 应用写错目录、文件误删、数据库逻辑损坏,PVC 都替你挡不住。保留备份还不够,务必在隔离环境恢复一次,确认数据能读、业务能用。
- 不要以为保留和扩容都有自动退路。 Retain 后的 PV 常会进入 Released,需要人工登记并处理,不能直接当空盘复用。允许扩容也要驱动和文件系统支持,扩出来的容量不能靠改回 YAML 缩回去。
这份清单我会和配置放在一起:上下文、驱动、命名空间、可用区、实际 PV 策略、备份恢复记录、验证结果。
少一项,变更单上的“已验证”就容易变成一句空话。
能保住数据的,是检查和退路
测试结束,要清理就按登记的资源处理。
本文选择 Retain,删 PVC 后 PV 和 EBS 卷仍可能留着。确认测试数据无用、没有消费者、没有恢复需求,再人工处理残留存储。别复制一条批量删云盘命令,把费用清掉的同时把数据也清了。
日常我会同时关注挂载点的使用率、inode、业务读写耗时和 EBS 排队情况,跟平时基线比较。
空间告警解决不了延迟问题,云盘扩容也不能代替瓶颈分析。Released PV 和未使用云盘则定期对账,记录负责人、保留原因和处理期限,免得过几个月只剩一句“这块盘谁的”。
部分旧资料提到的 Recycle 已经弃用,新方案不建议再选。
真正需要想清楚的是:谁申请盘、谁创建盘、谁挂载盘,以及谁有权决定这份数据什么时候消失。
你现在就能做一个小动作:查一查手头重要 PVC 对应 PV 的回收策略,顺便核对备份是否做过恢复验证。
服务能起来,只说明程序在跑;数据能恢复,才说明这套存储有退路。
我想把这些能落到命令、配置和现场判断上的东西继续写下去。觉得有用,欢迎关注 @运维躬行录,点个赞、点个在看,也转发给一起值班的同事。下次碰到存储故障,至少知道该先看哪里。
公众号:耕云躬行录
个人博客:躬行笔记