HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 概览

    • K8s 实战学习实验室
    • 服务访问清单
    • K8s-Lab 学习总纲、仓库评估与专家路线图
  • 课程正文

    • 环境验证与第一课:认识你的真实集群
    • 第二课:kubectl apply 之后,到底发生了什么
    • 第三课:调度器如何选节点,为什么 Pod 会 Pending
    • 第四课:Kubernetes 网络、协议分层、VXLAN/IPIP/WireGuard 原理与排障
    • 第五课:NetworkPolicy、零信任网络与流量边界
    • 第六课:身份、认证、授权、准入与 ServiceAccount / RBAC 原理
    • 第七课:ConfigMap 与 Secret 注入模型、更新机制与安全边界
    • 第八课:存储持久化、PV / PVC / StorageClass 与 NFS 原理
    • 第九课:StatefulSet、Headless Service、稳定身份与存储原理
    • 第十课:探针、滚动更新、优雅终止与 PDB 原理
    • 第十一课:requests / limits、QoS、OOM 与驱逐原理
    • 第十二课:HPA、自动扩缩容、指标链路与副本伸缩原理
    • 第十三课:Service、EndpointSlice、kube-proxy、CoreDNS 与服务发现原理
    • 第十四课:Ingress-nginx、反向代理、Host / Path、NodePort 与北南向流量原理
    • 第十五课:HTTPS、TLS、SNI、证书信任与 Ingress 终止原理
    • 第十六课:cert-manager、Ingress 自动签发、证书生命周期与 ACME 工作流原理
    • 第十七课:ACME、Let's Encrypt、HTTP-01 / DNS-01、Orders / Challenges 与生产限制原理
    • 第十八课:大模型全生态,从数据到训练到部署到治理原理
    • 第十九课:大模型数据集、清洗、标注、切分、版本管理与质量治理原理
    • 第二十课:大模型训练、SFT、LoRA、Checkpoint、Adapter 与模型产物原理
    • 第二十一课:大模型推理、量化、KV Cache、vLLM、吞吐/延迟与部署发布链路原理
  • 实验操作记录

    • 本次仓库审查操作记录与命令原理
    • 本轮操作记录:环境验证、集群基线盘点与故障样本采集
    • 本轮操作记录:kubectl apply 主链路实验
    • 本轮操作记录:调度实验与 Pending 排查
    • 本轮操作记录:Kubernetes 网络原理、协议对比与调试实验
    • 本轮操作记录:NetworkPolicy 与零信任网络实验
    • 本轮操作记录:身份、认证、授权、准入实验
    • 本轮操作记录:ConfigMap 与 Secret 注入、更新与安全边界实验
    • 本轮操作记录:存储持久化、PV / PVC / StorageClass 与 NFS 实验
    • 本轮操作记录:StatefulSet、Headless Service 与稳定身份实验
    • 本轮操作记录:探针、滚动更新、优雅终止与 PDB 实验
    • 本轮操作记录:资源模型、QoS、OOM 与 CPU 节流实验
    • 本轮操作记录:HPA 自动扩缩容实验
    • 本轮操作记录:Service、EndpointSlice、CoreDNS 与服务发现排障实验
    • 本轮操作记录:Ingress-nginx、NodePort 与北南向流量实验
    • 本轮操作记录:HTTPS、TLS、自签证书与 Ingress 实验
    • 本轮操作记录:cert-manager 安装、CA 签发与 Ingress 自动证书实验
    • 本轮操作记录:ACME staging、HTTP-01 失败样本与排障实验
    • 本轮操作记录:大模型全生态与基础原理科普文撰写
    • 本轮操作记录:大模型数据集样本与治理文档编写
    • 本轮操作记录:大模型训练与模型产物概念文撰写
    • 本轮操作记录:大模型推理与服务发布概念文撰写

本轮操作记录:存储持久化、PV / PVC / StorageClass 与 NFS 实验

本轮目标

这一轮的目标不是只做一个 PVC 示例。

我要把下面几件事做成可观察事实:

  1. emptyDir 为什么只是 Pod 级临时卷
  2. PVC 为什么能把数据从 Pod 生命周期里独立出来
  3. 动态供给到底是谁在干活
  4. NFS 共享卷为什么能跨节点共享
  5. 存储删除和回收在这套集群里到底表现成什么样

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-provisioner
  • reclaimPolicy = Delete
  • volumeBindingMode = Immediate
  • allowVolumeExpansion = true
  • archiveOnDelete = 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.5
  • NFS_PATH = /srv/nfs/k8s

NFS 服务器上真实导出的是:

  • /srv/nfs/k8s

导出参数里有:

  • rw
  • sync
  • no_subtree_check
  • no_root_squash

原理解释

这说明:

  • 集群里的 PVC 最终都会落到一台真实服务器和真实目录上
  • 动态供给不是魔法,它依赖一个常驻 controller 和一个真实 NFS export

Step 4:设计本轮实验对象

我创建了哪些文件

路径:

  • manifests/08-storage

对象包括:

  • Namespace/storage-lab
  • Pod/emptydir-demo
  • PVC/shared-data
  • Pod/pvc-writer
  • Pod/pvc-reader

为什么这样设计

这轮实验需要同时回答两类问题:

  1. 临时卷和持久卷的生命周期区别
  2. 共享存储的跨节点语义

所以我做了两条链:

链一: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-lab
  • emptydir-demo
  • shared-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-writer
  • pvc-reader

shared-data 已经:

  • Bound

而 describe pvc 事件里还出现了完整链路:

  • ExternalProvisioning
  • Provisioning
  • ProvisioningSucceeded

原理解释

这一步直接证明了:

  • 这套 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.5
  • nfs.path: /srv/nfs/k8s/storage-lab-shared-data-pvc-b14e5bac-dd83-48be-8191-f89acccd2a20
  • accessModes: [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 -> us590068728056
  • pvc-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

随后几秒再次读取时,两份文件都稳定可见。

我如何处理这个现象

我没有急着下结论,而是继续做了三步验证:

  1. 再次在 reader 中循环读取
  2. 在 writer 中再次确认文件都在
  3. 直接到 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.txt
  • source.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.txt
  • source.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": started
  • volume "pvc-b14e5bac-dd83-48be-8191-f89acccd2a20" provisioned
  • ProvisioningSucceeded

NFS 目录里还看到了大量:

  • archived-gitea-*

原理解释

这两条证据组合起来,就把你这套集群的回收和归档模式讲清了:

  • PVC / PV 删除链路确实会走 provisioner
  • 后端目录会按 archiveOnDelete 的逻辑保留归档痕迹

本轮最重要的结论

这轮实验最核心的,不是“创建了一个 PVC”,而是把以下规律做成了事实:

  1. emptyDir 数据跟 Pod 生命周期绑定,Pod 重建后数据消失。
  2. emptyDir.medium: Memory 在容器里表现为 tmpfs,吃的是内存。
  3. PVC 一旦绑定,数据生命周期可以独立于 Pod 生命周期。
  4. 你的默认存储类 nfs-dynamic 采用 Immediate 绑定模式,所以 PVC 会先绑定。
  5. NFS provisioner 会把 PVC 动态映射为 NFS 服务器上的真实子目录。
  6. RWX 可以让不同节点上的 Pod 共享同一卷。
  7. Access Mode 是 K8s 存储语义契约,不应被误当成应用级锁。
  8. 这套集群的存储删除链还有 archiveOnDelete,所以历史目录会以 archived-* 形式保留。

本轮新增交付物

  • 08-第八课-存储持久化PV-PVC-StorageClass与NFS原理.md
  • 08-操作记录-存储持久化与NFS实验.md
  • manifests/08-storage

下一轮最自然的衔接是:

  • StatefulSet
  • Headless Service
  • 稳定网络身份
  • 稳定卷
  • 有状态系统为什么不只是“挂一个 PVC”
Prev
本轮操作记录:ConfigMap 与 Secret 注入、更新与安全边界实验
Next
本轮操作记录:StatefulSet、Headless Service 与稳定身份实验