【11-kubenetes的持久化存储】
一、核心理念
我们把kubenetes集群想象成一个出租公寓楼:
Pod --> 租客(人)
Node --> 公寓楼(物理建筑)
容器内的数据 --> 租客脑子中的记忆
Volume (存储卷) --> 租客能用的"写字的地方"
核心问题:Pod 是临时工,随时可能被杀死重建(就像租客随时可能搬走)。数据只存在Pod内,Pod一删,数据就没了。所以我们需要各种"外部笔记本"来保存数据。
二、存储类型对比
2.1 EmptyDir ---一次性的便签纸
本质:Pod里的临时共享空间
想想同一个房间内住着两人(nginx,redis),他们之间传递纸条:
Pod(一个房间) ├── nginx 容器(人A)→ 往 /opt 写纸条 ├── redis 容器(人B)→ 从 /mnt 看纸条 └── emptyDir(桌上的一叠纸)← 两个人共享apiVersion: v1 kind: Pod metadata: name: disk-emptydir-demo spec: # ──────────── 两个容器共享一个 emptyDir containers: # 容器A:nginx,往共享卷写文件 - name: writer image: nginx:1.25 volumeMounts: - name: shared-data # 引用 volumes 中定义的卷名 mountPath: /opt/data # 容器内的挂载路径 # readOnly: false # 默认就是 false,可读写 # 容器B:redis,从共享卷读文件 - name: reader image: redis:7 volumeMounts: - name: shared-data # 同一个卷名 → 同一份存储 mountPath: /mnt/data # 容器B 用自己的路径挂载 # ──────────── 卷定义 ──────────── volumes: - name: shared-data emptyDir: medium: "" # 空字符串 = 使用磁盘(默认行为) sizeLimit: "100Mi" # 限制最大 100MiB为什么 mountPath 不同,读到的数据却一样?
name 决定"接的是哪间仓库",mountPath 决定"在你家叫什么门牌号"。门牌号不同,进的是同一间仓库。 关键是 name 匹配: 容器A 说:我要挂载 name: note-paper 的卷 → 找到 emptyDir 容器B 说:我要挂载 name: note-paper 的卷 → 找到同一个 emptyDir name 相同 = 同一份存储 mountPath 不同 = 从各自内部看到的路径不同 在 Kubernetes 中,emptyDir 卷是由 **kubelet(节点级别的组件)** 来维护的,而非控制平面(API Server、Controller Manager 等)卷定义关键参数:
volumes: - name: <string> # 必填:卷的名称,供容器引用 emptyDir: medium: <string> # 可选:"Memory" 或 ""(默认空=磁盘) sizeLimit: <quantity> # 可选:最大容量限制 #mainC使用 volumeMounts: - name: shared mountPath: /data #挂载点 readOnly: true #默认可读性,trune 只读medium:“” --> 数据存磁盘,便宜,慢,不限内存额度。
medium:“Memory” -->数据存内存,贵,快,占内存额度。
sizeLimit: “X” --> 磁盘型是软限制(驱逐该 Pod 自身;如果导致节点磁盘压力,才可能触发节点级驱逐(按 QoS 排序驱逐,非全部),内存型是硬限制(报错);不设置定时炸弹,一个进程能瘫痪整个节点。
内存额度计算:
容器Memory limit = 256Mi emptyDir sizeLimit =64MI (从Memory limit里扣) 实际MainC 可用 约等于 256 - 64 = 192Mi **写的量会"占用"容器的内存额度!**2.2 hostPath --- “借用房东的桌子”
本质:直接用节点(服务器)上的某个目录,数据跟着节点走,Pod被调度到别的节点就看不到原来的数据。
Node01(一栋楼) ├── /data 目录(房东的桌子) │ └── file1.txt │ ├── Pod A(租客)→ 挂载 /data → 看到 file1.txt ✓ └── Pod B(租客)→ 挂载 /data → 看到 file1.txt ✓ Node02(另一栋楼) └── /data 目录(另一张桌子) └── (空的) Pod C(租客)→ 挂载 /data → 什么都没有 ✗volumes: - name: data hostPath: path: /data # 用节点上的 /data 目录 type: DirectoryOrCreate # 如果 /data 不存在,自动创建 #type: # DirectoryOrCreate ,目录没有就创建一个。 # Dirrectory ,没有就报错 # FileOrCreate,没有创建空文件 # File ,没有就报错生产环境中不建议使用hostPath,因为Pod被调度到其他的节点就找不到数据了,除非是Daemonset日志采集这类场景。
2.3 NFS --- “公共图书馆的书架”
本质:所有人共享的网络存储。
NFS 服务器(图书馆,IP: 192.168.62.15) └── /data/nfs(公共书架) Node01 上的 Pod A → 写入 hello.txt Node02 上的 Pod B → 读到 hello.txt ✓✓✓ 这就是"跨节点共享"!NFS搭建:
# 1. 装工具 yum install nfs-utils rpcbind -y # 2. 建共享目录 + 配置谁能访问 mkdir -p /data/nfs echo "/data/nfs 192.168.62.0/24(rw,sync,no_root_squash)" >> /etc/exports #exports 参数: 192.168.62.0/24 --> 谁可以范围,可网段/ip/域名 rw --> 可读可写 sync --> 写完立刻同步 no_subtree_check -->不检查子目录(性能优化) no_root_squash --> 客户端的root到服务端还是root(权限问题) insecure --> 允许1024以上端口连接 # 3. 生效 + 启动 exportfs -rav systemctl enable --now nfs-server rpcbind # 4. 节点客户端 yum install nfs-utils -y showmount -e 192.168.62.15 # 5. 放行防火墙,selinux.Pod 挂载使用
apiVersion: v1 kind: Pod metadata: name: nfs-basic spec: nodeName: node01 # 指定跑在哪个节点(方便测试) containers: - name: app image: busybox command: - sh - -c - | echo "=== 我在 /data 里看到的文件 ===" ls -la /data/ echo "" echo "=== 写入一个新文件 ===" echo "hello from pod on $(hostname) at $(date)" > /data/from-pod.txt echo "写入完成" volumeMounts: - name: nfs-vol mountPath: /data # Pod 里的挂载点 volumes: - name: nfs-vol nfs: server: 192.168.62.15 # NFS 服务器 IP path: /data/nfs # NFS 服务器上的共享目录(必须已存在!) readOnly: false # 可读可写,true只能读三、PV 和 PVC
核心矛盾点:Pod 被删除重建后,它之前写的数据就没了。需要一种机制,把存储从Pod中解耦出来,变成独立管理的资源。
本质:类图书借阅系统,职责分离。
管理员(运维) → 买书,放在书架上,登记入库 = 创建 PV 读者(开发) → 填借书单,说"我要一本数据结构" = 创建 PVC 图书管理系统 → 自动把借书单和书匹配起来 = K8s 的绑定机制 书架 → 实际的存储(NFS、云盘等)3.1 PV (PersistentVolume) ---书架上的一本书
PV 是管理员提前准备好的"存储资源",入库操作:
apiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs # 这本书叫 pv-nfs spec: capacity: storage: 5Gi # 这本书有 5GB accessModes: - ReadWriteMany # 多人可以同时翻阅(RWX) nfs: server: 192.168.62.15 path: /data/nfs/pvnfs storageClassName: nfs-class # 它属于"nfs-class"这个书架 persistentVolumeReclaimPolicy: Retain # 还书后不销毁(Retain)关键理解:PV是集群级别资源,不属于任何namespace.
3.2 PVC(PersistentVolume) ---借书单
PVC 是用户说"我需要多大的存储":
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-nfs # 借书单叫 pvc-nfs spec: storageClassName: nfs-class # 我要从"nfs-class"书架找 accessModes: - ReadWriteMany # 我需要多人同时读写 resources: requests: storage: 3Gi # 我要借3GBⓂ️ accessModes :
不同存储支持不同模式
本地磁盘 hostPath ├── 磁盘插在一台机器上 ├── 其他机器物理上就访问不到 └── 所以只支持 RWO ✅ AWS EBS / 阿里云云盘 ├── 一块云盘同时只能挂载到一台机器 ├── (就像移动硬盘,一次只能插一台电脑) └── 支持 RWO ✅ 和 RWOP ✅,不支持多节点 NFS ├── 网络文件系统,天然支持多台机器同时挂载 ├── 可以设置读写或只读权限 └── 支持 RWO ✅ ROX ✅ RWX ✅ CephFS ├── 分布式文件系统,功能最全 ├── 多节点读写没问题,还支持单 Pod 独占 └── 全部支持 ✅✅✅✅不同的阅读权限,适配不同的场景。
| 全称 | 缩写 | 含义 | 场景 |
|---|---|---|---|
| ReadWriteOnce | RWO | 节点级,只能被一个节点以读写模式挂载 | 单节点数据库 |
| ReadOnlyMany | ROX | 多节点级,可以被多个节点以只读模式挂载 | 共享配置文件、证书、静态资源 |
| ReadWriteMany | RWX | 多节点级,可以被多个节点以读写模式挂载 | 共享日志目录、多人文件上传、NFS共享存储 |
| ReadWriteOncePod | RWOP | Pod 级,只能被一个Pod以读写模式挂载 | 严格的隔离存储需求,防止同节点多个Pod 意外共享。 |
PV 的能力必须 >= PVC 的要求
3.3 Pod 使用PVC ---读者拿到书开始使用
apiVersion: v1 kind: Pod metadata: name: my-app spec: containers: - name: app image: nginx volumeMounts: - mountPath: /data # 在容器里挂到 /data name: my-storage volumes: - name: my-storage persistentVolumeClaim: claimName: pvc-nfs # 引用借书单(PVC)完整流程:管理员备书--> 读者写借书单---> 读者用书
3.4 绑定规则--- “图书管理系统的匹配逻辑”
PV和PVC绑定的所有条件满足才能绑定,规则如下(全部满足才能绑定):
storageClassName 相同 ---> 必须是同一个书架
accessModes 兼容 ---> 你需要的能力必须满足
PV 容量 >= PVC 请求的 ---> 我有这么多才能给你
PV 状态是 Available --> 没被借走
注意:PV 和 PVC 是 1:1 独占绑定。 一个 PV 绑了这个 PVC,就不会再分给别人。所以不存在"切割"的场景。3Gi 是下限要求,不是上限限制。绑上之后,PV 有多少你就用多少。
筛选条件(全部满足) ├── 1. storage ClassName 相同 ├── 2. accessModes 兼容 ├── 3. PV.capacity >= PVC.request └── 4. PV.status == Available 优选规则(从通过筛选的里挑) └── 候选中选容量最小的 绑定时机 ├── Immediate:立刻绑 └── WaitForFirstConsumer:等 Pod 出现再绑3.5 回收策略---“还书后怎么办”
生产环境中必须使用Retain!,默认是Delete,PVC一删,底层数据就没了,不可恢复。
persistentVolumeReclaimPolicy: Retain Retain (保留) -> 还书,管理员处理 Delete (删除) -> 还书,之间销毁3.6 PV的生命周期
Released ---> Available 这一步不是自动的,需要管理员:
# 1. 查看 PV 状态 kubectl get pv pv-nfs# 2. 编辑 PV,清除绑定引用 kubectl edit pv pv-nfs # 删除 spec.claimRef 字段段,绑定后自动生成字段。 # PV 就会回到 Available 状态 status.phase: Available/Bound/Released3.7 PVC 一直Pending的原因
就像借书单一直没人处理: 1. 书架上没有合适的书(没有匹配的 PV) 2. 你说要"科幻类"(storageClassName),但书架上只有"历史类" 3. 你要能多人翻阅(RWX),但所有书都只允许一人借(RWO) 4. 书都被借走了(没有 Available 的 PV)四、动态存储---“自动买书机器人”
核心矛盾点:手动创建PV太麻烦了(每本书都要管理员登记)。
本质:动态存储就是你说你要什么书,机器人自己去采购。
流程对比:
手动存储(Static): 管理员创建 PV → 开发创建 PVC → K8s 绑定 → Pod 使用 (就像:图书馆先买好书 → 你来借) 动态存储(Dynamic): 管理员配置 StorageClass → 开发创建 PVC → 机器人自动创建 PV 并绑定 → Pod 使用 (就像:图书馆配了自动购书机 → 你填借书单 → 机器人自动去买书并给你)4.1 nfs-provisioner---“买书机器人”
当你创建一个PVC要求(要16G的NFS存储)---> nfs-provisioner监听到这个请求 ---> 自动在NFS 服务器上创建子目录并且自动创建对应的PV ---> 自动绑定PVC 和 PV ---> Pod 就可以使用了。
部署nfs-provisioner :
# nfs-provisioner 需要一个真正的 NFS 服务器作为后端。 # 确认 NFS 服务器可达 showmount -e 192.168.62.15 # 创建命名空间(可选) kubectl create namespace storage # 部署 RBAC ,Provisioner需要权限去创建/删除 PV、监听 PVC 事件。 kubectl apply -f rbac.yaml # 部署 provisioner kubectl apply -f deployment.yaml # 确认 provisioner 运行 kubectl get pods -n kube-system | grep nfs # nfs-provisioner-xxxxx 1/1 Running 0 30s4.2 StorageClass 即购书规则
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client annotations: # 设为默认 StorageClass storageclass.kubernetes.io/is-default-class: "true" # 谁来干活,名字必须和 provisioner 里 PROVISIONER_NAME 一致 provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # 删 PVC 时自动删除 PV(数据也会删!生产环境建议改成 Retain) reclaimPolicy: Delete # 立即绑定(生产环境推荐 WaitForFirstConsumer) volumeBindingMode: Immediate # 允许 PVC 扩容 allowVolumeExpansion: true parameters: # 删 PVC 时不真删,改名为 archived-xxx archiveOnDelete: "true"关键字段翻译:
provisioner → 谁来干活(指定用哪个机器人) reclaimPolicy → 删 PVC 时 PV 怎么处理 volumeBindingMode: - Immediate → PVC 创建立刻绑定 PV(不管 Pod 在哪) - WaitForFirstConsumer → 等 Pod 确定调度到哪个节点后再绑定(推荐生产用)4.3 创建PVC
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-app-data spec: storageClassName: nfs-client # 指定 StorageClass accessModes: - ReadWriteMany # 多节点读写 resources: requests: storage: 10Gi4.4 Pod使用
apiVersion: v1 kind: Pod metadata: name: test-app spec: containers: - name: app image: nginx volumeMounts: - mountPath: /usr/share/nginx/html name: web-data volumes: - name: web-data persistentVolumeClaim: claimName: my-app-data五、StatefulSet + volume Claim Templates --- “每人一个独立保险箱”
问题场景: deployment部署3个Mysqal 副本,如果共用一个PVC:
MySQL-0 ─┐ MySQL-1 ─┼──→ 共用同一个 PVC → 数据互相覆盖!灾难! MySQL-2 ─┘使用statefulset 控制器部署有状态应用
MySQL-0 ──→ PVC: mysql-data-mysql-stateful-0 ──→ NFS目录1 MySQL-1 ──→ PVC: mysql-data-mysql-stateful-1 ──→ NFS目录2 MySQL-2 ──→ PVC: mysql-data-mysql-stateful-2 ──→ NFS目录3apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql # 关联的 Headless Service 名字 replicas: 3 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD value: "my-secret-pw" volumeMounts: - mountPath: /var/lib/mysql # 容器内挂载路径 name: data # 对应下面的 volume 名字 volumeClaimTemplates: # PVC 模板定义 - metadata: name: data # PVC 名字的前缀 spec: accessModes: - ReadWriteOnce # 数据库用 RWO storageClassName: nfs-client # 使用的 StorageClass resources: requests: storage: 10Gi # 每个副本请求 10Gi这样每个Pod都有自己稳定的PVC ,互不影响,别忘了Headless Service服务来给我们的Pod 提供一个稳定的DNS 名字。
StatefulSet 缩容或者删除时,PVC 和里面的数据保留---这是故意设计的,防止误删数据。要清理数据得手动删除PVC。
kubectl delete pvc mysql-data-mysql-stateful-2 PVC 名 = 模板名-StatefulSet名-序号 → 稳定可预测 PV 名 = pvc-随机UID → 系统生成,不用关心 Pod 只认 PVC,PVC 认 PV 所以 PV 叫什么都不重要,PVC 的稳定性才是关键六、Volume Snapshot 快照备份---“给数据拍个快照”
给磁盘PVC 拍一张"照片",记录当前的状态。之后随时可以用这张照片,把磁盘恢复到拍照片那一个时刻。
原始数据(PVC) ──拍快照──▶ 快照(VolumeSnapshot) ──恢复──▶ 新PVC6.1 创建快照
apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: db-snapshot-2025-04-12 spec: volumeSnapshotClassName: csi-snapclass #指定快照的"规格" source: persistentVolumeClaimName:>6.2从快照恢复apiVersion: v1 kind: PersistentVolumeClaim # 注意:恢复出来的是 PVC,不是新的快照 metadata: name:>七、fsGroup 与文件权限---“门禁卡”核心矛盾点:容器以非 root 运行(readOnlyRootFilesystem: true)时,挂载的 PVC 可能因为卷的权限和容器用户不匹配而写不进去
securityContext: runAsUser: 10000 runAsGroup: 10000 fsGroup: 10000 # 给卷加一个"门禁卡",让 Pod 能写进去 seccompProfile: type: RuntimeDefault
八、多租户存储配额---“每人限额”
核心矛盾点:集群里不能随便使用存储,得设置上限。
团队 A:一口气申请了 500 个 PVC,把存储全占了 团队 B:一个 PVC 申请了 10TB,完全用不上 团队 C:啥也申请不到,PVC 一直 Pending
所以得设置两种限制策略:**单个PVC的限制 **和整个团队的总配额
8.1 LimitRange ---限制单个PVC
apiVersion: v1 kind: LimitRange metadata: name: tenant-limits namespace: team-alpha spec: limits: - type: PersistentVolumeClaim max: storage: 50Gi # 最大不能超过 50Gi min: storage: 1Gi # 最小不能低于 1Gi
作用:单笔订单限额,kubenetes 自动校验,超出范围直接拒绝创建。
8.2 ResourceQuota --- 限制整个命名空间的总量
apiVersion: v1 kind: ResourceQuota metadata: name: team-quota namespace: team-alpha spec: hard: requests.storage: 200Gi # 所有 PVC 加起来最多 200Gi persistentvolumeclaims: "10" # 最多创建 10 个 PVC
作用:限制团队的总额和PVC总数。
两者缺一不可:
只有 ResourceQuota 没有 LimitRange → 用户可以申请一个 190Gi 的巨型 PVC,一个人占掉几乎全部配额.
只有 LimitRange 没有 ResourceQuota → 每个 PVC 不超过 50Gi,但用户可以申请 100 个小的,总量还是爆炸.
九、速记
EmptyDir 便签纸,Pod 一删就消失 hostPath 房东桌,换节点就找不到 NFS 共享图书馆,跨节点都能读 PV 是书架上书,PVC 是借书单 StorageClass 购书机,动态创建不用愁 StatefulSet 保险箱,每人一个 PVC Retain 保留数据,Delete 销毁不可逆 fsGroup 门禁卡,非 root 也能写 sizeLimit 必须有,磁盘写满全跑路