Repository Reading Site
本轮操作记录:存储持久化、PV / PVC / StorageClass 与 NFS 实验
这一轮的目标不是只做一个 PVC 示例。 我要把下面几件事做成可观察事实: 1. `emptyDir` 为什么只是 Pod 级临时卷 2. PVC 为什么能把数据从 Pod 生命周期里独立出来 3. 动态供给到底是谁在干活 4. NFS 共享卷为什么能跨节点共享 5. 存储删除和回收在这套集群里到底表现成什么样 --- 因为仓库里已经有: 的基础说明。 我这
本轮操作记录:存储持久化、PV / PVC / StorageClass 与 NFS 实验
本轮目标
这一轮的目标不是只做一个 PVC 示例。
我要把下面几件事做成可观察事实:
emptyDir为什么只是 Pod 级临时卷- PVC 为什么能把数据从 Pod 生命周期里独立出来
- 动态供给到底是谁在干活
- NFS 共享卷为什么能跨节点共享
- 存储删除和回收在这套集群里到底表现成什么样
Step 1:先读仓库里的已有存储资料
实际命令
sed -n '1,280p' phase-2/02-storage-nfs.md
为什么先读已有资料
因为仓库里已经有:
- PV / PVC / StorageClass
- NFS 动态供给
的基础说明。
我这轮不是从零重复,而是要升级成:
- 真实集群取证
- 真实动态供给链路
- 真实 Pod 生命周期对比
我得到的结论
现有文档能帮助入门,但还不够解释:
Immediate绑定时机archiveOnDelete- Pod 重建与数据保留
emptyDir.medium: Memory- 真实 NFS 后端路径与控制器日志
所以我决定把这些都补成实验课。
Step 2:先看集群当前存储全景
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl get storageclass,pv,pvc -A | sed -n '1,260p'
KUBECONFIG=~/.kube/config-k8s-lab kubectl get sc nfs-dynamic -o yaml | sed -n '1,220p'
KUBECONFIG=~/.kube/config-k8s-lab kubectl get pvc -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" "}{.spec.accessModes[*]}{" "}{.spec.resources.requests.storage}{" "}{.spec.storageClassName}{"\n"}{end}' | sed -n '1,200p'
为什么先看全局
因为我不想把 lesson 建在“假设存储栈存在”上。
我需要先确认:
- 默认 StorageClass 是什么
- 集群里真实用的是哪种后端
- 现有 PVC 大多是 RWO 还是 RWX
我看到的关键结果
当前默认 StorageClass 是:
nfs-dynamic
关键字段包括:
provisioner = cluster.local/nfs-provisioner-nfs-subdir-external-provisionerreclaimPolicy = DeletevolumeBindingMode = ImmediateallowVolumeExpansion = truearchiveOnDelete = true
已有 PVC 中:
- 很多数据库/组件请求的是
RWO ml-platform/ml-models、aiforge/aiforge-models这类共享目录请求的是RWX
原理解释
这一步先把真实场景建立起来:
- 这不是空集群
- 不是没有状态型系统
- 你的平台已经真实依赖这套 NFS 动态供给机制
Step 3:检查 NFS provisioner 和后端服务器
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n kube-system get deploy,pod | rg 'nfs|provisioner'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get deploy nfs-provisioner-nfs-subdir-external-provisioner -o yaml | \
sed -n '1,260p'
ssh root@154.219.104.66 \
'hostname && sudo exportfs -v && sudo ls -lah /srv/nfs/k8s | sed -n "1,120p"'
为什么必须走到这一步
因为很多人学存储时只停在:
kubectl get pvc
但如果你想成为能排障的人,就必须知道:
- 是哪个 provisioner 在干活
- 它挂的是哪台 NFS 服务器
- 后端真实目录长什么样
我看到的结果
Provisioner Deployment 明确写着:
NFS_SERVER = 10.10.0.5NFS_PATH = /srv/nfs/k8s
NFS 服务器上真实导出的是:
/srv/nfs/k8s
导出参数里有:
rwsyncno_subtree_checkno_root_squash
原理解释
这说明:
- 集群里的 PVC 最终都会落到一台真实服务器和真实目录上
- 动态供给不是魔法,它依赖一个常驻 controller 和一个真实 NFS export
Step 4:设计本轮实验对象
我创建了哪些文件
路径:
对象包括:
Namespace/storage-labPod/emptydir-demoPVC/shared-dataPod/pvc-writerPod/pvc-reader
为什么这样设计
这轮实验需要同时回答两类问题:
- 临时卷和持久卷的生命周期区别
- 共享存储的跨节点语义
所以我做了两条链:
链一:emptyDir
用来证明:
- Pod 删了,数据就没了
链二:RWX PVC + NFS
用来证明:
- 两个不同节点上的 Pod 可以同时看到同一份数据
- 删 Pod 不删 PVC,数据还在
Step 5:先只创建 namespace、emptyDir Pod 和 PVC
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply \
-f manifests/08-storage/00-namespace-storage-lab.yaml \
-f manifests/08-storage/10-emptydir-demo.yaml \
-f manifests/08-storage/20-shared-data-pvc.yaml
为什么先不创建 writer / reader
因为我想先单独观察:
- PVC 在没有消费者 Pod 时会不会先绑定
这就是验证 Immediate 的最好办法。
实际结果
成功创建:
storage-labemptydir-demoshared-data
Step 6:验证 PVC 在无消费者时就先 Bound
实际命令
kubectl -n storage-lab wait --for=condition=Ready pod/emptydir-demo --timeout=180s
kubectl -n storage-lab wait --for=jsonpath='{.status.phase}'=Bound pvc/shared-data --timeout=180s
kubectl -n storage-lab get pvc shared-data -o wide
kubectl -n storage-lab describe pvc shared-data
我看到的结果
即使这时还没有:
pvc-writerpvc-reader
shared-data 已经:
Bound
而 describe pvc 事件里还出现了完整链路:
ExternalProvisioningProvisioningProvisioningSucceeded
原理解释
这一步直接证明了:
- 这套 StorageClass 是
Immediate
PVC 不是“等 Pod 真来用时才绑定”,而是创建后立刻触发供给。
Step 7:把 PVC 对应到真实 PV
实际命令
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get pv pvc-b14e5bac-dd83-48be-8191-f89acccd2a20 -o yaml | sed -n '1,220p'
为什么要看 PV YAML
因为 PVC 只是“需求”,PV 才是“真正供给出来的卷”。
我看到的关键信息
PV 里明确有:
nfs.server: 10.10.0.5nfs.path: /srv/nfs/k8s/storage-lab-shared-data-pvc-b14e5bac-dd83-48be-8191-f89acccd2a20accessModes: [ReadWriteMany]
原理解释
这一步把“抽象申请”变成了“真实目录和真实协议”。
Step 8:把 writer / reader Pod 创建出来,并固定到不同节点
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply \
-f manifests/08-storage/30-pvc-writer.yaml \
-f manifests/08-storage/31-pvc-reader.yaml
kubectl -n storage-lab wait --for=condition=Ready pod/pvc-writer --timeout=180s
kubectl -n storage-lab wait --for=condition=Ready pod/pvc-reader --timeout=180s
kubectl -n storage-lab get pod -o wide
为什么固定到不同节点
因为我要验证的是:
- 跨节点共享
而不是:
- 同一节点本地共享
所以我在清单里明确写了:
pvc-writer -> us590068728056pvc-reader -> wk-1
我确认到的结果
两个 Pod 都成功启动在不同节点。
Step 9:writer 写数据,reader 读同一份数据
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n storage-lab exec pvc-writer -- \
sh -c 'echo writer-host=$(hostname); date > /data/created-at.txt; echo writer-node=us590068728056 >> /data/created-at.txt; echo storage-lab-demo > /data/source.txt; ls -l /data; echo; cat /data/created-at.txt'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n storage-lab exec pvc-reader -- \
sh -c 'echo reader-host=$(hostname); ls -l /data; echo; cat /data/created-at.txt; echo; cat /data/source.txt'
我第一次看到的一个小现象
第一次 reader 读取时:
- 先看到了
created-at.txt - 还没看到
source.txt
随后几秒再次读取时,两份文件都稳定可见。
我如何处理这个现象
我没有急着下结论,而是继续做了三步验证:
- 再次在
reader中循环读取 - 在
writer中再次确认文件都在 - 直接到 NFS 服务器目录里看
最终确认到的结果
reader能稳定看到两份文件writer也能看到两份文件- NFS 服务器目录里也确实存在两份文件
原理解释
这说明共享链路是通的。
至于第一次读时那一点点差异,我在课程文档里明确标成:
- 基于观测的推断
很可能和:
- NFS 客户端缓存
- 元数据可见性时机
有关。
这就是实战态度:
- 看到异常现象,不武断
- 继续收证据
- 再做结论
Step 10:看 Pod 里真实挂载类型
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n storage-lab exec emptydir-demo -- \
sh -c 'echo "[mounts]"; cat /proc/mounts | grep "/work\|/memory"; echo; echo first-write > /work/data.txt; echo hot-cache > /memory/cache.txt; ls -l /work /memory; echo; cat /work/data.txt; echo; cat /memory/cache.txt'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n storage-lab exec pvc-writer -- \
sh -c 'cat /proc/mounts | grep "/data"'
我看到的结果
emptydir-demo 中:
/work是ext4/memory是tmpfs
pvc-writer 中:
/data是nfs4- 服务端是
10.10.0.5 - 路径就是那条
storage-lab-shared-data-pvc-...
原理解释
这一步非常有价值,因为它把三种存储介质在容器里的实际表现彻底分开了:
- 本地磁盘型临时卷
- 内存型临时卷
- 网络文件系统持久卷
Step 11:删除 emptyDir Pod,验证数据丢失
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n storage-lab delete pod emptydir-demo --wait=true
然后我做了什么
重新 apply:
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/08-storage/10-emptydir-demo.yaml
kubectl -n storage-lab wait --for=condition=Ready pod/emptydir-demo --timeout=180s
再进入容器里检查:
kubectl -n storage-lab exec emptydir-demo -- \
sh -c 'echo "[work]"; ls -l /work; echo; echo "[memory]"; ls -l /memory; echo; [ -f /work/data.txt ] && cat /work/data.txt || echo "/work/data.txt missing"; [ -f /memory/cache.txt ] && cat /memory/cache.txt || echo "/memory/cache.txt missing"'
我看到的结果
重建后的新 Pod 里:
/work空了/memory空了- 两个文件都没了
原理解释
这一步用真实现象证明了:
emptyDir的数据跟 Pod 走- Pod 删了,临时卷也就跟着没了
Step 12:删除使用 PVC 的 Pod,验证数据还在
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n storage-lab delete pod pvc-writer --wait=true
我先踩到的一个坑
第一次我在删除还没完全完成时,就立刻重新 apply。
结果 Kubernetes 提示:
- resource is currently being deleted
这个坑为什么值得记录
因为它说明:
对象生命周期是异步的。
你在自动化脚本里如果不等删除真正完成,就很容易拿到“看起来重建了、其实没重建干净”的混乱状态。
修正后的动作
我先确认:
- Pod 真正不存在了
然后再重新 apply:
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/08-storage/30-pvc-writer.yaml
kubectl -n storage-lab wait --for=condition=Ready pod/pvc-writer --timeout=180s
再检查:
kubectl -n storage-lab exec pvc-writer -- sh -c 'ls -l /data; echo; cat /data/created-at.txt; echo; cat /data/source.txt'
kubectl -n storage-lab exec pvc-reader -- sh -c 'ls -l /data; echo; cat /data/created-at.txt; echo; cat /data/source.txt'
我看到的结果
新 pvc-writer 和一直没删的 pvc-reader 都还能看到:
created-at.txtsource.txt
原理解释
这一步把“Pod 生命周期”和“数据生命周期”真正拆开了。
Step 13:直接到 NFS 服务器上确认后端目录
实际命令
ssh root@154.219.104.66 \
'sudo ls -l /srv/nfs/k8s/storage-lab-shared-data-pvc-b14e5bac-dd83-48be-8191-f89acccd2a20 && echo && sudo sed -n "1,20p" /srv/nfs/k8s/storage-lab-shared-data-pvc-b14e5bac-dd83-48be-8191-f89acccd2a20/created-at.txt && echo && sudo sed -n "1,20p" /srv/nfs/k8s/storage-lab-shared-data-pvc-b14e5bac-dd83-48be-8191-f89acccd2a20/source.txt'
为什么必须做这一步
因为我要把这一课从“kubectl 世界”拉回“真实存储世界”。
我看到的结果
底层目录里真实有:
created-at.txtsource.txt
原理解释
这一步说明 PVC 背后的数据绝不是抽象概念,它就真实躺在 NFS 服务器的某个目录里。
Step 14:看 provisioner 日志与 archive 目录
实际命令
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system logs deploy/nfs-provisioner-nfs-subdir-external-provisioner | \
rg 'storage-lab/shared-data|pvc-b14e5bac-dd83-48be-8191-f89acccd2a20'
ssh root@154.219.104.66 \
'sudo ls -d /srv/nfs/k8s/archived-* | sed -n "1,20p"'
我看到的结果
日志里明确有:
provision "storage-lab/shared-data" class "nfs-dynamic": startedvolume "pvc-b14e5bac-dd83-48be-8191-f89acccd2a20" provisionedProvisioningSucceeded
NFS 目录里还看到了大量:
archived-gitea-*
原理解释
这两条证据组合起来,就把你这套集群的回收和归档模式讲清了:
- PVC / PV 删除链路确实会走 provisioner
- 后端目录会按
archiveOnDelete的逻辑保留归档痕迹
本轮最重要的结论
这轮实验最核心的,不是“创建了一个 PVC”,而是把以下规律做成了事实:
emptyDir数据跟 Pod 生命周期绑定,Pod 重建后数据消失。emptyDir.medium: Memory在容器里表现为tmpfs,吃的是内存。- PVC 一旦绑定,数据生命周期可以独立于 Pod 生命周期。
- 你的默认存储类
nfs-dynamic采用Immediate绑定模式,所以 PVC 会先绑定。 - NFS provisioner 会把 PVC 动态映射为 NFS 服务器上的真实子目录。
RWX可以让不同节点上的 Pod 共享同一卷。- Access Mode 是 K8s 存储语义契约,不应被误当成应用级锁。
- 这套集群的存储删除链还有
archiveOnDelete,所以历史目录会以archived-*形式保留。
本轮新增交付物
下一轮最自然的衔接是:
- StatefulSet
- Headless Service
- 稳定网络身份
- 稳定卷
- 有状态系统为什么不只是“挂一个 PVC”