上一篇把配置与密钥从镜像剥离,但 Pod 重建后可写层仍会消失。本篇区分临时卷与持久卷,并用 PVC 完成一次“写入数据、删除 Pod、重新读取”的实验,建立存储生命周期的正确边界。
一、痛点:Pod 可替换,数据不能一起消失
容器可写层适合临时文件,不适合业务状态。容器重启、Pod 重建或迁移节点后,它可能消失。emptyDir与 Pod 同生共死,适合同一 Pod 内容器交换文件、缓存和临时工作区;它能跨容器重启保留,却不能跨 Pod 删除保留。把数据库放进 emptyDir 只是把数据事故延后。
持久化涉及 PersistentVolume、PersistentVolumeClaim 和 StorageClass。PVC 表达工作负载对容量、访问模式和存储类的需求;动态制备器据此创建 PV;Pod 引用 PVC 而不是绑定某块云盘。这个抽象让应用清单不必知道具体磁盘 ID,同时保留管理员对后端、扩容与回收策略的控制。
访问模式容易误解。ReadWriteOnce 表示卷可由单个节点读写,不等同于只允许一个 Pod;同节点的多个 Pod 可能同时使用。ReadWriteOncePod 才是集群范围单 Pod 读写,但需要 CSI 支持。ReadWriteMany 需要 NFS、分布式文件系统或支持它的云存储,普通块盘通常不提供。
二、原理:绑定、挂载和回收是三段生命周期
PVC 与 PV 绑定后,调度器还要考虑卷所在可用区和节点拓扑。StorageClass 使用WaitForFirstConsumer可推迟制备,等 Pod 调度约束明确后再选择拓扑,避免卷创建在无法使用的区域。kind 默认存储类适合学习,生产必须核对制备器、扩容、快照与故障域。
删除 Pod 不会删除 PVC;删除 PVC 后 PV 如何处理由 reclaimPolicy 决定。Delete 常随之删除后端卷,Retain 则保留并要求人工回收。两者都不是备份策略。快照也通常与源存储在同一故障域或账户,关键数据应有独立备份、恢复演练和明确 RPO/RTO。
下面的清单创建 PVC 和一个单副本 Deployment。initContainer 仅在文件不存在时写入初值,主容器通过 HTTP 提供该文件。单副本是因为示例文件不是并发安全数据库;真实数据库优先使用成熟 Operator 或托管服务。
apiVersion:v1kind:PersistentVolumeClaimmetadata:name:web-dataspec:accessModes:-ReadWriteOnceresources:requests:storage:256Mi---apiVersion:apps/v1kind:Deploymentmetadata:name:web-storagespec:replicas:1selector:matchLabels:app.kubernetes.io/name:web-storagetemplate:metadata:labels:app.kubernetes.io/name:web-storagespec:initContainers:-name:initializeimage:busybox:1.36.1command:["sh","-c"]args:-test-f /data/index.html||printf 'persistent-v1\n'>/data/index.htmlvolumeMounts:-name:datamountPath:/datacontainers:-name:webimage:nginx:1.27.4-alpineports:-name:httpcontainerPort:80volumeMounts:-name:datamountPath:/usr/share/nginx/htmlvolumes:-name:datapersistentVolumeClaim:claimName:web-data三、实现:亲手证明数据跨 Pod 存活
脚本检查 PVC Bound,找到当前 Pod,写入唯一内容,然后删除 Pod 并等待新实例 Ready。最后读取文件并比较。这里不依赖 Service,直接在容器内部检查,减少网络因素干扰。
#!/usr/bin/env bashset-euopipefailtest"$(kubectl config current-context)"="kind-k8s-lab"kubectl apply-fstorage.yaml kubectlwait--for=jsonpath='{.status.phase}'=Bound pvc/web-data--timeout=120s kubectl rollout status deployment/web-storage--timeout=120sold_pod="$(kubectl get pod-lapp.kubernetes.io/name=web-storage-ojsonpath='{.items[0].metadata.name}')"value="survived-pod-replacement"kubectlexec"${old_pod}"--sh-c"printf '%s\n' '${value}' > /usr/share/nginx/html/index.html"kubectlexec"${old_pod}"--cat/usr/share/nginx/html/index.html kubectl delete pod"${old_pod}"--wait=true kubectl rollout status deployment/web-storage--timeout=120snew_pod="$(kubectl get pod-lapp.kubernetes.io/name=web-storage-ojsonpath='{.items[0].metadata.name}')"test"${old_pod}"!="${new_pod}"actual="$(kubectlexec"${new_pod}"--cat/usr/share/nginx/html/index.html)"test"${actual}"="${value}"echo"old_pod=${old_pod}"echo"new_pod=${new_pod}"echo"data=${actual}"kubectl get pvc web-data kubectl getpv预期旧、新 Pod 名称不同,而 data 仍为survived-pod-replacement。这证明的是该集群中卷跨 Pod 重建保留,不证明跨集群、跨区域或误删 PVC 后可恢复。必须区分高可用、持久化和备份三个能力。
StatefulSet 适合需要稳定网络身份、稳定序号和每副本独立 PVC 的应用。它不会自动让数据库具备复制、一致性或故障转移能力。若应用本身不知道如何组建集群,换成 StatefulSet 也不会得到可靠数据库。无状态前端仍应使用 Deployment。
四、踩坑:权限、拓扑与扩容
挂载后 Permission denied 常来自镜像用户 UID 与卷所有权不一致。可以让镜像初始化正确权限,或合理设置fsGroup;不要为了省事长期运行 privileged 或 chmod 777。大卷递归修改权限会拖慢启动,支持时可使用fsGroupChangePolicy降低重复操作。
PVC Pending 时先 describe 查看事件:可能没有默认 StorageClass、请求的访问模式不支持、容量不足,或拓扑无法满足。Pod Pending 也可能是卷节点亲和性冲突。扩容前确认 StorageClassallowVolumeExpansion,文件系统是否支持在线扩容,并做好备份;缩容通常不受支持。
删除 Deployment 不会自动删 PVC,这有利于防误删,也会留下费用。清理实验应先删除 Deployment,再明确确认数据不再需要后删除 PVC。生产中可用资源标签、所有者和费用告警治理遗留卷,不能靠定期盲删。
五、验证:用恢复能力定义存储完成
存储验收至少覆盖 Pod 重建、节点迁移、容量告警、卷扩容、快照创建和从备份恢复。只有最后一项在隔离环境成功,才能声称备份可用。还要测应用一致性:崩溃时写到一半的文件、数据库事务与文件系统快照是否一致。
本篇得到的可复用方法是用 PVC 表达需求,用 StorageClass 隔离后端,用故障实验验证生命周期,并把备份恢复独立验收。清理前可保留清单,确认实验数据无价值后执行kubectl delete deployment/web-storage与kubectl delete pvc/web-data。
下一篇回到无状态 web,在集群中引入 Gateway API,把外部请求通过 Gateway 和 HTTPRoute 路由到 Service,并诊断路由为何未被控制器接受。
容量规划还要区分逻辑用量、文件系统可用量和后端配额。监控卷使用率、索引节点、读写延迟、错误与队列深度,并按增长速度预测耗尽时间。告警阈值应留出扩容和审批所需的操作窗口,而不是到百分之九十九才通知。数据库还需监控日志盘、临时空间与复制延迟,因为总容量充足也可能由局部资源耗尽导致停写。
恢复演练建议新建隔离命名空间,从指定时间点的备份恢复,然后运行数据校验与应用只读查询;记录下载、挂载、恢复和验证各阶段耗时。恢复成功后再删除演练资源,保留报告而非保留敏感副本。若恢复依赖某位管理员记忆中的命令,就还没有真正达到可恢复。把这些步骤自动化,但对覆盖现有数据的动作保持显式审批。
存储选型可先写成确定性决策,而不是看到“有数据”就一律申请 PVC。下面程序根据是否需要跨 Pod、是否共享写入和数据重要性给出卷类型,并拒绝本地临时卷承载关键数据。
fromdataclassesimportdataclass@dataclass(frozen=True)classDataNeed:name:strsurvives_pod:boolshared_writers:boolcritical:booldefchoose_volume(need:DataNeed)->str:ifnotneed.survives_pod:return"emptyDir"ifneed.shared_writers:return"PVC(ReadWriteMany)"return"PVC(ReadWriteOnce)"needs=[DataNeed("cache",False,False,False),DataNeed("uploads",True,True,True),DataNeed("database",True,False,True),]forneedinneeds:selected=choose_volume(need)ifneed.criticalandselected=="emptyDir":raiseRuntimeError(f"critical data is ephemeral:{need.name}")print(f"{need.name}={selected}")运行输出:
cache=emptyDir uploads=PVC(ReadWriteMany) database=PVC(ReadWriteOnce)第二个程序模拟写入校验和、Pod 被替换、重新挂载后读取的验收逻辑。真正的集群实验应把volume替换成挂载目录,但“比较原始校验和而非只看文件存在”这一验收方式保持不变。
importhashlibdefchecksum(data:bytes)->str:returnhashlib.sha256(data).hexdigest()volume:dict[str,bytes]={}pod_a="writer-7fd9"payload=b"order=K8S-2026;amount=128"volume["/data/record.txt"]=payload written_hash=checksum(payload)print(f"writer={pod_a}bytes={len(payload)}")delpod_a pod_b="reader-a314"restored=volume["/data/record.txt"]restored_hash=checksum(restored)matches=written_hash==restored_hashprint(f"reader={pod_b}bytes={len(restored)}")print(f"checksum_match={matches}")ifnotmatches:raiseSystemExit("restored data is corrupted")print("persistence=PASS")运行输出:
writer=writer-7fd9 bytes=25 reader=reader-a314 bytes=25 checksum_match=True persistence=PASS参考来源
- Persistent Volumes
- Storage Classes
- Volumes
- StatefulSets
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《Kubernetes 上手实战》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。