Kubernetes 的存储(PV PVC)
从 Kubernetes 的存储模型、资源关系、挂载流程、扩容方式和实际 YAML 示例几个方面,系统理解: - StorageClass - PV - PVC - Volume 挂载 - 存储扩容
从 Kubernetes 的存储模型、资源关系、挂载流程、扩容方式和实际 YAML 示例几个方面,系统理解: - StorageClass - PV - PVC - Volume 挂载 - 存储扩容
Kubernetes 中,Pod 通常不应该直接关心底层存储设备,例如:
Pod 只需要声明:
我需要多大的存储空间、什么访问模式、什么存储类型。
这个声明就是 PVC。
Kubernetes 再根据 PVC 找到或创建对应的 PV。
整体关系如下:
StorageClass:定义“如何提供存储”
↓
PV:实际的一块存储资源
↓
PVC:用户对存储的申请
↓
Pod:通过 PVC 使用存储可以类比为:
| Kubernetes 存储对象 | 类比 |
|---|---|
| StorageClass | 存储产品或存储套餐 |
| PV | 实际分配出来的硬盘空间 |
| PVC | 用户提交的存储申请 |
| Pod 挂载 | 应用把硬盘挂载到容器目录 |
PV,全称 PersistentVolume,持久卷。
它代表集群中的一块实际存储资源,例如:
PV 是集群级资源,不属于某个 Namespace。
kubectl get pvPV 的生命周期通常包括:
Available → Bound → Released → Reclaimed下面是一个静态创建的 NFS PV:
字段说明:
capacity:
storage: 10Gi表示该 PV 声称提供 10 GiB 存储空间。
注意:
PV 中声明的容量不一定等于底层存储实际容量,具体取决于存储后端。
accessModes:
- ReadWriteMany表示访问模式。
常见模式:
| 模式 | 含义 |
|---|---|
| ReadWriteOnce,RWO | 以读写方式挂载到一个节点 |
| ReadOnlyMany,ROX | 可以被多个节点只读挂载 |
| ReadWriteMany,RWX | 可以被多个节点读写挂载 |
| ReadWriteOncePod,RWOP | 只能被一个 Pod 读写挂载 |
需要注意:
RWO 通常表示“一个节点”,不一定是“一个 Pod”。
一个节点上的多个 Pod,可能都可以使用一个 RWO 卷。
persistentVolumeReclaimPolicy: Retain表示 PVC 被删除后,PV 和底层数据怎么处理。
常见策略:
| 策略 | 含义 |
|---|---|
| Retain | 保留 PV 和数据,需要人工处理 |
| Delete | 删除 PVC 时,自动删除 PV 以及底层存储资源 |
| Recycle | 旧机制,已经基本不推荐使用 |
生产环境中,数据库、重要业务通常考虑:
persistentVolumeReclaimPolicy: Retain这样可以防止误删 PVC 导致数据被自动删除。
storageClassName: nfs-storage表示该 PV 属于哪个 StorageClass。
PVC 通常通过这个字段匹配 PV。
PVC,全称 PersistentVolumeClaim,持久卷声明。
PVC 不是实际存储本身,而是用户对存储的申请:
我需要 10Gi 存储空间,要求支持 ReadWriteOnce,使用 fast-storage 存储类型。
PVC 属于 Namespace。
kubectl get pvc -A一个 Namespace 中的 Pod,只能直接使用该 Namespace 中的 PVC。
含义:
ReadWriteManynfs-storage查看 PVC:
kubectl get pvc查看详情:
kubectl describe pvc app-data常见状态:
表示 PVC 还没有绑定到 PV。
可能原因:
表示 PVC 已经成功绑定到 PV。
kubectl get pvc输出可能类似:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
app-data Bound pv-nfs-demo 10Gi RWX nfs-storage表示绑定的 PV 出现问题,通常需要重点检查。
PVC 创建后,Kubernetes 会根据以下条件寻找合适的 PV:
例如 PVC 请求:
resources:
requests:
storage: 5GiPV 提供:
capacity:
storage: 10Gi通常 10Gi 的 PV 可以满足 5Gi 的 PVC。
但是:
PV 绑定后,通常不会把剩余的 5Gi 再分配给其他 PVC。
也就是说,一个 PV 通常只能绑定一个 PVC。
StorageClass 用来描述:
如何自动创建某种类型的 PV。
它通常包含:
StorageClass 的最大作用是支持动态供应。
没有 StorageClass 时,通常需要管理员手动创建 PV。
有 StorageClass 后,用户只需要创建 PVC,Kubernetes 就可以自动创建底层存储和 PV。
下面是一个抽象的 CSI StorageClass:
注意:
provisioner: csi.example.com只是示例。实际环境需要使用具体存储系统提供的 CSI 驱动,例如:
provisioner: csi.example.com表示由哪个存储插件负责创建存储。
传统的内置 Provisioner 已逐渐被 CSI 替代。
reclaimPolicy: Delete动态创建的 PV 默认经常使用 Delete。
例如:
PVC 删除
↓
PV 删除
↓
云盘或底层卷也可能被删除对于重要数据,应谨慎使用 Delete。
可以设置为:
reclaimPolicy: RetainallowVolumeExpansion: true表示是否允许通过修改 PVC 的容量实现扩容。
如果没有设置为 true,修改 PVC 容量通常不会触发真正的存储扩容。
常见值:
volumeBindingMode: Immediate创建 PVC 后,立即创建或绑定 PV。
适用于:
volumeBindingMode: WaitForFirstConsumer等 Pod 被调度时,再决定存储应该在哪个拓扑位置创建。
适用于:
这是云环境和本地存储中非常重要的配置。
例如:
mountOptions:
- nfsvers=4.1或者:
mountOptions:
- noatime具体选项取决于存储类型和 CSI 驱动。
管理员提前创建 PV,用户再创建 PVC。
管理员创建 PV
↓
用户创建 PVC
↓
PVC 绑定已有 PV适合:
示例:
hostPath 只适合测试环境,不建议用于生产数据。
PVC:
用户只创建 PVC:
PVC 示例:
前提是集群中已经安装对应 CSI 驱动并创建了 StorageClass。
其中:
volumes:
- name: app-storage定义 Pod 级别的卷。
volumeMounts:
- name: app-storage
mountPath把卷挂载到容器内部目录。
claimName: app-data表示使用名为 app-data 的 PVC。
mountPath: /data表示容器内部的挂载路径。
如果容器原来 /data 目录中有文件,挂载卷后通常会被卷内容覆盖或隐藏。
例如:
容器原始目录:/data/config.txt
挂载 PVC 后:看到的是 PVC 中的内容因此配置镜像时需要注意挂载目录是否包含重要的默认文件。
可以只挂载卷中的某个子目录:
表示把 PVC 中的:
logs/挂载到容器中的:
/var/log/app注意:
subPath 对 ConfigMap、Secret、PVC 都有一些特殊行为subPath 时,某些卷内容更新不会实时反映到容器中表示容器以只读方式访问该挂载点。
即使 PVC 的访问模式是 ReadWriteMany,也可以在某个容器中单独使用只读挂载。
进入容器查看:
kubectl exec -it app-pod -- sh
cat /data/message.txt删除 Pod 后重新创建,只要 PVC 和底层 PV 还在,数据通常仍然存在。
需要特别注意:
如果 replicas 大于 1:
replicas: 3而 PVC 使用的是:
ReadWriteOnce那么三个 Pod 是否能正常使用该 PVC,取决于它们是否运行在同一个节点以及存储插件的具体行为。
如果多个节点上的 Pod 需要同时读写,通常需要:
ReadWriteMany以及支持 RWX 的后端存储,例如 NFS、CephFS、部分云文件存储等。
StatefulSet 通常为每个 Pod 创建独立 PVC。
示例:
StatefulSet 会生成类似的 PVC:
mysql-data-mysql-0
mysql-data-mysql-1每个 Pod 都有自己的独立存储。
这是数据库、消息队列、分布式存储等有状态应用常用的方式。
PVC 扩容通常分为两部分:
扩容底层存储
↓
扩容文件系统
↓
容器中看到新的容量例如:
原 PVC:10Gi
修改为:20Gi如果 StorageClass 和 CSI 驱动支持扩容,Kubernetes 会:
kubectl get storageclass fast-storage -o yaml检查:
allowVolumeExpansion: true完整示例:
原 PVC:
resources:
requests:
storage: 10Gi修改为:
resources:
requests:
storage: 20Gi也可以直接执行:
kubectl patch pvc app-data \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'查看状态:
kubectl get pvc app-data
kubectl describe pvc app-data成功后可能看到:
CAPACITY 20GiKubernetes 通常支持:
10Gi → 20Gi但不支持直接:
20Gi → 10Gi原因是缩容可能导致数据丢失。
如果确实需要缩容,通常需要:
不同存储驱动支持能力不同。
Pod 不停止,直接扩容并在容器中看到新容量。
需要先停止 Pod,完成底层扩容和文件系统扩容,再重新挂载。
是否支持在线扩容,取决于:
常见 Linux 文件系统如 ext4、xfs 通常支持在线扩容,但仍要以具体 CSI 驱动文档为准。
扩容时可以查看:
kubectl describe pvc app-data可能看到一些条件:
FileSystemResizePending这通常表示:
底层卷已经扩容,但文件系统还没有完成扩容。
可能需要:
检查容器内部容量:
kubectl exec -it app-pod -- df -h /data检查 PV:
kubectl get pv从应用角度看,挂载过程大致如下:
例如云盘场景:
这三个概念经常混淆。
定义 Pod 使用什么卷:
定义容器内挂载到什么目录:
volumeMounts:
- name: data
mountPath: /data定义申请哪一块持久化存储:
kind: PersistentVolumeClaim可以理解为:
PVC:申请硬盘
volume:把这块硬盘交给 Pod
volumeMounts:把硬盘挂到容器目录Pod 创建时生成,Pod 删除时数据消失。
volumes:
- name: cache
emptyDir: {}适合:
不适合:
适合挂载配置文件。
适合挂载密码、证书等敏感信息。
适合持久化业务数据。
直接使用节点上的目录:
风险:
适合:
示例:
accessModes:
- ReadWriteOnce适合:
常见后端:
适合:
适合:
需要 CSI 驱动和 Kubernetes 版本支持。
删除 Pod 通常不会删除 PVC:
kubectl delete pod app-podPVC 和 PV 通常仍然存在。
删除 PVC:
kubectl delete pvc app-data可能导致:
是否删除底层数据,取决于:
persistentVolumeReclaimPolicy生产环境删除前建议检查:
kubectl get pvc
kubectl get pv
kubectl get并确认备份策略。
执行:
kubectl describe pvc <pvc-name>检查:
执行:
kubectl describe pod <pod-name>常见原因:
WaitForFirstConsumer,但没有满足调度条件查看事件:
kubectl describe pod <pod-name>也可以查看节点和 CSI 组件日志。
常见原因:
例如容器以非 root 用户运行,挂载目录属于 root:
Permission denied可以使用 securityContext:
securityContext:
fsGroup: 1000示例:
spec:
securityContext:
fsGroup: 1000也可以在镜像或存储端调整目录权限。
但注意:
chmod 777数据库、订单、用户上传文件等重要数据,优先考虑:
reclaimPolicy: Retain并建立备份和恢复方案。
尽量使用成熟的 CSI 驱动,不建议依赖旧的内置存储插件。
不要完全依赖默认 StorageClass,建议显式指定:
storageClassName: fast-storage这样可以避免默认类变化导致存储类型改变。
PVC 解决的是存储挂载问题,不等于完整的数据高可用方案。
数据库还需要考虑:
不要因为“想让多个 Pod 使用”就直接使用 RWX。
对于数据库、消息队列、ZooKeeper、Elasticsearch 等应用,通常使用 StatefulSet 的:
volumeClaimTemplates让每个副本拥有独立 PVC。
可以用一句话理解:
StorageClass 定义存储如何创建,PV 代表实际存储资源,PVC 是应用对存储的申请,Pod 通过 volume 和 volumeMounts 使用 PVC,扩容则是修改 PVC 容量并由 CSI 驱动扩展底层存储和文件系统。
典型流程:
一个最常见的动态存储使用结构如下:
Pod 挂载:
容器挂载:
volumeMounts:
- name: data
mountPath: /data扩容:
kubectl patch pvc app-data \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'前提是对应 StorageClass 支持:
allowVolumeExpansion: true下面继续深入讲解 K8s 存储,重点包括:
CSI,全称 Container Storage Interface,是容器存储接口。
Kubernetes 通过 CSI 与各种存储系统对接,例如:
Kubernetes 本身不需要了解每种存储的具体实现,只需要调用 CSI 提供的标准接口。
整体结构:
Kubernetes
↓
CSI Driver
↓
存储系统例如:
Kubernetes
↓
Ceph CSI
↓
Ceph 集群一个 CSI 驱动通常包含以下组件:
一般以 Deployment 形式运行,负责:
一般以 DaemonSet 形式运行,每个节点运行一个,负责:
结构可以理解为:
查看 CSI 相关组件:
kubectl get pods -A | grep -i csi查看 CSI 驱动:
kubectl get csidrivers查看 CSI 节点信息:
kubectl get csinode可以把三者看成三层抽象。
StorageClass 关心:
使用什么存储驱动?
创建什么类型的盘?
是否支持扩容?
删除 PVC 后是否删除底层卷?
什么时候创建存储?PV 关心:
这块存储多大?
支持什么访问模式?
属于哪个 StorageClass?
当前绑定给谁?
数据回收策略是什么?PVC 关心:
我要多大容量?
我要什么访问模式?
我要使用哪类存储?例如:
PVC:需要 50Gi、RWO、SSD
StorageClass:用云厂商 SSD 云盘自动创建
PV:实际创建出来的一块 50Gi 云盘当 PVC 创建时,Kubernetes 会根据条件选择 PV。
大致考虑:
例如 PVC:
一个合适的 PV 可能是:
因为:
50Gi >= 20Gi并且访问模式、StorageClass 匹配,所以可以绑定。
例如:
PVC 请求 100Gi
集群有两个 50Gi PV通常不能自动把两个 50Gi PV 拼成一个 100Gi PVC。
PV 和 PVC 一般是:
一个 PVC ↔ 一个 PV如果需要更大容量,需要创建一个容量足够大的底层卷。
PVC 还有一个重要字段:
volumeMode: Filesystem常见取值:
| volumeMode | 含义 |
|---|---|
| Filesystem | 以文件系统目录方式挂载 |
| Block | 以原始块设备方式提供 |
默认模式:
volumeMode: Filesystem容器中看到的是目录:
/data常见数据库、Web 应用、日志系统都使用这种模式。
volumeMode: Block容器中看到的是块设备:
volumeDevices:
- name: block-storage
devicePath: 完整示例:
块模式一般用于:
大部分普通应用使用 Filesystem 即可。
有些存储卷不能随便创建,它只能位于某个可用区。
例如:
节点 node-a 位于 zone-a
节点 node-b 位于 zone-b
云盘创建在 zone-a如果 Pod 最终被调度到 node-b,那么它可能无法使用 zone-a 的云盘。
因此 StorageClass 可以设置:
volumeBindingMode: WaitForFirstConsumer含义:
PVC 创建时先不立即创建存储,等 Pod 调度时,根据 Pod 最终选择的节点创建合适位置的存储。
流程:
适合:
有些 PV 只能被某些节点使用。
例如:
含义:
这个 PV 只能在 zone-a 中的节点使用。
查看 PV:
kubectl get pv <pv-name> -o yaml如果 Pod 一直 Pending,可以重点检查:
spec.nodeAffinity以及节点标签:
kubectl get nodes --show-labelsPVC 可以通过 selector 选择特定标签的 PV。
PV:
PVC:
含义:
只绑定同时具有
environment=production和disk-type=ssd标签的 PV。
注意:
PVC 使用 selector 时,通常不会触发动态供应,因为它要求匹配现有 PV。
部分 CSI 驱动支持从已有 PVC 克隆出新的 PVC。
例如已有 PVC:
source-pvc创建克隆:
要求:
适合场景:
VolumeSnapshot 是对 PVC 当前数据状态的一个时间点副本。
可以理解为:
PVC 当前状态
↓
创建快照
↓
将来可以从快照恢复新 PVC它通常依赖:
查看快照相关资源:
kubectl get volumesnapshot
kubectl get volumesnapshotclass
不同 CSI 驱动配置不同,示例:
deletionPolicy 常见值:
| 值 | 含义 |
|---|---|
| Delete | 删除 VolumeSnapshot 时删除底层快照 |
| Retain | 保留底层快照 |
查看状态:
kubectl get volumesnapshot app-data-snapshot
kubectl describe volumesnapshot app-data-snapshot注意:
快照不等于完整备份。
如果底层存储集群损坏,快照也可能不可用。因此生产环境一般还需要:
创建快照前,如果应用正在写数据,可能出现数据不一致。
例如数据库正在执行:
事务 A 已写入部分数据
事务 B 尚未完成
此时创建快照恢复后可能出现:
因此数据库建议采用:
例如 MySQL 不能只依赖简单的 PVC 快照作为全部备份方案。
Deployment 的 Pod 通常是无状态的:
web-xxx
web-yyy
web-zzzPod 名称和身份不稳定。
StatefulSet 提供:
例如:
mysql-0
mysql-1
mysql-2每个 Pod 都有自己的 PVC:
mysql-data-mysql-0
mysql-data-mysql-1
mysql-data-mysql-2原来:
replicas: 1修改为:
replicas: 3通常会创建:
mysql-0
mysql-1
mysql-2并为每个实例创建独立 PVC。
如果从 3 副本缩容到 1:
replicas: 1通常:
mysql-2 被删除mysql-1 被删除mysql-0 保留这是一种保护机制,避免缩容时直接删除数据。
查看 PVC:
kubectl get pvc执行后,每个 Pod 使用不同的 PVC:
data-app-0
data-app-1
data-app-2PVC 挂载成功不代表应用一定能写入。
常见问题:
Permission denied原因可能是:
含义:
某些场景下,递归修改大目录权限会很慢,可以设置:
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch含义:
ReadWriteOnce 常被理解为:
只能被一个 Pod 使用。
更准确地说:
该卷可以被一个节点以读写方式挂载。
因此:
node-1:
pod-a 使用 RWO 卷
pod-b 使用 RWO 卷某些存储插件是允许的。
但如果:
node-1 使用该 RWO 卷
node-2 也想使用该 RWO 卷通常会出现挂载冲突。
如果应用副本可能跨节点运行,并且需要共享写入,应考虑 RWX 存储。
假设要把数据从旧 PVC 迁移到新 PVC。
不过,如果两个 PVC 都是 RWO,并且底层存储不允许同时挂载,需要:
kubectl get pvc
kubectl describe pvc <pvc-name>重点看:
kubectl get pv
kubectl describe pv <pv-name>重点看:
kubectl get pod
kubectl describe pod <pod-name>重点看事件:
FailedMount
FailedAttachVolume
FailedScheduling
Multi-Attach errorkubectl describe node <node-name>检查:
具体组件名称取决于存储厂商。
pod has unbound immediate PersistentVolumeClaims含义:
Pod 使用的 PVC 还没有绑定。
检查:
kubectl get pvc
kubectl describe pvc <pvc-name>no persistent volumes available for this claim含义:
没有满足 PVC 要求的静态 PV。
检查:
Multi-Attach error含义:
一个只支持单节点挂载的卷,正在尝试挂载到多个节点。
常见于:
处理方式:
kubectl get volumeattachmentFailedMount含义:
节点或容器挂载存储失败。
可能原因:
FileSystemResizePending含义:
底层卷扩容完成,但文件系统扩容尚未完成。
可以尝试:
df -h这三个概念容易混淆。
同一存储系统中的时间点副本优点:
缺点:
将数据复制到独立位置例如:
优点:
实时或准实时地复制到另一个副本例如:
复制主要提高可用性,备份主要用于恢复历史数据。
单副本数据库即使使用了 PVC,也仍然存在:
PVC 只是持久化存储,不等于高可用。
至少考虑:
本地快照
+
远程备份
+
定期恢复演练只创建快照、不测试恢复,不能算完整备份方案。
开发环境可以:
reclaimPolicy: Delete生产数据库建议:
reclaimPolicy: Retain并建立人工回收流程。
可以创建不同存储类:
standard-storage
fast-ssd-storage
archive-storage示例:
storageClassName: fast-ssd-storage业务根据性能需求选择对应存储。
hostPath 的数据绑定在节点上:
Pod 在 node-1:看到 /data/app
Pod 迁移到 node-2:看到的是另一份 /data/app除非你明确知道应用只运行在固定节点,否则不建议用它做业务持久化。
下面假设集群已经安装了名为 csi.example.com 的 CSI 驱动。
kubectl exec app -- tail -f /data/app.logkubectl patch pvc app-data \
-p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'可以把整个流程记成:
最常见的生产组合是:
而排查存储问题时,优先遵循:
这样可以快速定位问题到底是在:
哪个层面。
还有,K8s 存储还有一些比较重要的进阶内容。前面主要讲了“怎么申请、绑定、挂载和扩容”,下面继续补充实际生产中经常遇到的内容。
PV 的典型生命周期:
Available → Bound → Released → FailedPV 已经创建,但还没有绑定 PVC。
kubectl get pv可能显示:
NAME CAPACITY ACCESS MODES STATUS CLAIM
pv-demo 10Gi RWO AvailablePV 已经绑定到 PVC:
PV pv-demo
↕
PVC default/app-data一个 PV 通常只能绑定一个 PVC。
PVC 被删除后,PV 进入 Released。
PVC 被删除
↓
PV 进入 Released此时 PV 通常不能立即被新的 PVC 使用,因为原来的绑定关系和数据仍然存在。
查看:
kubectl get pvPV 回收失败,通常表示底层存储删除、清理或回收操作异常。
需要查看:
kubectl describe pv <pv-name>PV 被 PVC 绑定后,通常会出现:
spec:
它表示该 PV 当前属于哪个 PVC。
有时管理员希望让某个 PV 只绑定给特定 PVC,可以预先设置:
spec:
这可以避免 PV 被其他 PVC 抢先绑定。
假设:
persistentVolumeReclaimPolicy: Retain删除 PVC 后,PV 可能变成:
Released如果想重新使用这块 PV,通常需要:
claimRef查看 PV:
kubectl get pv pv-demo -o yaml删除 claimRef:
kubectl patch pv pv-demo \
--type=json \
但需要注意:
删除 claimRef 只是解除 Kubernetes 绑定,不会自动清理底层数据。
如果底层是 NFS,旧文件可能仍然存在;如果底层是云盘,也可能保留原数据。
Kubernetes 为了避免资源被误删或在底层存储尚未处理完成时提前删除,会使用 Finalizer。
查看 PVC:
kubectl get pvc app-data -o yaml可能看到:
metadata:
finalizers:
- kubernetes.io/pvc-protection查看 PV:
kubectl get pv pv-demo -o yaml可能看到:
metadata:
finalizers:
- kubernetes.io/pv-protection含义:
如果资源长期处于:
Terminating可以检查:
kubectl describe
不要一开始就强制删除 Finalizer,因为可能导致:
只有确认底层资源已经人工处理后,才考虑强制移除 Finalizer。
对于需要 Attach/Detach 的块存储,Kubernetes 可能创建 VolumeAttachment 资源。
查看:
kubectl get volumeattachment示例:
NAME ATTACHED NODE
csi-xxxxx true worker-1它描述:
某个卷是否已经挂载到某个节点。
当出现:
Multi-Attach error时,可以检查:
kubectl describe volumeattachment <name>常见原因:
存储挂载实际上可能包含多个步骤。
将云盘或块设备连接到节点:
云盘 → 节点例如:
云盘 /dev/sdb把块设备挂载到节点目录:
/dev/sdb → /var/lib/kubelet/plugins/...再把节点上的目录映射到 Pod 目录:
节点目录 → Pod 容器目录整体可以理解为:
因此挂载失败时,错误可能发生在不同层级:
某些特殊场景下,容器内执行的挂载操作需要传播到宿主机,或者宿主机挂载需要传播到容器。
相关配置:
常见取值:
| 值 | 含义 |
|---|---|
| None | 不传播 |
| HostToContainer | 宿主机挂载传播到容器 |
| Bidirectional | 容器和宿主机双向传播 |
常见应用:
注意:
Bidirectional具有较高风险,可能影响宿主机挂载树,普通业务 Pod 不应随意使用。
Kubernetes 中还有一类“临时存储”。
volumes:
- name: cache
emptyDir: {}特点:
适合:
数据存储在内存中,速度快,但会消耗 Pod 或节点内存。
适合:
不适合:
某些 CSI 驱动支持临时 CSI 卷:
它的生命周期与 Pod 绑定:
Pod 创建 → 创建临时卷
Pod 删除 → 删除临时卷适合:
除了 PVC,容器还会使用节点本地临时空间,例如:
可以为容器设置资源限制:
完整示例:
含义:
查看节点磁盘压力:
kubectl describe node <node-name>如果出现:
DiskPressure=True说明节点本地磁盘空间不足或 inode 紧张。
存储空间不只受容量限制,还受 inode 数量影响。
查看文件系统:
df -h
df -i可能出现:
容量还有很多
但 inode 已耗尽此时仍然无法创建新文件。
常见原因:
对于日志、缓存和对象存储类应用,除了关注 GiB,还要关注:
容量不是存储的全部。
常见性能指标:
| 指标 | 含义 |
|---|---|
| IOPS | 每秒输入输出次数 |
| Throughput | 每秒读写吞吐量 |
| Latency | 读写延迟 |
| Queue Depth | I/O 请求队列深度 |
| Bandwidth | 网络或存储带宽 |
不同应用关注点不同:
关注:
关注:
关注:
关注:
例如云硬盘、Ceph RBD。
特点:
适合:
MySQL、PostgreSQL、Redis、单实例应用例如 NFS、CephFS、云文件存储。
特点:
适合:
共享文件、上传文件、Web 静态资源例如 S3、OSS、COS。
特点:
适合:
图片、视频、备份、归档、日志不适合直接替代:
数据库数据目录例如 Local PV、节点本地 SSD。
特点:
适合:
但必须配合:
对应 StorageClass:
关键点:
volumeBindingMode: WaitForFirstConsumer本地盘必须等 Pod 调度后,再决定使用哪个节点上的 PV。
假设 PVC 当前是 10Gi。
kubectl get sc fast-storage -o yaml确认:
allowVolumeExpansion: truekubectl edit pvc app-data修改:
spec:
resources:
requests
或者:
kubectl patch pvc app-data \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'kubectl get pvc app-datakubectl describe pvc app-datakubectl exec -it <pod-name> -- df -h /data如果 PVC 已显示 20Gi,但容器中仍然是旧容量:
FileSystemResizePending不能直接修改:
storageClassName也不能直接把 PVC 的底层 PV 改成另一种存储类型。
常见迁移方式有三种。
旧 PVC
↓
创建快照
↓
使用新 StorageClass 从快照恢复
适合:
旧 PVC + 新 PVC
↓
迁移 Pod
↓
cp、rsync、tar适合:
例如数据库:
mysqldump
pg_dump
redis backup然后恢复到新 PVC。
适合:
通常数据库优先使用应用原生备份,而不是单纯复制数据目录。
PVC 是 Namespace 级资源:
kubectl get pvc -n appPod 只能使用同一个 Namespace 中的 PVC:
如果 Pod 在 prod Namespace,而 PVC 在 default Namespace,则不能直接引用。
解决方法:
PV 则是集群级资源:
kubectl get pv推荐顺序:
不要直接执行:
kubectl delete pvc --all尤其是在生产环境中,这可能导致大量底层存储被删除。
RWX 存储中的目录如果权限管理不严格,可能被多个应用读取。
建议:
某些存储插件需要特权权限,但普通业务容器一般不需要:
securityContext:
privileged: true这会增加风险。
需要考虑:
PVC 本身不自动等于加密。
推荐:
不要简单地让多个 MySQL Pod 共用一个 RWX 目录。
推荐:
Deployment
+
RWX 文件存储或者更推荐:
Deployment
+
对象存储Web Pod 将上传文件放入对象存储,而不是依赖本地文件系统。
不建议单纯依赖容器内部文件。
常见方案:
容器标准输出
↓
日志采集器
↓
Elasticsearch / Loki / 对象存储如果必须写文件,需要考虑:
推荐:
对象存储
+
启动时下载或者:
只读 PVC不建议每个 Pod 都复制一份超大数据到本地。
K8s 存储可以分成六个层次:
排查问题时建议按照这个顺序:
最重要的几条经验是:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-demo
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs-storage
nfs:
server: 192.168.1.100
path: /data/k8s/demo
readOnly: falseapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
namespace: default
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs-storage
resources:
requests:
storage: 5GiapiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: csi.example.com
parameters:
type: ssd
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumerapiVersion: v1
kind: PersistentVolume
metadata:
name: pv-static
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
storageClassName: manual
hostPath:
path: /data/staticapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-static
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi创建 PVC
↓
StorageClass 调用 CSI 驱动
↓
自动创建底层存储
↓
自动创建 PV
↓
PVC 与 PV 自动绑定apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-data
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20GiapiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: app-storage
mountPath: /usr/share/nginx/html
volumes:
- name: app-storage
persistentVolumeClaim:
claimName: app-datavolumeMounts:
- name: app-storage
mountPath: /var/log/app
subPath: logsvolumeMounts:
- name: app-storage
mountPath: /data
readOnly: trueapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10GiapiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- |
echo "hello kubernetes" >> /data/message.txt
sleep 3600
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-dataapiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.25
volumeMounts:
- name: web-data
mountPath: /usr/share/nginx/html
volumes:
- name: web-data
persistentVolumeClaim:
claimName: app-dataapiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 2
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: "example-password"
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20GiapiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: csi.example.com
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer底层存储
↓
CSI Controller 创建或管理卷
↓
PV 绑定 PVC
↓
节点上的 kubelet 挂载卷
↓
Pod volume
↓
容器 volumeMount云盘
↓
CSI 驱动创建云盘
↓
PV 指向云盘
↓
PVC 绑定 PV
↓
云盘 Attach 到节点
↓
节点 Mount 文件系统
↓
挂载到容器 /datavolumes:
- name: data
persistentVolumeClaim:
claimName: app-datavolumes:
- name: data
persistentVolumeClaim:
claimName: app-datavolumes:
- name: host-data
hostPath:
path: /data/app
type: DirectoryOrCreate创建 StorageClass
↓
创建 PVC
↓
动态创建 PV
↓
PVC 与 PV 绑定
↓
Pod 通过 volume 使用 PVC
↓
通过修改 PVC requests.storage 实现扩容apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Givolumes:
- name: data
persistentVolumeClaim:
claimName: app-dataCSI Controller
├── 创建卷
├── 删除卷
├── 扩容卷
└── 创建快照
CSI Node
├── 节点挂载
├── Pod 挂载
└── 文件系统扩容apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gicapacity:
storage: 50Gi
accessModes:
- ReadWriteOnce
storageClassName: fast-storageapiVersion: v1
kind: Pod
metadata:
name: block-pod
spec:
containers:
- name: app
image: busybox
command:
- sh
- -c
- sleep 3600
volumeDevices:
- name: block-storage
devicePath: /dev/mydisk
volumes:
- name: block-storage
persistentVolumeClaim:
claimName: block-pvc创建 PVC
↓
PVC 暂时 Pending
↓
创建 Pod
↓
调度器选择节点
↓
根据节点拓扑创建存储
↓
PV 创建并绑定
↓
Pod 挂载成功apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: zonal-storage
provisioner: csi.example.com
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: truespec:
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-aapiVersion: v1
kind: PersistentVolume
metadata:
name: pv-production
labels:
environment: production
disk-type: ssd
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
storageClassName: manual
hostPath:
path: /data/productionapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: production-data
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
selector:
matchLabels:
environment: production
disk-type: ssdapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cloned-pvc
spec:
storageClassName: fast-storage
dataSource:
name: source-pvc
kind: PersistentVolumeClaim
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20GiapiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-snapshot-class
driver: csi.example.com
deletionPolicy: RetainapiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: app-data-snapshot
spec:
volumeSnapshotClassName: csi-snapshot-class
source:
persistentVolumeClaimName: app-dataapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restored-data
spec:
storageClassName: fast-storage
dataSource:
name: app-data-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20GiapiVersion: apps/v1
kind: StatefulSet
metadata:
name: app
spec:
serviceName: app
replicas: 3
selector:
matchLabels:
app: stateful-app
template:
metadata:
labels:
app: stateful-app
spec:
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- |
echo "$(hostname)" >> /data/pod.txt
sleep 3600
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10GiapiVersion: v1
kind: Pod
metadata:
name: app
spec:
securityContext:
fsGroup: 1000
containers:
- name: app
image: busybox
command:
- sh
- -c
- sleep 3600
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-dataapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: new-data
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100GiapiVersion: v1
kind: Pod
metadata:
name: data-migration
spec:
restartPolicy: Never
containers:
- name: migration
image: alpine:3.19
command:
- sh
- -c
- |
cp -a /old/. /new/
echo migration completed
volumeMounts:
- name: old-data
mountPath: /old
readOnly: true
- name: new-data
mountPath: /new
volumes:
- name: old-data
persistentVolumeClaim:
claimName: old-data
- name: new-data
persistentVolumeClaim:
claimName: new-datakubectl get pods -A | grep csi
kubectl logs -n <namespace> <csi-controller-pod>
kubectl logs -n <namespace> <csi-node-pod>apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: app-storage
provisioner: csi.example.com
parameters:
type: ssd
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumerapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
storageClassName: app-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20GiapiVersion: v1
kind: Pod
metadata:
name: app
spec:
securityContext:
fsGroup: 1000
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- |
while true; do
date >> /data/app.log
sleep 10
done
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-datakubectl get storageclass
kubectl get pvc
kubectl get pv
kubectl get podkubectl get pvc app-data
kubectl describe pvc app-data
kubectl exec app -- df -h /dataStorageClass
= 如何生产存储
PV
= 已经生产出来的存储资源
PVC
= 应用申请存储
Pod volume
= Pod 使用 PVC
volumeMounts
= 容器内挂载路径
CSI
= Kubernetes 与实际存储系统之间的桥梁
扩容
= 修改 PVC 容量,由 CSI 扩大底层卷和文件系统
快照
= 某个时间点的存储副本
备份
= 将数据复制到独立位置StorageClass
+ CSI
+ PVC
+ StatefulSet
+ volumeClaimTemplates
+ 快照
+ 远程备份
+ Retain 回收策略Pod
↓
PVC
↓
PV
↓
StorageClass
↓
CSI Controller / CSI Node
↓
底层存储系统云盘
↓ Attach
节点块设备
↓ Mount
节点临时目录
↓ Bind Mount
Pod volume
↓ Mount
容器 /datavolumeMounts:
- name: host-mount
mountPath: /mnt
mountPropagation: Bidirectionalvolumes:
- name: cache
emptyDir:
medium: Memory
sizeLimit: 512Mivolumes:
- name: ephemeral-storage
csi:
driver: csi.example.com
volumeAttributes:
mode: temporaryresources:
requests:
ephemeral-storage: "1Gi"
limits:
ephemeral-storage: "2Gi"containers:
- name: app
image: nginx
resources:
requests:
ephemeral-storage: 500Mi
limits:
ephemeral-storage: 2GiapiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-1
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/disks/ssd1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-1apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumervolumes:
- name: data
persistentVolumeClaim:
claimName: app-data停止应用
↓
确认数据已同步
↓
删除 Deployment 或 StatefulSet
↓
确认 Pod 已终止
↓
备份或快照
↓
删除 PVC
↓
根据策略处理 PVStatefulSet
+
RWO 块存储
+
独立 PVC
+
定期数据库备份
+
快照1. 业务数据层
数据库、文件、日志、缓存
2. 容器挂载层
volumeMounts、subPath、权限
3. Pod 卷层
volumes、PVC、emptyDir
4. K8s 资源层
PVC、PV、StorageClass、VolumeSnapshot
5. CSI 层
Controller、Node、Attach、Mount、Expand
6. 底层存储层
云盘、NFS、Ceph、本地盘、对象存储应用是否正常写入
↓
容器挂载路径是否正确
↓
Pod 是否正确引用 PVC
↓
PVC 是否 Bound
↓
PV 是否正常
↓
StorageClass 是否正确
↓
CSI 组件是否正常
↓
节点是否可以挂载
↓
底层存储是否正常