Repository Reading Site
第八课:存储持久化、PV / PVC / StorageClass 与 NFS 原理
上一课我们讲清了: 但配置和凭据还不是“业务数据”。 你以后真正做系统架构时,必须把三类东西分清楚: 1. 配置 2. 凭据 3. 数据 这三者都能“进入容器”,但它们的工程语义完全不同。 通常应该: 通常应该: 通常需要考虑: 这就是为什么存储课非常关键。 如果你说不清: 那么你做项目架构时就很容易把“配置、缓存、状态、共享文件、数据库数据”混在一起。 -
第八课:存储持久化、PV / PVC / StorageClass 与 NFS 原理
为什么这一课必须接在 ConfigMap / Secret 后面
上一课我们讲清了:
- 配置如何进入 Pod
- Secret 如何进入 Pod
- 为什么有的注入方式会热更新,有的不会
但配置和凭据还不是“业务数据”。
你以后真正做系统架构时,必须把三类东西分清楚:
- 配置
- 凭据
- 数据
这三者都能“进入容器”,但它们的工程语义完全不同。
配置
通常应该:
- 可版本化
- 可替换
- 通常体量小
凭据
通常应该:
- 最小暴露
- 可轮换
- 严格控权
数据
通常需要考虑:
- 重启后还在不在
- 换 Pod 后还在不在
- 换节点后还在不在
- 多个 Pod 能不能同时读写
- 删除资源时底层目录会不会一起删
这就是为什么存储课非常关键。
如果你说不清:
emptyDir- 容器可写层
- PVC
- PV
- StorageClass
- NFS / 本地盘 / 云盘
那么你做项目架构时就很容易把“配置、缓存、状态、共享文件、数据库数据”混在一起。
先建立一个最重要的认知:容器里的“文件”不止一种来源
初学者很容易把容器里的目录都看成同一类东西。
这会直接导致错误设计。
你必须把它们拆开。
1. 容器可写层
也就是镜像启动后,叠加在镜像只读层上面的那层可写文件系统。
特点:
- 跟着容器生命周期走
- 容器重建通常就没了
- 不适合放真正有价值的数据
2. emptyDir
这是 Pod 级临时卷。
特点:
- Pod 创建时出现
- Pod 删除时消失
- 同 Pod 内多个容器可共享
- 适合缓存、临时文件、中间结果
3. 持久卷
也就是:
- PV
- PVC
- StorageClass
这条体系的核心目标是:
把“数据生命周期”从“Pod 生命周期”里解耦出来。
Kubernetes 存储主链路,必须背下来
以后只要谈持久化,你脑子里就应该立刻出现这条链路:
应用声明需求(PVC)
|
v
StorageClass 选择存储类型
|
v
Provisioner 动态创建 PV
|
v
PVC 绑定到 PV
|
v
Pod 挂载 PVC 使用
这里每一层回答的问题不同。
PVC
回答的是:
我需要多大、什么访问模式、什么存储类型。
PV
回答的是:
真正提供给你的底层卷是什么。
StorageClass
回答的是:
用哪种后端、用什么 provisioner、回收策略是什么、绑定时机是什么。
你的真实集群存储现状,不是纸上谈兵
我先核对了这套集群当前的真实存储栈。
默认 StorageClass
当前默认存储类是:
nfs-dynamic
关键字段包括:
provisioner = cluster.local/nfs-provisioner-nfs-subdir-external-provisionerreclaimPolicy = DeletevolumeBindingMode = ImmediateallowVolumeExpansion = trueparameters.archiveOnDelete = "true"
这些字段每一项都很重要。
它们分别意味着什么
reclaimPolicy = Delete
PVC 删除后,PV 也会被回收删除。
但这里因为 provisioner 还启用了:
archiveOnDelete = true
所以底层目录并不是简单粗暴地立即物理抹掉,而是会被改名归档。
这个我们后面有真实证据。
volumeBindingMode = Immediate
表示 PVC 一创建,就会尽快触发绑定和供给。
它不需要等到有 Pod 真正消费时再决定。
这在:
- NFS
- 网络共享文件系统
这种“所有节点都能挂”的场景里通常没问题。
但在:
- 本地盘
- 带拓扑限制的云盘
场景里,Immediate 和 WaitForFirstConsumer 会对调度结果产生很大影响。
allowVolumeExpansion = true
表示这个类支持 PVC 扩容。
这不代表应用一定能无感知完成扩容,但至少存储侧允许你申请更大容量。
真实后端是什么:NFS 动态供给
我检查了 provisioner 的 Deployment,看到它真实配置是:
NFS_SERVER = 10.10.0.5NFS_PATH = /srv/nfs/k8s
也就是说,集群里所有 nfs-dynamic 的 PVC,最终都落在:
wk-110.10.0.5/srv/nfs/k8s
这台机器上的 NFS 导出目录里。
我进一步 SSH 到这台节点,看到了:
exportfs -v/srv/nfs/k8s真实目录结构
真实导出配置里有:
rwsyncno_subtree_checkno_root_squash
这说明它就是一个典型的实验/平台型 NFS 后端。
我们这次做了两组存储实验
实验文件在:
对象包括:
storage-labnamespaceemptydir-demoshared-dataPVCpvc-writerpvc-reader
这次实验的目标非常明确:
- 用
emptyDir证明“数据跟着 Pod 走” - 用 PVC 证明“数据可以从 Pod 生命周期里独立出来”
- 用两个不同节点上的 Pod 证明
RWX + NFS的共享语义 - 用真实 PV / NFS 目录证明“PVC 最终落到哪里”
实验一:emptyDir 不是持久化存储,它只是 Pod 级临时卷
我专门设计了一个:
emptydir-demo
它里面挂了两个卷:
/work- 普通
emptyDir
- 普通
/memoryemptyDir.medium: Memory
为什么这样设计
因为这能同时讲清两件事:
emptyDir是临时卷emptyDir还有“落盘型”和“内存型”两种常见表现
我在容器里看到的真实挂载
/proc/mounts 显示:
/work是ext4/memory是tmpfs
这意味着:
/work走节点本地磁盘/根文件系统的临时空间/memory直接吃节点内存
这两个都安全吗
都不是“持久化数据”的正确归宿。
只是用途不同。
普通 emptyDir
适合:
- 临时工作目录
- 解压中间文件
- 排序/转换缓存
medium: Memory
适合:
- 极短生命周期热点缓存
- 临时小文件
- 对 I/O 延迟很敏感、但可丢失的数据
但你必须警惕:
- 它吃的是内存
- 大了会造成内存压力
- Pod 删了数据必丢
我确实写了文件,然后删除 Pod 验证
我先在 emptydir-demo 中写入:
/work/data.txt/memory/cache.txt
然后删除 Pod,再用同一份清单重建。
重建后的真实结果
新 Pod 里:
/work是空目录/memory是空目录- 两个文件都不存在
这说明什么
这说明 emptyDir 的生命周期绑定的是:
- Pod
不是:
- 容器
- Namespace
- 节点
也不是:
- “只要名字一样就还能找到旧目录”
所以你如果把数据库、上传文件、模型文件、用户数据放在 emptyDir 里,本质上就是把它们交给 Pod 生命周期赌博。
实验二:PVC 在没有消费者 Pod 时就已经 Bound 了
我创建了一个 PVC:
storage-lab/shared-data
规格是:
1GiReadWriteManystorageClassName: nfs-dynamic
真实结果
在还没有创建 pvc-writer / pvc-reader 之前,它就已经是:
STATUS = Bound
为什么会这样
因为这套集群的 nfs-dynamic 使用:
volumeBindingMode = Immediate
也就是说,PVC 一出现,控制器就会立刻走动态供给流程。
这条链我拿到了完整证据
kubectl describe pvc shared-data 事件里真实出现了:
ExternalProvisioningProvisioningProvisioningSucceeded
而 provisioner 日志里,也看到了对应记录:
provision "storage-lab/shared-data" class "nfs-dynamic": startedvolume "pvc-b14e5bac-dd83-48be-8191-f89acccd2a20" provisionedsucceeded
这说明动态供给不是黑箱,而是有清晰控制链的。
PV 最终长什么样,我也给你看了
这个 PVC 自动生成的 PV 是:
pvc-b14e5bac-dd83-48be-8191-f89acccd2a20
真实 PV YAML 里最关键的字段是:
accessModes: [ReadWriteMany]nfs.server: 10.10.0.5nfs.path: /srv/nfs/k8s/storage-lab-shared-data-pvc-b14e5bac-dd83-48be-8191-f89acccd2a20persistentVolumeReclaimPolicy: Delete
这一步非常重要,因为它把抽象的 PVC 申请,真正落到了:
- 哪台机器
- 哪个目录
- 哪种协议
这才是真正的“知其所以然”。
实验三:两个不同节点上的 Pod,同挂一块 RWX PVC
为了把共享语义讲透,我没有让两个 Pod 都跑在同一台机器上。
而是刻意这样安排:
pvc-writer固定在us590068728056pvc-reader固定在wk-1
为什么要这样做
因为我要验证的不是“同一节点进程共享目录”。
我要验证的是:
两台不同节点上的 Pod,是否能同时看到同一份数据。
这才是 RWX + NFS 真正有价值的地方。
实际结果
我在 pvc-writer 里写入:
/data/created-at.txt/data/source.txt
然后立即去 pvc-reader 看。
最终在 pvc-reader 中也能看到同样两份文件和内容。
这说明:
- 这不是 Pod 本地目录
- 也不是节点本地目录
- 而是共享网络文件系统
再补一条底层证据
我在 pvc-writer 的 /proc/mounts 里看到了:
/data挂的是nfs4- 服务端是
10.10.0.5 - 路径正是那条
storage-lab-shared-data-pvc-...
也就是说,从容器视角看,它确实就是一块 NFS v4.2 挂载的文件系统。
一个值得你作为专家记住的小现象
在第一次跨节点读取时,我短暂观察到一个现象:
pvc-reader先看到了created-at.txtsource.txt没立刻出现- 随后几秒内再次读取时,两份文件都稳定可见
我把这件事记下来,是因为它很像网络文件系统里常见的:
- 目录项可见性时机
- 客户端缓存
- 元数据同步节奏
这是一种基于观测的推断,不是我在这次实验中彻底证明的唯一原因。
但它足够提醒你一件事:
共享文件系统不是分布式事务总线,不要用“文件刚写完、另一边立刻
ls一次”这种弱同步方式做严肃协调。
如果业务需要强同步、幂等、竞争控制,应该引入更明确的协调机制。
实验四:删除使用 PVC 的 Pod,数据仍然在
我删除了:
pvc-writer
但没有删除:
shared-dataPVC
然后再重新创建 pvc-writer。
重建后的真实结果
新 pvc-writer 挂上 /data 后,依然能看到:
created-at.txtsource.txt
并且 pvc-reader 一直也能看到同样内容。
这说明什么
这说明:
- Pod 生命周期结束
- 不等于数据生命周期结束
只要:
- PVC 还在
- PV 还在
- 底层卷还在
新的 Pod 就可以重新挂载原来的数据。
这正是持久化存储最核心的价值。
我还直接去 NFS 服务器看了底层目录
为了把整条链打通,我直接 SSH 到:
wk-110.10.0.5
看这条目录:
/srv/nfs/k8s/storage-lab-shared-data-pvc-b14e5bac-dd83-48be-8191-f89acccd2a20
真实结果里,我能直接看到:
created-at.txtsource.txt
这件事非常重要。
因为它证明:
- PVC 不是“神秘云对象”
- 它最终一定对应到某个真实存储后端
- 真实后端里一定有真实目录、真实文件、真实容量、真实权限问题
专家和初学者的差别之一,就是会不会沿着这条链一直追到最底层。
Access Modes 到底是什么,很多人理解错了
当前集群里我看到了两类常见声明:
ReadWriteOnceReadWriteMany
比如:
ml-platform/ml-models是RWXaiforge/aiforge-models是RWX- 很多数据库类 PVC 则是
RWO
正确认知是什么
Access Mode 表达的是:
这块卷在 Kubernetes 视角下被如何申请和使用。
它影响:
- PVC 和 PV 的匹配
- 调度/挂载语义
- 控制面的允许模型
你不要把它误解成什么
不要把它简单等同为:
- “底层物理上只能这样”
- “这是一个强分布式锁”
尤其在 NFS 这种文件共享后端上,底层能力和你请求的模式不是同一层概念。
更稳妥的专家表述应该是:
Access Mode 是 Kubernetes 与卷之间的使用契约,不是应用级并发正确性的保证。
所以如果你需要:
- 多副本共享读写
你应该明确声明:
RWX
而不是赌底层细节。
Reclaim Policy 和 archiveOnDelete,我也给你找到了真实证据
StorageClass 里
当前 nfs-dynamic 的配置是:
reclaimPolicy = DeletearchiveOnDelete = true
provisioner 日志里
我看到大量历史删除记录,例如:
delete "pvc-...": volume deletedpersistentvolume deleted
NFS 服务器目录里
我还直接看到了很多:
archived-gitea-*
目录。
这说明这套集群的删除行为不是:
- 直接把底层目录立刻物理删除
而更像是:
- PV / PVC 逻辑上删除
- 底层目录做归档改名
这在实验环境和平台排障里很有价值,因为:
- 你还能回头看旧目录
- 能追数据来源
- 能做人工救援
但也意味着:
- 存储空间会持续被历史归档占用
- 平台运维必须做归档清理策略
pvc-protection 是什么,为什么它存在
在 shared-data 的 PVC YAML 里,我也看到了:
finalizers: [kubernetes.io/pvc-protection]
这表示 Kubernetes 不希望你在卷仍可能被使用时,轻易把 PVC 直接从系统里“抹掉”。
你以后如果遇到:
- PVC 删除卡住
- 一直
Terminating
就应该想到:
- 是不是还有 Pod 在使用
- 是不是 finalizer 还在保护它
这不是故障,而是数据安全机制的一部分。
NFS 为什么常见,但为什么也不能乱用
NFS 是很多实验环境、内部平台、共享模型仓库里很常见的后端。
原因很现实:
- 便宜
- 简单
- 易懂
- 支持
RWX - 适合共享文件
- 很容易做动态供给
适合什么场景
- 共享模型文件
- 上传文件
- 报表输出
- 中小规模共享目录
- 开发/测试环境持久化
不适合什么场景
- 高 IOPS、低延迟强依赖的数据库
- 事务日志/WAL 极重的系统
- 对元数据操作延迟很敏感的负载
- 需要非常强一致语义的并发写入场景
为什么
因为 NFS 的典型问题包括:
- 所有 I/O 走网络
- 单个 NFS 服务器容易成为瓶颈或单点
- 元数据操作和小文件性能往往一般
- 并发写入的锁语义、缓存语义更复杂
- 一旦网络抖动,应用侧会感知明显
所以一个成熟的 CTO 视角,不是“有没有持久化”这么简单,而是:
哪类数据适合哪类存储后端。
这节课最重要的调试命令
看 StorageClass
kubectl get storageclass
kubectl get sc nfs-dynamic -o yaml
看 PVC 生命周期和事件
kubectl -n storage-lab get pvc
kubectl -n storage-lab describe pvc shared-data
看 PV 最终指向哪里
kubectl get pv pvc-b14e5bac-dd83-48be-8191-f89acccd2a20 -o yaml
看 Pod 里卷实际是什么类型
kubectl -n storage-lab exec emptydir-demo -- cat /proc/mounts | grep '/work\|/memory'
kubectl -n storage-lab exec pvc-writer -- cat /proc/mounts | grep /data
看 Pod 重建后数据是否还在
kubectl -n storage-lab delete pod emptydir-demo --wait=true
kubectl -n storage-lab delete pod pvc-writer --wait=true
kubectl apply -f manifests/08-storage/10-emptydir-demo.yaml
kubectl apply -f manifests/08-storage/30-pvc-writer.yaml
看 NFS provisioner 日志
kubectl -n kube-system logs deploy/nfs-provisioner-nfs-subdir-external-provisioner
直接看后端目录
ssh root@154.219.104.66
exportfs -v
ls -lah /srv/nfs/k8s
这节课你必须真正记住的结论
结论 1
emptyDir 不是持久化卷,它只是 Pod 生命周期内的临时卷。
结论 2
emptyDir.medium: Memory 本质是 tmpfs,吃的是内存,不是“更高级的持久化”。
结论 3
PVC 的核心价值,是把数据生命周期从 Pod 生命周期里抽出来。
结论 4
StorageClass -> Provisioner -> PV -> PVC -> Pod 是一条真实可追踪的链,不是抽象图。
结论 5
volumeBindingMode = Immediate 会让 PVC 在没有消费者 Pod 时就先绑定。
结论 6
RWX 的价值在于共享挂载,但不要把 Access Mode 当成应用级锁语义。
结论 7
NFS 很适合共享文件和实验平台,但不应被想当然地当成所有状态型系统的最佳后端。
结论 8
删除 Pod 不等于删除数据;删除 PVC / PV 才会进入真正的卷回收链。
你现在应该能回答的高级问题
学完这一课,你至少应该能独立回答这些问题:
- 为什么把业务数据放在
emptyDir是危险设计? - PVC 为什么能在 Pod 删除后保住数据?
Immediate和WaitForFirstConsumer的区别为什么会影响调度?- 为什么
RWX常常意味着文件共享存储,而不是本地盘? - 为什么很多数据库即使“能跑在 NFS 上”,也未必是好选择?
- 为什么删除 PVC 后,有时后端目录还可能以
archived-*形式存在? - 遇到“存储不见了”时,你应该顺着
Pod -> PVC -> PV -> StorageClass -> Provisioner -> 后端目录这条链一路追下去。
如果这些问题你能讲清楚,那么你对 Kubernetes 存储的理解,就已经不再是“会写一个 PVC YAML”,而是开始具备真正的平台和架构视角。
下一轮最自然的衔接方向是:
- Workload 进阶
- StatefulSet 为什么要和存储一起学
- Headless Service、稳定网络标识、稳定卷、滚动升级
- 为什么数据库和有状态系统通常不只是“挂一个 PVC”这么简单