在云原生时代,Kubernetes 已经成为容器编排的事实标准。然而,容器的短暂特性带来了一个根本性的挑战:如何持久化和管理数据?
想象这样一个场景:你的应用容器因为某种原因崩溃了,Kubernetes 会立即重启一个新的容器实例。但问题来了——之前容器中的所有数据都消失了!数据库的记录、用户上传的文件、应用的日志,全部丢失。这显然是不可接受的。
这就是 Kubernetes Storage 系统要解决的核心问题。本文在AI的帮助下,快速学习一下 Kubernetes 存储的方方面面,从最基础的 Volume 概念到高级的 CSI 驱动实现。
Volumes(卷)
在深入技术细节之前,让我们先理解容器存储面临的两个根本性问题:
- 容器文件系统的短暂性
容器中的磁盘文件是临时的,这给在容器中运行的非简单应用程序带来了一些问题。一个问题是当容器崩溃或停止时,容器状态不会被保存,因此在容器生命周期内创建或修改的所有文件都会丢失。
这一点,我在浅析Docker基于overlay的存储结构中有详细介绍,Container的读写层是基于Image roLayer + Container init-Layer堆叠而成;Container在运行的时候创建了init-Layer,停止后该Layer就会自动销毁。
- 容器间数据共享
当多个容器需要访问相同的数据时,例如:
- Web 服务器容器和日志收集器容器共享日志目录
- 应用容器和 Sidecar 容器共享配置文件
- 多个处理容器共享工作队列
没有 Volume,这些场景将无法实现。Kubernetes volumes 为 Pod 中的容器提供了一种通过文件系统访问和共享数据的方式。与 Docker 卷不同,Kubernetes Volume 有明确的生命周期,可以比容器更持久。
Kubernetes volumes 可用于不同的目的,包括:基于 ConfigMap 或 Secret 填充配置文件、为 Pod 提供临时暂存空间、在同一 Pod 中的两个不同容器之间共享文件系统、在两个不同 Pod 之间共享文件系统(即使这些 Pod 运行在不同节点上)、持久化存储数据以便在 Pod 重启或替换后数据仍然可用、基于 Pod 容器的详细信息向容器中运行的应用传递配置信息(例如:告诉 sidecar 容器 Pod 正在哪个命名空间中运行)、提供对不同容器镜像中数据的只读访问。
让我们通过具体场景来理解:
- 场景 1:容器内进程间数据共享
1 | 同一容器内 |
- 场景 2:同一 Pod 内容器间共享
1 | Pod: nginx-with-logger |
- 场景 3:跨 Pod 数据共享
1 | Node A Node B |
- 场景 4:持久化数据
1 | 数据库 Pod (版本 1) |
关于Volume结构的定义如下:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
Kubernetes 支持多种 Volume 类型,每种类型适用于不同的场景。让我们详细探讨最常用的类型:
emptyDir - 临时数据存储
核心特性:
- 初始内容为空(因此得名 emptyDir)
- emptyDir 卷在 Kubernetes 将 Pod 分配到节点时创建
- Pod 删除时,emptyDir 中的数据会被永久删除
- 容器崩溃不会导致数据丢失
emptyDir的结构定义如下:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
如下存储介质:
1 | # 默认使用节点磁盘 |
典型用例:编译过程中的临时文件,图片处理的中间结果,下载的临时文件,容器间数据共享等,如下:
用例 1:临时缓存
1 | apiVersion: v1 |
用例 2:容器间数据共享
1 | apiVersion: v1 |
hostPath - 宿主机路径挂载
核心概念: hostPath 允许 Pod 访问位于宿主机节点文件系统上的文件或目录,通常用于单节点测试。
重要警告:
1 | ⚠️ 安全风险: |
hostPath的Volume的结构定义如下:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
type 字段详解:
1 | volumes: |
type 可选值:
| Type | 说明 | 使用场景 |
|---|---|---|
"" |
不做任何检查(默认) | 快速测试 |
DirectoryOrCreate |
目录不存在则创建 | 需要确保目录存在 |
Directory |
必须存在且是目录 | 挂载已有目录 |
FileOrCreate |
文件不存在则创建 | 日志文件等 |
File |
必须存在且是文件 | 挂载配置文件 |
Socket |
必须是 UNIX socket | Docker socket 等 |
CharDevice |
必须是字符设备 | GPU 设备等 |
BlockDevice |
必须是块设备 | 裸块存储 |
实际用例:日志收集
1 | apiVersion: v1 |
configMap - 配置数据注入
核心概念: ConfigMap 允许你将配置数据与容器镜像解耦,实现配置的外部化管理。configMap 卷提供了向 Pod 注入配置数据的方法。 ConfigMap 对象中存储的数据可以被 configMap 类型的卷引用,然后被 Pod 中运行的容器化应用使用。
创建 ConfigMap 的多种方式,例如:从字面值创建,从文件创建,YAML 定义。
ConfigMap Volume结构的定义如下:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
使用 ConfigMap 的多种方式:如下是作为 Volume 挂载的示例:
1 | apiVersion: v1 |
如下是挂载特定键:
1 | apiVersion: v1 |
secret - 敏感数据管理
核心概念: Secret 用于存储和管理敏感信息,如密码、OAuth 令牌、SSH 密钥等。secret 卷用来给 Pod 传递敏感信息,例如密码。你可以将 Secret 存储在 Kubernetes API 服务器上,然后以文件的形式挂载到 Pod 中,无需直接与 Kubernetes 耦合。 secret 卷由 tmpfs(基于 RAM 的文件系统)提供存储,因此它们永远不会被写入非易失性(持久化的)存储器。
Secret Volume的结构定义如下:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
Secret 类型:
| 类型 | 说明 | 用途 |
|---|---|---|
Opaque |
任意用户定义数据(默认) | 密码、API 密钥 |
kubernetes.io/service-account-token |
ServiceAccount 令牌 | Kubernetes API 访问 |
kubernetes.io/dockercfg |
Docker 配置文件 | 私有镜像仓库认证 |
kubernetes.io/dockerconfigjson |
Docker config.json | 私有镜像仓库认证(新格式) |
kubernetes.io/basic-auth |
基本认证 | 用户名和密码 |
kubernetes.io/ssh-auth |
SSH 认证 | SSH 私钥 |
kubernetes.io/tls |
TLS 证书和密钥 | HTTPS/TLS |
bootstrap.kubernetes.io/token |
Bootstrap token | 节点引导 |
创建 Secret的方式有多种,例如:从字面值,从文件,从Docker 镜像仓库,从YAML(需要 base64 编码)
1 | apiVersion: v1 |
如下通过Volume 挂载:
1 | apiVersion: v1 |
结果:
1 | /etc/secrets/ |
这里也简单介绍一下ConfigMap和Secret两种Kubernetes资源对象的差异:
| 特性 | ConfigMap | Secret |
|---|---|---|
| 用途 | 存储非敏感配置数据(如 app.conf、feature flags、环境变量) | 存储敏感数据(如数据库密码、API 密钥、TLS 证书、OAuth Token) |
| 数据格式 | 普通字符串或文件 | 数据以 Base64 编码 存储(⚠️ 不是加密!) |
| 安全性 | 无特殊安全保护,明文存储 | 默认仅 Base64 编码(可被解码),但支持: • 启用 Encryption at Rest(需手动配置) • RBAC 控制访问权限 |
| 大小限制 | 建议 ≤ 1 MB(受 etcd 限制) | 同样建议 ≤ 1 MB(etcd 限制) |
| 挂载方式 | 可作为环境变量或 Volume 挂载到 Pod | 同样可作为环境变量或 Volume 挂载到 Pod |
| 更新行为 | Volume 挂载的 ConfigMap 支持自动更新(约 1 分钟延迟,需容器内应用重新读取) | Volume 挂载的 Secret 也支持自动更新(同上) |
| 默认权限(Volume 挂载) | 文件权限为 644(可读) |
文件权限为 644,但可通过 defaultMode 设置(如 400 更安全) |
| 是否加密传输 | 否(通过 API Server 明文传输) | 否(Base64 不是加密),但可通过 TLS 加密 API 通信(Kubernetes 默认启用) |
| 最佳实践 | 用于解耦配置与镜像,实现环境差异化 | 绝不将敏感信息硬编码在镜像或 ConfigMap 中;优先使用 Secret,并结合外部密钥管理(如 Vault、AWS Secrets Manager) |
downwardAPI - Pod 元数据注入
核心概念: Downward API 允许容器访问关于自身或集群的信息,而无需直接调用 Kubernetes API。downwardAPI 卷用于为应用提供 downward API 数据。 在这类卷中,所公开的数据以纯文本格式的只读文件形式存在。
DownwardAPI的Volume结构定义如下:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
从上面的定义可知,目前Downward API的Volume只支持访问Pod的: Selects a field of the pod: only annotations, labels, name, namespace and uid are supported.
如下是通过DownwardAPI Volume 来访问Pod的相关Metadata数据:
1 | apiVersion: v1 |
Projected Volumes(投射卷)
Projected Volume(投射卷) 是 Kubernetes 中一种特殊的卷类型,它允许你将多个不同来源的数据(如密钥、配置、令牌等)合并挂载到 Pod 中的同一个目录下,而无需为每个数据源分别挂载不同的目录。目前,以下类型的卷源可以被投射:
假设你的应用需要:
- 一个数据库密码(存放在 Secret 中),
- 一些配置参数(存放在 ConfigMap 中),
- 以及当前 Pod 的元信息(如 Pod 名称、命名空间,可通过 Downward API 获取)。
你可以使用一个 projected 卷,将这三者全部挂载到容器内的 /etc/config 目录下,每个数据源以独立的文件形式出现,这样既整洁又方便管理。如下:
1 | apiVersion: v1 |
这种机制体现了 Kubernetes “声明式配置”和“关注点分离”的设计哲学:数据来源各自管理,但可在运行时灵活组合。
Ephemeral Volume(临时卷)
在 Kubernetes 中,临时卷是一类生命周期与 Pod 绑定的存储卷。与 PersistentVolume(持久卷)不同,临时卷:
- 随 Pod 创建而创建,随 Pod 删除而自动销毁;
- 不保留数据跨 Pod 重启或重建;
- 通常不涉及 PersistentVolumeClaim(PVC)资源对象(但部分类型例外,见下文)。
其核心思想是:为 Pod 提供“用完即弃”的本地或临时存储,适用于不需要持久化但需要比内存更大空间或文件系统接口的场景。
临时卷不是一种单一的卷类型,而是一类卷的统称,主要包括:
- emptyDir:在 Pod 启动时为空,其存储空间来自 kubelet 的基础目录(通常位于节点的根磁盘)或 RAM(内存)。
- configMap, downwardAPI, secret:将不同类型的 Kubernetes 数据注入到 Pod 中。
- image:允许将容器镜像中的文件或制品(artifacts)直接挂载到 Pod 中。
- CSI ephemeral volumes:与上述卷类型类似,但由专门支持此功能的第三方 CSI 驱动提供。
- generic ephemeral volumes:通用临时卷可由第三方 CSI 存储驱动提供,也可由任何支持动态制备(dynamic provisioning)的其他存储驱动提供。有些 CSI 驱动是专为 CSI 临时卷而设计的,并不支持动态制备;这类驱动无法用于通用临时卷。
emptyDir、configMap、downwardAPI 和 secret 属于本地临时存储,由每个节点上的 kubelet 负责管理。这几类是独立的VolumeSource的,只是归类上被分类为Ephemeral Volume。
为什么还要搞出 “generic ephemeral volume” 这种写法?
这是 Kubernetes 为了统一模型而做的改进。传统 emptyDir 只能用节点本地存储。但有时你希望:临时用一块高性能云盘(比如 AWS io2),但用完自动删。如果用普通 PVC,你要手动创建/删除,麻烦且容易残留。所以引入 generic ephemeral volume:
- 写法上用
ephemeral字段(看起来像新类型) - 但底层仍走 PVC/PV 机制
- 生命周期自动绑定 Pod,无需手动管理
它本质上还是“ephemeral volume”的一种,只是实现方式更强大。我们看一下关于Ephemeral Volume的定义
1 | // Represents an ephemeral volume that is handled by a normal storage driver. |
例如下面示例,通过ephemeral volume申明,在Pod创建时,自动生成一个专属 PVC,使用storageClassName: "scratch-storage-class"动态创建PV,进行临时挂载使用,Pod销毁后,会自动删除对应的PVC,这和后面介绍的PV主的差异点。如果 PV 的回收策略是 Delete(默认),底层云盘也会被自动销毁。
1 | kind: Pod |
Persistent Volumes(持久卷)
PV & PVC
PersistentVolume(持久卷)子系统为用户和管理员提供了一套 API,将存储的提供方式与存储的使用方式解耦。为实现这一目标,我们引入了两种新的 API 资源:PersistentVolume(PV) 和 PersistentVolumeClaim(PVC)。
- PersistentVolume(PV) 是集群中的一块存储资源,可由管理员预先配置,也可通过 StorageClass(存储类) 动态创建。PV 是集群中的一种资源,就像节点(Node)一样。PV 与普通卷(Volume)类似,都是基于卷插件实现的,但其生命周期独立于任何使用它的 Pod。PV 对象封装了存储的具体实现细节,无论是 NFS、iSCSI,还是云服务商提供的特定存储系统。
- PersistentVolumeClaim(PVC) 是用户对存储资源的请求。它的作用类似于 Pod:Pod 消耗节点的计算资源(如 CPU 和内存),而 PVC 消耗 PV 存储资源。正如 Pod 可以请求特定数量的 CPU 和内存一样,PVC 也可以请求特定的存储容量和访问模式(例如:
ReadWriteOnce、ReadOnlyMany、ReadWriteMany或ReadWriteOncePod,详见 AccessModes)。
如下是PersistentVolume的源码结构定义:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
Static & Dynamic
Kubernetes 中针对持久卷(Persistent Volume, PV)有两种供应方式:静态供应(Static Provisioning)与动态供应(Dynamic Provisioning)。
静态(Static)
集群管理员会创建若干个持久卷(Persistent Volumes,PV)。这些 PV 包含了真实存储的详细信息,可供集群用户使用。它们存在于 Kubernetes API 中,并可供用户声明(PersistentVolumeClaim,PVC)绑定和使用。动态(Dynamic)
当管理员创建的所有静态 PV 都无法匹配用户的 PersistentVolumeClaim(PVC)时,集群可能会尝试为该 PVC 动态创建一个卷。这种动态创建基于存储类(StorageClass):PVC 必须明确请求某个存储类,并且管理员必须已创建并正确配置了该存储类,才能实现动态卷供应。如果 PVC 请求的存储类名称为 “”(空字符串),则表示该 PVC 主动禁用动态供应。
Binding
用户创建一个 PersistentVolumeClaim(PVC),指定所需的存储容量和特定的访问模式。控制平面中的一个控制循环会持续监控新创建的 PVC,尝试为其找到一个匹配的持久卷(PV)(如果存在),并将二者绑定在一起。
如果该 PVC 触发了动态供应,并因此创建了一个新的 PV,那么该控制循环一定会将这个新创建的 PV 绑定到该 PVC 上。
在静态供应的情况下,用户最终获得的存储容量至少等于其请求量,但实际分配的 PV 容量可能大于所请求的容量。
一旦绑定完成,PVC 与 PV 之间的绑定关系就是排他性的(exclusive)——无论该绑定是通过静态还是动态方式建立的。PVC 与 PV 的绑定是一对一的映射,通过一个名为 ClaimRef 的引用实现,该引用在 PV 和 PVC 之间建立双向绑定。
如果当前集群中不存在与某个 PVC 匹配的 PV,该 PVC 将无限期保持未绑定状态。一旦有匹配的 PV 被添加到集群中,该 PVC 就会自动与其绑定。例如,如果集群中只有多个 50Gi 的 PV,那么一个请求 100Gi 的 PVC 就无法被绑定;但当管理员或动态供应机制向集群加入一个 100Gi 的 PV 后,该 PVC 就会立即被绑定。
下图引用Kubernetes PVC,很好的解释了PV和PVC的设计:
如下是通过Static的方式,集群中先创建PV对象:
1 | # pv-nfs.yaml |
然后通过PVC创建当前Namespace的PV分配资源:
1 | # pvc-nfs.yaml |
最后在Pod中使用PVC来进行Volume的挂载:
1 | # pod-nfs.yaml |
Access Mode
ReadWriteOnce (RWO):单节点读写(最常见,如云硬盘)ReadOnlyMany (ROX):多节点只读(如共享配置)ReadWriteMany (RWX):多节点读写(如 NFS、EFS)ReadWriteOncePod (RWOP)(K8s 1.29+):单 Pod 读写(即使跨节点迁移,也确保同一时间只有一个 Pod 访问)
⚠️ 注意:能否多 Pod 共享,既要看 PVC 声明的模式,也要看底层存储是否支持。
如下是源码的定义:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
Reclaiming
当用户不再需要其存储卷时,可以删除 API 中的 PersistentVolumeClaim(PVC)对象,从而触发资源的回收流程。PersistentVolume(PV)的 回收策略(reclaim policy) 告诉集群:当该卷被释放(即对应的 PVC 被删除)后,应该如何处理该卷。目前,PV 的回收策略可以是以下三种之一:Retain(保留)、Recycle(回收) 或 Delete(删除)。
- Retain(保留)
Retain策略允许手动回收资源。当 PVC 被删除后,对应的 PV 仍然存在,并且该卷状态变为 “released”(已释放)。但此时它还不能被其他 PVC 使用,因为原用户的数据仍保留在卷上。
管理员可以通过以下步骤手动回收该卷:
- 删除该 PersistentVolume 对象(PV 在 Kubernetes 中被移除);
- 手动清理外部存储系统(如云盘、NFS 服务器等)上该存储资产中的数据;
- 手动删除外部存储系统中的该存储资产(可选);
- 如果希望复用同一存储资产,可以创建一个新的 PV,其配置指向同一个底层存储资源。
- Delete(删除)
对于支持 Delete回收策略的存储插件,删除操作会同时移除:
- Kubernetes 中的 PersistentVolume 对象;
- 以及外部基础设施中对应的底层存储资产(例如 AWS EBS 卷、GCE PD、Azure Disk 等)。
通过动态供应创建的卷会自动继承其 StorageClass 的回收策略,而 StorageClass 的默认回收策略通常为 Delete。因此,集群管理员应根据用户预期提前配置好 StorageClass 的 reclaimPolicy。否则,就需要在 PV 创建后手动编辑或打补丁(patch)来修改其回收策略(参见:Change the Reclaim Policy of a PersistentVolume)。
- Recycle(回收)
⚠️ :“Recycle” 回收策略已被弃用(deprecated)。推荐的做法是使用动态供应(dynamic provisioning)。
如果底层卷插件支持 “Recycle” 策略,它会对卷执行一次基础的数据清理(例如执行 rm -rf /thevolume/*),然后将该卷标记为可用,供新的 PVC 绑定。但由于该机制存在安全性和可靠性问题,Kubernetes 官方已不再维护,强烈建议避免使用。
如下是源码定义:
1 | // https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go |
PersistentVolume Source
持久卷(PersistentVolume,PV)类型通过插件方式实现。Kubernetes 目前支持以下插件:
- csi:容器存储接口(Container Storage Interface,CSI)
- fc:光纤通道(Fibre Channel,FC)存储
- hostPath:HostPath 卷(仅用于单节点测试;在多节点集群中无法正常工作;建议改用 local 卷)
- iscsi:iSCSI(基于 IP 的 SCSI)存储
- local:挂载在节点上的本地存储设备
- nfs:网络文件系统(Network File System,NFS)存储
以下类型的 PersistentVolume 已被弃用(deprecated)但仍然可用。除非您使用的是 flexVolume、cephfs 或 rbd,否则请安装对应的 CSI 驱动程序:
- awsElasticBlockStore:AWS Elastic Block Store(EBS)(从 v1.23 起默认启用迁移)
- azureDisk:Azure 磁盘(从 v1.23 起默认启用迁移)
- azureFile:Azure 文件(从 v1.24 起默认启用迁移)
- cinder:Cinder(OpenStack 块存储)(从 v1.21 起默认启用迁移)
- flexVolume:FlexVolume(从 v1.23 起弃用,无迁移计划,也无移除支持的计划)
- gcePersistentDisk:GCE 持久磁盘(从 v1.23 起默认启用迁移)
- portworxVolume:Portworx 卷(从 v1.31 起默认启用迁移)
- vsphereVolume:vSphere VMDK 卷(从 v1.25 起默认启用迁移)
旧版本 Kubernetes 还曾支持以下“内建(in-tree)”的 PersistentVolume 类型,但已在后续版本中彻底移除:
- cephfs(从 v1.31 起不再可用)
- flocker:Flocker 存储(从 v1.25 起不再可用)
- glusterfs:GlusterFS 存储(从 v1.26 起不再可用)
- photonPersistentDisk:Photon 控制器持久磁盘(从 v1.15 起不再可用)
- quobyte:Quobyte 卷(从 v1.25 起不再可用)
- rbd:Rados 块设备(RBD)卷(从 v1.31 起不再可用)
- scaleIO:ScaleIO 卷(从 v1.21 起不再可用)
- storageos:StorageOS 卷(从 v1.25 起不再可用)
Kubernetes 最初将所有存储驱动代码直接编译进核心代码库(称为 “in-tree” 驱动)。这种方式带来诸多问题:
- 核心代码臃肿;
- 存储厂商更新需等待 Kubernetes 发布周期;
- 安全和稳定性风险高。
为解决这些问题,Kubernetes 引入了 CSI(Container Storage Interface)标准,允许存储厂商以独立插件形式提供驱动,无需修改 Kubernetes 核心代码。
因此,社区启动了 “in-tree 驱动迁移到 CSI” 的长期计划。
这些是仍被官方支持且推荐使用的类型:
csi:唯一推荐的现代存储扩展方式。所有新存储集成都应通过 CSI 实现。- **
fc、iscsi、nfs、local**:属于基础、通用协议,Kubernetes 仍保留其 in-tree 支持,但未来也可能逐步迁移或仅维护。 hostPath:仅用于单节点开发/测试(如 Minikube),绝不能用于生产多节点集群,因为数据只存在于某一个节点上,Pod 调度到其他节点将无法访问。
✅ 建议:生产环境应优先使用
csi+ 对应云厂商或存储系统的 CSI 驱动。
Storage Classes(存储类)
StorageClass 为集群管理员提供了一种方式,用于描述其所提供的存储类型(或“类别”)。不同的存储类可能对应不同的服务质量(QoS)等级、备份策略,或由集群管理员定义的任意策略。Kubernetes 本身不对“存储类”代表什么做任何假设或限制。
StorageClass 对象的名称具有实际意义,用户正是通过这个名称来请求特定的存储类。管理员在首次创建 StorageClass 对象时,会设定其名称及其他参数。
前面在PV一章节中我们知道,Kubernetes 中针对持久卷(Persistent Volume, PV)有两种供应方式:静态供应(Static Provisioning)与动态供应(Dynamic Provisioning)。这里StorageClass就是提供Dynamic Provision的实现方式。相对于静态PV的使用,StorageClass实现了:
- 用户只需声明“我要什么”(通过 PVC 指定 StorageClass 名称);
- Kubernetes 自动创建 PV(调用 provisioner);
- 完全按需分配,零闲置,弹性伸缩;
作为管理员,你可以指定一个默认的 StorageClass,用于处理那些未明确请求特定存储类的 PVC。更多细节:PersistentVolumeClaim concept.
StorageClass的结构定义如下:
1 | https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/storage/v1/types.go |
每个 StorageClass 对象的关键字段主要包括:
provisioner:指定使用哪个卷插件来创建 PV
parameters:传递给 provisioner 的参数
reclaimPolicy:PV 的回收策略(Delete 或 Retain)
allowVolumeExpansion:是否允许扩容
volumeBindingMode
Immediate:PVC 创建时立即绑定和供应WaitForFirstConsumer:延迟绑定,直到 Pod 被调度
如下是StroageClass示例,运行在 AWS EKS,已安装 Amazon EBS CSI Driver
1 | # sc-ebs.yaml |
创建 PVC,引用 StorageClass
1 | # pvc-ebs.yaml |
创建Pod,触发动态PV的创建和绑定:
1 | # pod-ebs.yaml |
设置默认 StorageClass
1 | apiVersion: storage.k8s.io/v1 |
常见云厂商的 StorageClass
AWS EBS:
1 | apiVersion: storage.k8s.io/v1 |
GCP Persistent Disk:
1 | apiVersion: storage.k8s.io/v1 |
Azure Disk:
1 | apiVersion: storage.k8s.io/v1 |
下图引用,显示了静态PV和动态StorageClass的设计上的差异:
让Claude帮我生成了,静态 PV和动态 StorageClass的差异对比如下:
| 对比维度 | 静态 PV | 动态 StorageClass | 详细说明 |
|---|---|---|---|
| 工作原理 | 管理员预先手动创建一批 PV 放在”池子”里,用户创建 PVC 时从池子里挑选匹配的 | 管理员只需定义存储的”配方”,用户创建 PVC 时系统自动”生产”新的 PV | 类比:PV=仓库现货,SC=生产线 |
| PV与PVC绑定关系 | 一对一,第一个 PVC 绑定后整个 PV 被独占,即使只用了一部分容量 | 一对一,但 PV 是按 PVC 请求的精确大小创建的,没有浪费 | 关键:两者都是 1:1 绑定,但 SC 不浪费 |
| 资源浪费问题 | 严重:PVC 请求 10Gi 可能绑定到 50Gi 的 PV,浪费 40Gi;预创建的 PV 没人用也占用资源 | 几乎没有:请求多少创建多少,不用不创建 | PV 可能浪费 30-50% 的存储 |
| 管理 100 个应用的工作量 | 需要写 100 个 PV 的 YAML 文件,每个都要指定不同的名字、路径、后端存储位置等,然后逐个 kubectl apply | 只需写 1 个 StorageClass 的 YAML,用户自己创建 PVC,系统自动生成 100 个 PV | 工作量差异:100 倍 |
| PV 用尽时的处理 | 用户创建 PVC 后一直 Pending,管理员收到告警,登录服务器手动创建新 PV,通常需要 10-30 分钟人工介入 | 自动创建新 PV,通常 30 秒-2 分钟完成,无需人工 | 响应速度差异:分钟级 vs 秒级 |
| 容量精确度 | 不精确:你创建 10Gi、20Gi、50Gi 的 PV,但用户可能需要 15Gi,只能用 20Gi 的(浪费 5Gi) | 精确:用户要 15Gi 就创建 15Gi | 资源匹配度:SC 接近 100% |
| 跨可用区问题 | 手动处理:你必须为每个可用区创建 PV,且要确保 Pod 调度到正确的区域,容易出错 | 自动处理:设置 WaitForFirstConsumer 后,系统确保 PV 在 Pod 所在的可用区创建 | AWS EBS 等场景下 PV 方式极易出错 |
Volume Attributes Classes(卷属性类)
VolumeAttributesClass 是 Kubernetes 1.29+ 引入的特性,允许在运行时修改 PV 的属性,而无需重新创建卷。
Volume Attributes Classes 是一个相对较新的功能,在 Kubernetes v1.34 中默认开启。该功能旨在解耦存储类(StorageClass)中与驱动特定属性的绑定,使用户能够更灵活地定义存储卷的行为特性,而无需为每种属性组合创建多个 StorageClass。
在 Volume Attributes Classes 引入之前,Kubernetes 使用 StorageClass 来定义动态配置的存储参数(比如 type: ssd、iopsPerGB: "10" 等)。这些参数是通过 parameters 字段传递给 CSI(Container Storage Interface)驱动的。
这里存在的问题在于:
- 每当用户需要一种新的存储参数组合(如不同的 IOPS、吞吐量、加密策略等),就必须创建一个新的 StorageClass。
- 这导致 StorageClass 数量爆炸式增长,难以管理。
- 存储管理员和应用开发者职责耦合严重。
为了解决这个问题,Kubernetes 引入了 VolumeAttributesClass(简称 VAC),将“存储后端能力”(由 StorageClass 定义)与“卷的具体属性”(由 VolumeAttributesClass 定义)分离。
VolumeAttributesClass 是一种新的 Kubernetes API 资源,用于定义一组传递给 CSI 驱动的卷属性(volume attributes)。这些属性原本是写在 StorageClass 的 parameters 中的,现在可以单独提取出来。
1 | apiVersion: storage.k8s.io/v1alpha1 |
在 PersistentVolumeClaim (PVC) 中,你可以通过 volumeAttributesClassName 字段引用一个 VolumeAttributesClass:
1 | apiVersion: v1 |
这样拆分组合后:
storageClassName:决定使用哪个 CSI 驱动和底层存储池(如 AWS EBS、GCP PD 等)。volumeAttributesClassName:决定该卷的具体配置(如IOPS、加密、备份策略等)。
这样,同一个 StorageClass 可以配合多个 VolumeAttributesClass 使用,极大提升了灵活性。
- ✅ 解耦:存储基础设施(StorageClass)与应用需求(VolumeAttributesClass)分离。
- ✅ 复用性:一个 StorageClass 可被多个不同属性的卷复用。
- ✅ 简化管理:减少 StorageClass 的数量,提升可维护性。
- ✅ 权限控制:可对 VolumeAttributesClass 设置 RBAC,让开发者只能使用预定义的属性组合,而不能随意指定底层参数。
二者配合使用,实现“基础设施与策略分离”的现代云原生理念。
Dynamic Volume Provisioning(动态卷供应)
动态卷供应允许按需自动创建存储卷,无需管理员预先配置。动态卷制备的实现基于 storage.k8s.io API 组中的 StorageClass API 对象。
这种设计确保了终端用户无需关心存储是如何创建的复杂细节,同时仍能从多个存储选项中进行选择。
在 Kubernetes 中,动态卷制备(Dynamic Volume Provisioning) 是一种自动化机制:
当用户创建一个 PersistentVolumeClaim(PVC),而系统中没有现成的 PersistentVolume(PV) 与之匹配时,Kubernetes 会根据 PVC 所指定的 StorageClass,自动调用对应的存储插件(provisioner),在底层存储系统(如 AWS EBS、GCP PD、Ceph、NFS 等)中创建一个新的存储卷,并自动生成一个对应的 PV 对象,将其绑定到该 PVC。
整个过程完全无需人工干预。动态制备的核心表达其实就是Storage Class中的制备器(provisioner),用于创建存储卷,以及在制备时传递给该插件的一组参数。如下是不同云厂商的制备器及相关参数:
AWS EBS:
1 | apiVersion: storage.k8s.io/v1 |
GCP Persistent Disk:
1 | apiVersion: storage.k8s.io/v1 |
动态制备这里,也正好可以解释,为什么需要 CSI?在 CSI 出现之前,Kubernetes 内置了各种云存储的代码(in-tree 插件),导致:
- 代码臃肿;
- 云厂商更新慢;
- 安全风险高。
CSI 把存储逻辑完全外置,Kubernetes 只需实现标准接口,存储厂商自己维护插件。这就是为什么你现在看到的都是 ebs.csi.aws.com,而不是旧的 kubernetes.io/aws-ebs。制备的流程如下:
- 你写 PVC → 触发动态制备;
- Kubernetes 控制器调用 CSI Controller → 在云平台创建实际存储卷(买盘);
- Kubernetes 自动生成 PV 并绑定到 PVC(绑定);
- Pod 调度后,kubelet 调用 CSI Node Plugin → 将云盘挂载到节点并注入容器(插盘)。
🌟 所有这一切,你只需要写一个 PVC。剩下的,Kubernetes + CSI 插件全自动完成。这就是云原生存储的魔力!✨
Volume Snapshots(卷快照)
在 Kubernetes 中,VolumeSnapshot 表示存储系统中某个卷在某一时刻的快照(snapshot)。
与 PersistentVolume(PV)和 PersistentVolumeClaim(PVC)用于为用户和管理员提供卷的方式类似,Kubernetes 也提供了 VolumeSnapshotContent 和 VolumeSnapshot 这两个 API 资源,用于为用户和管理员创建卷的快照。
- VolumeSnapshotContent 是由管理员在集群中从某个卷创建的快照。它是一个集群级别的资源,类似于 PersistentVolume。
- VolumeSnapshot 是用户发起的对某个卷创建快照的请求,其作用类似于 PersistentVolumeClaim。
此外,VolumeSnapshotClass 允许你为快照指定不同的属性。这些属性可能因同一存储系统上对同一卷创建的不同快照而异,因此无法通过 PVC 所使用的 StorageClass 来表达。
卷快照为 Kubernetes 用户提供了一种标准化的方式,可以在不创建全新卷的前提下,复制某个卷在特定时间点的内容。例如,数据库管理员可以在执行编辑或删除操作之前,使用快照功能备份数据库。
使用卷快照时需要注意的关键点:
- VolumeSnapshot、VolumeSnapshotContent 和 VolumeSnapshotClass 是自定义资源(CRDs),不属于 Kubernetes 核心 API。
- 换句话说,这些资源需要额外安装才能使用,不是 Kubernetes 默认就有的。
- 卷快照功能仅支持 CSI(Container Storage Interface)驱动。
- 传统的 in-tree(内置)存储插件不支持快照功能。只有实现了 CSI 接口的存储驱动才可能支持快照。
- 部署卷快照功能需要两个关键组件:
- 快照控制器(Snapshot Controller):部署在控制平面(control plane)中,负责监听 VolumeSnapshot 和 VolumeSnapshotContent 对象,并管理 VolumeSnapshotContent 的创建与删除。
- csi-snapshotter(Sidecar 容器):与 CSI 驱动一起部署,监听 VolumeSnapshotContent 对象,并通过 CSI 接口调用底层存储系统的
CreateSnapshot和DeleteSnapshot操作。
- 存在一个验证 Webhook 服务器(Validating Webhook Server):
- 用于对快照对象进行更严格的合法性校验。
- 该 Webhook 应由 Kubernetes 发行版(如 RKE、OpenShift、EKS 等)负责安装,与快照控制器和 CRD 一起部署,而不是由 CSI 驱动提供。
- 所有启用快照功能的 Kubernetes 集群都应安装此 Webhook。
- 并非所有 CSI 驱动都实现了快照功能。
- 只有那些明确支持卷快照的 CSI 驱动才会使用
csi-snapshotter。 - 用户应查阅具体 CSI 驱动的文档,确认其是否支持快照。
- 只有那些明确支持卷快照的 CSI 驱动才会使用
- CRD 和快照控制器的安装由 Kubernetes 发行版负责。
- 作为用户,通常不需要手动安装这些组件,但需确保集群管理员已正确部署。
- 高级用例:多卷组快照(Group Snapshots)
- 如果你需要对多个卷同时创建快照(例如保证数据库和日志卷的一致性),可以参考 external CSI Volume Group Snapshot 项目文档。
关键概念解释:
| 概念 | 类比对象 | 作用 | 谁创建/管理 |
|---|---|---|---|
| VolumeSnapshot | PVC | 用户说:“我要给这个卷拍个快照” | 用户(通过 YAML 声明) |
| VolumeSnapshotContent | PV | 实际的快照资源,代表存储后端真正创建出来的快照 | 由 snapshot controller 自动创建 |
| VolumeSnapshotClass | StorageClass | 定义快照的策略(例如:每天保留几个、是否增量、删除策略等) | 管理员预先创建 |
| snapshot controller | PV Controller | 负责把用户的 VolumeSnapshot 请求转成真正的 VolumeSnapshotContent | 集群管理员部署 |
| csi-snapshotter | csi-provisioner | 侧车容器,真正去调用存储后端的快照接口 | 与 CSI 驱动一起部署 |
如下示例,创建 VolumeSnapshotClass:
1 | apiVersion: snapshot.storage.k8s.io/v1 |
创建快照:
1 | apiVersion: snapshot.storage.k8s.io/v1 |
从快照恢复:
1 | apiVersion: v1 |
CSI Volume Cloning(CSI 卷克隆)
卷克隆是指基于一个已有的 PVC(PersistentVolumeClaim),创建一个新的 PVC,其底层 PV(PersistentVolume)包含与源卷完全相同的数据副本。
- 克隆操作是存储系统原生支持的(由 CSI 驱动调用存储后端的克隆 API)。
- 克隆不是快照(Snapshot),但通常底层依赖快照机制实现。
- 克隆完成后,新卷与源卷完全独立:修改新卷不会影响源卷,反之亦然。
如下是克隆卷的使用:
1 | apiVersion: v1 |
如下是Volume Cloning和Volume Snapshot的差异对比:
| 特性 | Volume Cloning | Volume Snapshot |
|---|---|---|
| 目的 | 快速创建一个可读写的新卷 | 创建一个只读的时间点副本(用于备份/恢复) |
| 是否可直接使用 | ✅ 可直接挂载为 PVC | ❌ 不能挂载,需通过它创建新 PVC |
| 数据独立性 | ✅ 完全独立 | ❌ 快照本身不可写 |
| 底层实现 | 可能使用快照,也可能使用存储系统的克隆 API | 依赖存储系统的快照功能 |
| 用户操作 | 直接在 PVC 的 dataSource 中引用另一个 PVC |
先创建 VolumeSnapshot,再用它创建 PVC |
