上一篇【第36篇】K8s存储体系全景——从临时存储到持久化存储下一篇【第38篇】StorageClass——存储的自助餐摘要给你一个形象的比喻K8s集群内部有一座存储仓库。PVPersistentVolume是仓库里的货架——管理员提前搭好了每个货架有固定容量和规格。PVCPersistentVolumeClaim是租户的租赁合同——你只需要写清楚我要10平方、朝南、带空调系统就会自动在仓库里找到最匹配的那个货架租给你。这个供需匹配平台是整个K8s持久化存储的核心机制。本文带你完整拆解(1)什么是静态供应管理员手动建PV开发者手动写PVCvs动态供应StorageClass自动建PV(2)AccessModes三种模式RWO单写、ROX多读、RWX多写的生产场景选择(3)PV从创建到回收的完整生命周期(4)Reclaim Policy的三种回收策略。学完这篇你就掌握了K8s存储的房产交易逻辑。一、PV和PVC到底是什么——仓库 vs 合同1.1 如果不分PV和PVC——自助仓储乱象在第036篇我们讲了Volume可以用nfs、hostPath——但那些都需要在Pod YAML里硬编码存储细节。想象一下如果没有PV/PVC一个简单的Web应用# 如果没有PV/PVC——每个Pod都要写死存储细节apiVersion:v1kind:Podmetadata:name:web-appspec:volumes:-name:datanfs:server:192.168.1.100path:/exports/app-data/web-app-v2containers:-name:webimage:myapp:v2volumeMounts:-name:datamountPath:/data问题来了运维换了NFS服务器→你得改所有Pod的YAML→重新部署你没权限创建NFS目录→等运维→运维今天请假了→你今天没法上线三个团队用同一个NFS→目录名冲突→A把B的数据覆盖了要点PV/PVC的核心理念是职责分离——谁该干什么事情就只让谁干。运维管仓库PV开发者管需求PVCK8s做中介绑定。三方各司其职谁也不绑架谁。1.2 PV——仓库里的标准货架# PV——管理员创建的仓库货架# 这是运维/集群管理员的工作不需要开发者操心apiVersion:v1kind:PersistentVolumemetadata:name:pv-nfs-100g# 给这个PV起个名字labels:# 标签PV可以通过标签匹配PVCstorage:nfsspeed:fastspec:capacity:storage:100Gi# 这个货架有多大volumeMode:Filesystem# 文件系统还是块设备accessModes:-ReadWriteOnce# 只能被一个Pod以读写方式挂载-ReadOnlyMany# 可以被多个Pod以只读方式挂载persistentVolumeReclaimPolicy:Retain# PVC释放后保留数据storageClassName:slow# 归属哪个StorageClassmountOptions:# 挂载选项NFS专用-hard-nfsvers4.1nfs:# 具体存储后端信息server:192.168.1.100path:/exports/pv100g要点PV的关键设计是你只需要关心抽象需求多大、什么访问模式不需要关心具体实现NFS还是EBS、服务器在哪。PV就像一个标准集装箱——不管里面装的是食品、电子设备还是玩具NFS、EBS、Ceph外部规格都一样capacity、accessModes用同样的方式搬运。1.3 PVC——你的租赁合同# PVC——开发者创建的租赁合同# 你只需要说清楚我要多大什么访问方式什么类型的存储apiVersion:v1kind:PersistentVolumeClaimmetadata:name:my-app-datanamespace:productionspec:accessModes:-ReadWriteOnce# 我要可读写的独占模式resources:requests:storage:10Gi# 我要10Gi的空间storageClassName:slow# 我要慢速存储类型够用就行# 不用写NFS服务器地址# 不用写存储路径# 不用知道底层存储是什么# 开发者视角——像点外卖一样简单kubectl apply-fpvc.yaml# 下订单kubectl get pvc# 看订单状态# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS# my-app-data Bound pv-nfs-100g 100Gi RWO,ROX slow# 看自动匹配到了 pv-nfs-100g 这个PV【PV 和 PVC 的关系——仓库-合同模型】 ┌───────────────── 仓库K8s集群存储资源池─────────────────┐ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ PV-1 │ │ PV-2 │ │ PV-3 │ │ │ │ 100Gi NFS │ │ 50Gi SSD │ │ 200Gi 本地 │ │ │ │ RWO │ │ RWO │ │ RWO │ │ │ │ Status: │ │ Status: │ │ Status: │ │ │ │ Available │ │ Bound ← ─ ─│─ ─│─ Available │ │ │ └──────────────┘ └──────┬───────┘ └──────────────┘ │ │ │ │ └───────────────────────────┼───────────────────────────────┘ │ 绑定 ▼ ┌───────────────── 你的租赁合同PVC─────────────────┐ │ │ │ 我要 10Gi 存储RWO 模式SSD 类型 │ │ │ │ Status: Bound │ │ 已匹配到: PV-2 (50Gi SSD) │ │ 实际用的是50Gi的PV但你的Pod只能看到你申请的10Gi │ │ │ └───────────────────────────────────────────────────────┘要点PVC可以申请比PV小的空间——申请10G但匹配了50G的PV也是可以的。这是K8s的向上匹配策略Match UpwardPVC申请的容量只要≤PV的容量就能匹配剩余的40G被浪费了但不能给别的PVC用。如果你想精确匹配就让PV和PVC的容量一致或者用StorageClass动态创建下一篇讲。二、静态供应 vs 动态供应——“等货和定制”2.1 静态供应Static Provisioning——“先有货再下单”管理员先手动创建一堆PV开发者创建PVC后K8s自动匹配合适的PV【静态供应——先建后配】 管理员创建PV 开发者创建PVC ───────────── ───────────── 第1天 第3天 ┌─────────────────┐ ┌─────────────────┐ │ kubectl apply -f│ │ kubectl apply -f│ │ pv-nfs-100g.yaml│ │ pvc-myapp.yaml │ │ pv-nfs-50g.yaml │ │ storage: 10Gi │ │ pv-ssd-200g.yaml│ │ storageClass: │ │ ...建了10个PV │ └────────┬────────┘ └─────────────────┘ │ │ ▼ │ ┌─────────────────────┐ │ │ K8s 自动匹配 │ │ │ PVC:10G ←→ PV:100G │ │ │ 你的合同匹配到了 │ │ │ 仓库里的货架 │ │ └─────────────────────┘ ▼ ┌──────────────────────────────────────────────────┐ │ 等待PVC来匹配 │ │ PV-1 (100G) → 待匹配 Status: Available │ │ PV-2 (50G) → 待匹配 Status: Available │ │ PV-3 (200G) → 待匹配 Status: Available │ │ PV-4 (20G) → 已匹配 Status: Bound (太巧了) │ └──────────────────────────────────────────────────┘# 静态供应——管理员手动创建一堆PV# pv-small.yamlapiVersion:v1kind:PersistentVolumemetadata:name:pv-smallspec:capacity:storage:20GiaccessModes:-ReadWriteOncepersistentVolumeReclaimPolicy:Retainnfs:server:192.168.1.100path:/exports/pv-small---# pv-medium.yamlapiVersion:v1kind:PersistentVolumemetadata:name:pv-mediumspec:capacity:storage:50GiaccessModes:-ReadWriteOncepersistentVolumeReclaimPolicy:Retainnfs:server:192.168.1.100path:/exports/pv-medium---# pv-large.yamlapiVersion:v1kind:PersistentVolumemetadata:name:pv-largespec:capacity:storage:200GiaccessModes:-ReadWriteOncepersistentVolumeReclaimPolicy:Retainnfs:server:192.168.1.100path:/exports/pv-large静态供应的痛点每来一个新应用管理员就得手动创建一个PV。一个集群跑50个微服务管理员得建50个PV。万一有人申请了300G的存储但仓库里最大的PV才200G——啪PVC Pending应用部署失败。2.2 动态供应Dynamic Provisioning——“要啥现做”动态供应是第038篇StorageClass的重点这里先预热一下对比维度静态供应动态供应PV创建管理员手动创建StorageClass自动创建方式kubectl apply -f pv.yamlPVC声明storageClassName即可灵活性固定容量可能浪费按需创建不浪费运维成本高每个应用都要手动操作低一次配置永久使用适用场景存储类型固定、需求单一多样化的存储需求K8s版本1.11.4典型命令kubectl create -f pv-xxx.yaml只需要PVCPV自动出现# 静态供应——你永远不知道下一个PVC会申请多大# 要么建少了不够用要么建多了浪费# 动态供应——就这两行PV自动创建catEOF|kubectl apply-f-apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-app-data spec: storageClassName: fast-ssd # 指定自助餐剩下的全自动 accessModes: - ReadWriteOnce resources: requests: storage: 25Gi # 精确25G不多不少 EOF# 几秒后一个25G的PV自动创建、自动绑定三、AccessModes——一个人用还是多个人用3.1 三种模式的通俗解释这是K8s存储中最容易被混淆的概念。AccessModes不是这个存储允许多少人用而是How many Nodes can mount this volume, and in what way?【AccessModes 三种模式——谁的钥匙能开这把锁】 ReadWriteOnce (RWO) ReadOnlyMany (ROX) ReadWriteMany (RWX) ──────────────── ──────────────── ──────────────── ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Node-1 │ │ Node-1 │ │ Node-1 │ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │ │Pod-A │ │ │ │Pod-A │ │ │ │Pod-A ✏️│ │ │ │读写 ✏️ │ │ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │ ┌───────┐ │ │ │ │ │ │ │Pod-B │ │ │ Node-2 │ │ Node-2 │ │ └───────┘ │ │ ┌───────┐ │ │ ┌───────┐ │ │ │ │ │Pod-B ✏️│ │ │ │Pod-B │ │ │ Node-2 │ │ └───────┘ │ │ │❌不行│ │ │ ┌───────┐ │ │ ┌───────┐ │ │ └───────┘ │ │ │Pod-C │ │ │ │Pod-C ✏️│ │ │ │ │ └───────┘ │ │ └───────┘ │ └─────────────┘ └─────────────┘ └─────────────┘ 单节点单Pod读写 多节点只读 多节点多Pod读写 适用: 数据库 适用: 配置文件分发 适用: 共享文件存储 支持: EBS/本地盘/ 支持: NFS/CephFS 支持: NFS/CephFS 大多数块存储 (需要存储系统支持)3.2 常见存储的AccessModes支持一览存储类型RWOROXRWX说明AWS EBS (gp3)✅❌❌块存储绑定单个EC2实例GCE Persistent Disk✅✅ (只读)❌块存储ROX是新功能Azure Disk✅❌❌块存储绑定单个VMNFS✅✅✅网络文件系统天然支持多客户端CephFS✅✅✅分布式文件系统支持多客户端Ceph RBD✅✅❌块存储只有CSI新版部分支持ROXhostPath✅❌❌单节点本地存储Local PV✅❌❌单节点本地持久化存储要点AccessModes选择的核心依据是你底层的存储类型——不是你想用什么就用什么。EBS是块存储天生就只能单节点挂载你想用RWX就得用NFS或CephFS。选择存储类型时先想清楚你的应用需要什么AccessMode再反推选什么存储方案。3.3 多容器共享 vs 多Pod共享——不是一个概念# 误区1同一Pod内的多个容器共享Volume不需要RWX# 它们已经在同一个Pod里了Volume挂给Pod就行apiVersion:v1kind:Podmetadata:name:shared-volume-podspec:containers:-name:writerimage:busyboxcommand:[sh,-c,echo hello /data/file.txt sleep 3600]volumeMounts:-name:shared-datamountPath:/data-name:readerimage:busyboxcommand:[sh,-c,cat /data/file.txt]volumeMounts:-name:shared-datamountPath:/datavolumes:-name:shared-dataemptyDir:{}# 同Pod容器间共享——不需要PVCRWO就够了# 误区2跨Pod共享才需要RWX# Pod-A在Node-1上Pod-B在Node-2上它们要读写同一个文件 → 必须RWX四、PV的完整生命周期——从出生到退休4.1 生命周期状态图【PV生命周期完整状态机】 kubectl create -f pv.yaml │ ▼ ┌──────────────┐ │ Provisioning │ 存储后端正在创建底层存储资源 │ (配置中) │ 仅动态供应有这一步下一篇StorageClass详讲 └──────┬───────┘ │ 存储创建成功 ▼ ┌──────────────┐ │ Available │ 仓库里有货等人来租 │ (可用) │ 没有PVC绑定到这个PV └──────┬───────┘ │ PVC匹配成功 → K8s执行绑定 ▼ ┌──────────────┐ │ Bound │ 已经租出去了正在使用中 │ (已绑定) │ 被某个PVC独占 │ │ 其他PVC不能绑定这个PV └──────┬───────┘ │ PVC被删除 → 解除绑定 ▼ ┌──────────────┐ │ Released │ 租户搬走了房间需要处理 │ (已释放) │ PVC没了但数据还在PV上 │ │ 这个PV不能直接被新PVC绑定 └──────┬───────┘ │ ┌────────────┼────────────┐ │ Reclaim │ Reclaim │ Reclaim │ Retain │ Delete │ Recycle │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │Retained │ │ 删除数据 │ │ 清空数据后 │ │(保留) │ │ 删PV │ │ 回到Available│ │需手动回收│ │ 啥都没了 │ │ (已弃用) │ └─────────┘ └─────────┘ └─────────────┘4.2 生命周期各阶段的实操# 完整的PV生命周期演示# 阶段1: Available——创建PV等待PVCkubectl apply-fpv-demo.yaml kubectl getpv# NAME CAPACITY ACCESS MODES STATUS CLAIM STORAGECLASS# pv-demo 100Gi RWO Available slow# 阶段2: Bound——PVC匹配后自动绑定kubectl apply-fpvc-demo.yaml kubectl getpv# NAME CAPACITY ACCESS MODES STATUS CLAIM STORAGECLASS# pv-demo 100Gi RWO Bound default/myapp-pvc slowkubectl get pvc# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS# myapp-pvc Bound pv-demo 100Gi RWO slow# 阶段3: Released——PVC被删除后PV进入Released状态kubectl delete pvc myapp-pvc kubectl getpv# NAME CAPACITY ACCESS MODES STATUS CLAIM STORAGECLASS# pv-demo 100Gi RWO Released default/myapp-pvc slow# 注意PV还在但不能再被直接绑定# 阶段4: 根据ReclaimPolicy进入最终状态# Retain: 管理员手动清理# Delete: 自动删除PV和底层存储# Recycle: 清空数据后回到Available已弃用要点Released状态的PV不能被新的PVC直接绑定——这是个经常让人踩坑的地方。PV从Bound变成Released后K8s认为这个PV的前租户可能还留了数据在上面的、还没打扫所以不允许新租户直接搬进去。管理员需要手动处理Retain模式或者等系统自动处理Delete模式。五、Reclaim Policy——“租户退房后怎么办”5.1 三种策略详解# Reclaim Policy——决定PVC释放后PV的命运apiVersion:v1kind:PersistentVolumemetadata:name:pv-retain-demospec:persistentVolumeReclaimPolicy:Retain# ← 关键字段三种取值capacity:storage:100Ginfs:server:192.168.1.100path:/exports/retain-demo策略行为数据PV对象适用场景状态RetainPVC删除后保留一切✅ 保留✅ 保留但Released需要手动审计/恢复数据✅ 推荐DeletePVC删除后自动删除一切❌ 删除❌ 删除数据不重要自动化优先✅ 推荐Recycle清空数据后PV回Available❌ 清空✅ 回Available已弃用不要用⚠️ 弃用5.2 Retain——“租户走了东西别扔”【Retain 策略——小心谨慎手动处理】 PVC删除前 PVC删除后 ┌──────────────┐ ┌──────────────┐ │ PV: Bound │ │ PV: Released │ │ 数据在NFS上 │ │ 数据还在NFS上 │ │ Pod正常使用 │ │ 不能自动重用 │ └──────────────┘ └──────┬───────┘ │ 管理员手动操作 ┌──────┴───────┐ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ 1. 备份数据 │ │ 2. 删除PV │ │ 2. 清理NFS │ │ 3. 重建新PV │ │ 3. 删除旧PV │ │ │ │ 4. 重建可用PV │ │ │ └──────────────┘ └──────────────┘# Retain策略——PV Released后管理员手动处理kubectl getpvpv-retain-demo# NAME STATUS CLAIM STORAGECLASS# pv-retain-demo Released default/myapp-pvc slow# Step 1: 检查数据去NFS服务器上看 /exports/retain-demo 目录# Step 2: 确认数据不再需要删除PVkubectl deletepvpv-retain-demo# Step 3: 清理NFS上的数据登录NFS服务器sshnfs-serverrm -rf /exports/retain-demo/*# Step 4: 重建PV手动apply或者等StorageClass自动创建5.3 Delete——“一键退房打包带走”# Delete策略——最常见的选择配合StorageClassapiVersion:v1kind:PersistentVolumemetadata:name:pv-delete-demospec:persistentVolumeReclaimPolicy:Deletecapacity:storage:100GiawsElasticBlockStore:# EBS卷volumeID:vol-0d8f3a2b...fsType:ext4# Delete策略——PVC被删除后PV和底层EBS卷都被自动删除kubectl delete pvc myapp-pvc# 几秒后……# PV pv-delete-demo 被删除# AWS EBS卷 vol-0d8f3a2b... 被删除# 数据 永远没了# 生产环境建议# 1. 给重要的PVC加注解防止误删# 2. 开启EBS快照/Ceph快照做备份# 3. 用K8s VolumeSnapshot做定期备份要点Delete策略配合StorageClass是生产环境最常用的组合——简洁、自动、不操心。但如果你的数据值钱数据库、订单数据一定要配快照VolumeSnapshot。Delete只保护你不被残留数据占用空间困扰不保护你不被删库跑路困扰——那是备份和快照的事。六、PV和PVC的匹配规则——相亲背后的算法6.1 匹配条件K8s的PVC和PV匹配不是随便找的有一套严格的规则【PV/PVC 匹配流程——相亲算法】 PVC 需求单 PV 资源池 ────────── ──────── 我要找一个对象 PV-1: 100Gi NFS RWO slow 1. 容量 ≥ 10Gi PV-2: 50Gi SSD RWO fast 2. AccessModes RWO PV-3: 10Gi NFS RWX slow 3. StorageClass slow PV-4: 20Gi NFS RWO slow 4. 最好刚好20Gi PV-5: 5Gi NFS RWO slow PV-6: 20Gi NFS RWO slow (已绑定) PV-7: 100Gi 本地盘 RWO 匹配过程 ───────── Step 1: 过滤 AccessModes 不匹配的 PV-3 (RWX) ❌ 删掉 Step 2: 过滤 StorageClass 不匹配的 PV-2 (fast) ❌ 删掉 PV-7 () ❌ 删掉空字符串也算一种SC Step 3: 过滤 已绑定的 PV-6 (Bound) ❌ 删掉 Step 4: 过滤 容量不够的 PV-5 (5Gi10Gi) ❌ 删掉 候选池: PV-1 (100Gi), PV-4 (20Gi) Step 5: 选择最匹配的容量最小的那个 PV-4 (20Gi) ✅ 最接近10Gi不浪费6.2 storageClassName的匹配逻辑PVC的storageClassNamePV的storageClassName匹配说明slowslow✅精确匹配空空✅两个都没有SC默认匹配空slow❌PVC没指定SC不会匹配有SC的PVslow空❌PVC指定了SC不会匹配没SC的PV不写storageClassName字段slow⚠️PVC会用默认SC如果设置了default StorageClass# 典型错误PVC没指定storageClassName但PV有——匹配不上# ❌ 错误写法apiVersion:v1kind:PersistentVolumeClaimmetadata:name:my-pvcspec:accessModes:-ReadWriteOnceresources:requests:storage:10Gi# 没写storageClassName → 默认为空字符串 ---# PV有storageClassName → 不匹配apiVersion:v1kind:PersistentVolumemetadata:name:my-pvspec:storageClassName:slow# ← PVC没写这个导致匹配失败capacity:storage:100Ginfs:server:192.168.1.100path:/exports/data要点storageClassName是PV/PVC匹配的最常见坑。如果你的PVC一直Pending第一件事就是检查kubectl describe pvc看错误信息里是不是有no persistent volumes available for this claim——十有八九是storageClassName没对上。七、PV/PVC的日常操作命令# 查看所有PV和PVC状态kubectl getpvkubectl get pvc --all-namespaces# 查看PVC为啥一直Pending最常用的排查命令kubectl describe pvc my-pvc# 关键看Events# - waiting for a volume to be created → 动态供应等StorageClass创建# - no persistent volumes available → 没有匹配的PV# 查看PV的详细信息kubectl describepvpv-nfs-100g# 关键信息# - Status: Available/Bound/Released# - Claim: 绑定到哪个PVCBound状态才有# - Reclaim Policy: Retain/Delete/Recycle# 手动绑定PVC到特定PV不推荐但偶尔需要# 方法在PVC里加volumeName字段apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-pvc spec: volumeName: pv-nfs-100g# 强制绑定到这个PVaccessModes: - ReadWriteOnce resources: requests: storage: 10Gi# 扩容PVC需要StorageClass支持allowVolumeExpansion: truekubectl patch pvc my-pvc-p{spec:{resources:{requests:{storage:20Gi}}}}# 查看哪些Pod在使用这个PVCkubectl get pods --all-namespaces-ojson|jq.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimNamemy-pvc) | .metadata.name# 强制删除卡在Terminating的PVCkubectl patch pvc my-pvc-p{metadata:{finalizers:null}}kubectl delete pvc my-pvc--force--grace-period0本篇小结PV和PVC这套供需匹配机制是K8s存储体系的基石。记住三个核心概念PV是管理员建的仓库货架供给方、PVC是开发者下的租赁合同需求方、K8s是自动匹配的中介绑定过程。实际生产中你需要搞清楚的几个重点(1)静态供应满足小众需求、动态供应StorageClass才是主流——下一篇深入(2)AccessModes选型先看底层存储支持什么不是你想用什么就用什么(3)PV的生命周期让你知道什么时候PV处于什么状态排查问题就靠这个(4)Reclaim Policy推荐用Delete快照备份的组合生产环境有备无患。下一篇咱们聊StorageClass——有了它你再也不用手动创建PV了。写一个PVCPV自动创建、自动绑定、自动挂载就像自助餐——你只需要说我要这个、我要那个剩下的全自动。上一篇【第36篇】K8s存储体系全景——从临时存储到持久化存储下一篇【第38篇】StorageClass——存储的自助餐