【11-kubenetes的持久化存储】
一、核心理念
我们把kubenetes集群想象成一个出租公寓楼:
-
Pod --> 租客(人)
-
Node --> 公寓楼(物理建筑)
-
容器内的数据 --> 租客脑子中的记忆
-
Volume (存储卷) --> 租客能用的"写字的地方"
核心问题:Pod 是临时工,随时可能被杀死重建(就像租客随时可能搬走)。数据只存在Pod内,Pod一删,数据就没了。所以我们需要各种"外部笔记本"来保存数据。
二、存储类型对比
2.1 EmptyDir ---一次性的便签纸
本质:Pod里的临时共享空间
想想同一个房间内住着两人(nginx,redis),他们之间传递纸条:
YAML
Pod(一个房间)
├── nginx 容器(人A)→ 往 /opt 写纸条
├── redis 容器(人B)→ 从 /mnt 看纸条
└── emptyDir(桌上的一叠纸)← 两个人共享
YAML
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 不同,读到的数据却一样?
Python
name 决定"接的是哪间仓库",mountPath 决定"在你家叫什么门牌号"。门牌号不同,进的是同一间仓库。
关键是 name 匹配:
容器A 说:我要挂载 name: note-paper 的卷 → 找到 emptyDir
容器B 说:我要挂载 name: note-paper 的卷 → 找到同一个 emptyDir
name 相同 = 同一份存储
mountPath 不同 = 从各自内部看到的路径不同
在 Kubernetes 中,emptyDir 卷是由 **kubelet(节点级别的组件)** 来维护的,而非控制平面(API Server、Controller Manager 等)
卷定义关键参数:
YAML
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 排序驱逐,非全部),内存型是硬限制(报错);不设置定时炸弹,一个进程能瘫痪整个节点。
内存额度计算:
YAML
容器Memory limit = 256Mi
emptyDir sizeLimit =64MI (从Memory limit里扣)
实际MainC 可用 约等于 256 - 64 = 192Mi
**写的量会"占用"容器的内存额度!**
2.2 hostPath --- "借用房东的桌子"
本质:直接用节点(服务器)上的某个目录,数据跟着节点走,Pod被调度到别的节点就看不到原来的数据。
YAML
Node01(一栋楼)
├── /data 目录(房东的桌子)
│ └── file1.txt
│
├── Pod A(租客)→ 挂载 /data → 看到 file1.txt ✓
└── Pod B(租客)→ 挂载 /data → 看到 file1.txt ✓
Node02(另一栋楼)
└── /data 目录(另一张桌子)
└── (空的)
Pod C(租客)→ 挂载 /data → 什么都没有 ✗
YAML
volumes:
- name: data
hostPath:
path: /data # 用节点上的 /data 目录
type: DirectoryOrCreate # 如果 /data 不存在,自动创建
#type:
# DirectoryOrCreate ,目录没有就创建一个。
# Dirrectory ,没有就报错
# FileOrCreate,没有创建空文件
# File ,没有就报错
生产环境中不建议使用hostPath,因为Pod被调度到其他的节点就找不到数据了,除非是Daemonset日志采集这类场景。
2.3 NFS --- "公共图书馆的书架"
本质:所有人共享的网络存储。
YAML
NFS 服务器(图书馆,IP: 192.168.62.15)
└── /data/nfs(公共书架)
Node01 上的 Pod A → 写入 hello.txt
Node02 上的 Pod B → 读到 hello.txt ✓✓✓
这就是"跨节点共享"!
NFS搭建:
Markdown
# 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 挂载使用
YAML
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中解耦出来,变成独立管理的资源。
本质:类图书借阅系统,职责分离。
YAML
管理员(运维) → 买书,放在书架上,登记入库 = 创建 PV
读者(开发) → 填借书单,说"我要一本数据结构" = 创建 PVC
图书管理系统 → 自动把借书单和书匹配起来 = K8s 的绑定机制
书架 → 实际的存储(NFS、云盘等)
3.1 PV (PersistentVolume) ---书架上的一本书
PV 是管理员提前准备好的"存储资源",入库操作:
YAML
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 是用户说"我需要多大的存储":
YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs # 借书单叫 pvc-nfs
spec:
storageClassName: nfs-class # 我要从"nfs-class"书架找
accessModes:
- ReadWriteMany # 我需要多人同时读写
resources:
requests:
storage: 3Gi # 我要借3GB
Ⓜ️ accessModes :
不同存储支持不同模式
YAML
本地磁盘 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 ---读者拿到书开始使用
YAML
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 有多少你就用多少。
Plain
筛选条件(全部满足)
├── 1. storage ClassName 相同
├── 2. accessModes 兼容
├── 3. PV.capacity >= PVC.request
└── 4. PV.status == Available
优选规则(从通过筛选的里挑)
└── 候选中选容量最小的
绑定时机
├── Immediate:立刻绑
└── WaitForFirstConsumer:等 Pod 出现再绑
3.5 回收策略---"还书后怎么办"
生产环境中必须使用Retain!,默认是Delete,PVC一删,底层数据就没了,不可恢复。
YAML
persistentVolumeReclaimPolicy: Retain
Retain (保留) -> 还书,管理员处理
Delete (删除) -> 还书,之间销毁
3.6 PV的生命周期

Released ---> Available 这一步不是自动的,需要管理员:
YAML
# 1. 查看 PV 状态
kubectl get pv pv-nfs#
2. 编辑 PV,清除绑定引用
kubectl edit pv pv-nfs
# 删除 spec.claimRef 字段段,绑定后自动生成字段。
# PV 就会回到 Available 状态
status.phase: Available/Bound/Released
3.7 PVC 一直Pending的原因
YAML
就像借书单一直没人处理:
1. 书架上没有合适的书(没有匹配的 PV)
2. 你说要"科幻类"(storageClassName),但书架上只有"历史类"
3. 你要能多人翻阅(RWX),但所有书都只允许一人借(RWO)
4. 书都被借走了(没有 Available 的 PV)
四、动态存储---"自动买书机器人"
核心矛盾点:手动创建PV太麻烦了(每本书都要管理员登记)。
本质:动态存储就是你说你要什么书,机器人自己去采购。
流程对比:
YAML
手动存储(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 :
YAML
# 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 30s
4.2 StorageClass 即购书规则
YAML
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"
关键字段翻译:
YAML
provisioner → 谁来干活(指定用哪个机器人)
reclaimPolicy → 删 PVC 时 PV 怎么处理
volumeBindingMode:
- Immediate → PVC 创建立刻绑定 PV(不管 Pod 在哪)
- WaitForFirstConsumer → 等 Pod 确定调度到哪个节点后再绑定(推荐生产用)
4.3 创建PVC
YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-app-data
spec:
storageClassName: nfs-client # 指定 StorageClass
accessModes:
- ReadWriteMany # 多节点读写
resources:
requests:
storage: 10Gi
4.4 Pod使用
YAML
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:
YAML
MySQL-0 ─┐
MySQL-1 ─┼──→ 共用同一个 PVC → 数据互相覆盖!灾难!
MySQL-2 ─┘
使用statefulset 控制器部署有状态应用
YAML
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目录3
YAML
apiVersion: 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。
YAML
kubectl delete pvc mysql-data-mysql-stateful-2
PVC 名 = 模板名-StatefulSet名-序号 → 稳定可预测
PV 名 = pvc-随机UID → 系统生成,不用关心
Pod 只认 PVC,PVC 认 PV
所以 PV 叫什么都不重要,PVC 的稳定性才是关键
六、Volume Snapshot 快照备份---"给数据拍个快照"
给磁盘PVC 拍一张"照片",记录当前的状态。之后随时可以用这张照片,把磁盘恢复到拍照片那一个时刻。
YAML
原始数据(PVC) ──拍快照──▶ 快照(VolumeSnapshot) ──恢复──▶ 新PVC
6.1 创建快照
YAML
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: db-snapshot-2025-04-12
spec:
volumeSnapshotClassName: csi-snapclass #指定快照的"规格"
source:
persistentVolumeClaimName: data-postgres-0 #对谁拍照,pvc的名字
6.2从快照恢复
YAML
apiVersion: v1
kind: PersistentVolumeClaim # 注意:恢复出来的是 PVC,不是新的快照
metadata:
name: data-postgres-restored
spec:
storageClassName: fast-retain
dataSource: # 关键字段:数据来源
name: db-snapshot-2025-04-12 # 来自哪个快照
kind: VolumeSnapshot # 来源类型是快照
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
场景:破环性操作之前,先打快照。数据迁移、删除PVC、大版本更新。
注意事项:
-
快照可能是"崩溃一致性"(crash-consistent),不是"应用一致"(application-consistent)。
-
数据要做逻辑备份(pg-dump、mysqldump)来补充
-
定期测试恢复,从没恢复过的备份不算备份。
七、fsGroup 与文件权限---"门禁卡"
核心矛盾点:容器以非 root 运行(readOnlyRootFilesystem: true)时,挂载的 PVC 可能因为卷的权限和容器用户不匹配而写不进去
YAML
securityContext:
runAsUser: 10000
runAsGroup: 10000
fsGroup: 10000 # 给卷加一个"门禁卡",让 Pod 能写进去
seccompProfile:
type: RuntimeDefault
八、多租户存储配额---"每人限额"
核心矛盾点:集群里不能随便使用存储,得设置上限。
YAML
团队 A:一口气申请了 500 个 PVC,把存储全占了
团队 B:一个 PVC 申请了 10TB,完全用不上
团队 C:啥也申请不到,PVC 一直 Pending
所以得设置两种限制策略:**单个PVC的限制 **和 整个团队的总配额
8.1 LimitRange ---限制单个PVC
YAML
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 --- 限制整个命名空间的总量
YAML
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 个小的,总量还是爆炸.
九、速记
YAML
EmptyDir 便签纸,Pod 一删就消失
hostPath 房东桌,换节点就找不到
NFS 共享图书馆,跨节点都能读
PV 是书架上书,PVC 是借书单
StorageClass 购书机,动态创建不用愁
StatefulSet 保险箱,每人一个 PVC
Retain 保留数据,Delete 销毁不可逆
fsGroup 门禁卡,非 root 也能写
sizeLimit 必须有,磁盘写满全跑路