对于任何一个需要在真实世界中运行的应用来说,数据存储都是一个绕不开的话题。幸运的是,Kubernetes 提供了一个功能强大且成熟的存储子系统,名为“持久化卷(Persistent Volume)”,专门用来解决这个问题。

首先要明白一点,Kubernetes 能够支持各式各样的存储类型,来源也五花八门。比如,无论是 iSCSI、SMB、NFS 这样的传统网络存储,还是云服务商提供的对象存储,都可以接入到 Kubernetes 中。 但无论这些存储的本质是什么,一旦它们被接入到 Kubernetes 集群,我们都统一称之为“卷(Volume)”。 例如,一个来自 Azure 的文件存储资源,在 Kubernetes 里就是一个卷;同样,一个来自 AWS 弹性块存储(EBS)的块设备,也是一个卷。简而言之,Kubernetes 集群里的所有存储,都叫卷。
让我们通过一张图来理解这个宏观架构。
在图的上方,是各种各样的外部存储系统。它们可能是你公司数据中心里像 EMC、NetApp 这样的传统企业级存储设备,也可能是像 AWS EBS 或 GCE PD 这样的云存储服务。只要有一个对应的插件,这些外部存储资源就能被 Kubernetes “看作”是自己的卷。
图的中间是插件层。简单来说,它就像一个万能转换插头,负责将外部存储和 Kubernetes 连接起来。未来的趋势是,所有的插件都会基于容器存储接口(CSI) 来开发。CSI 是一个开放标准,为存储插件提供了一个清晰、统一的接口。对于开发者而言,CSI 屏蔽了 Kubernetes 内部复杂的存储实现细节,让他们可以更轻松地开发独立于 Kubernetes 核心代码的插件。
在 CSI 出现之前,所有的存储插件代码都必须作为 Kubernetes 主代码库的一部分(称为"in-tree")。这意味着插件的更新和修复都得跟着 Kubernetes 的发布周期走,这对于插件开发者和 Kubernetes 维护者来说都是一场噩梦。 而现在有了 CSI,存储厂商不再需要开源他们的插件代码,并且可以按照自己的节奏发布更新和修复 Bug。
在图的下方,是 Kubernetes 的持久化卷子系统。这是一套 API 对象,应用程序通过它们来"消费"存储。从高层次理解,PersistentVolume (PV) 负责将外部存储映射到集群内部, 而 PersistentVolumeClaim (PVC) 就像一张"使用券",授权应用程序(Pod)去使用某个 PV。
让我们来看一个简单的例子。
假设我们的 Kubernetes 集群运行在 AWS 上,AWS 管理员已经创建好了一个 25GB 大小的 EBS 卷,名为 “ebs-vol”。现在,Kubernetes 管理员可以创建一个名为 “k8s-vol” 的 PV,通过 kubernetes.io/aws-ebs 这个插件链接到 “ebs-vol”。
这个 PV 其实就是那个外部 EBS 卷在 Kubernetes 集群里的一个“代理”或“代表”。最后,一个 Pod 可以通过创建一个 PVC 来“申请”使用这个 PV,一旦申请成功,就可以把这个卷挂载到自己内部开始使用了。
这里有两点值得注意:
Kubernetes 可以从非常广泛的外部系统中获取存储资源。这些系统通常是云服务商的原生服务,如 AWS Elastic Block Store 或 Azure Disk,但也可以是提供 iSCSI 或 NFS 卷的传统本地存储阵列。关键点在于,Kubernetes 的存储来源非常多样。
当然,这里也会有一些显而易见的限制。例如,如果你的 Kubernetes 集群运行在微软 Azure 上,你就不可能使用 AWS EBS 的存储供应插件。
CSI 是 Kubernetes 存储版图中非常重要的一块。不过,除非你是一名编写存储插件的开发者,否则你可能不会经常直接和它打交道。
它是一个开源项目,旨在定义一个标准化的接口,让存储能够在多个容器编排器(如 Kubernetes、Docker Swarm)中以统一的方式被使用。在 Kubernetes 的世界里,CSI 是编写存储驱动(插件)的首选方式,这意味着插件代码不再需要存在于 Kubernetes 的核心代码库中。
对于日常的管理者来说,你与 CSI 的唯一交集可能就是在 YAML 配置文件中引用正确的插件名称。
从日常运维的角度来看,这里是你花费大部分时间配置和交互 Kubernetes 存储的地方。这个子系统的三大核心资源是:
我们可以把它们的关系想象成一个图书馆的借书流程:
让我们通过一个手动创建 PV 和 PVC 的例子来理解这个过程。 假设你有一个运行在 Google Cloud 上的 Kubernetes 集群,并且已经在 GKE 中预先创建好了一个名为 “uber-disk” 的 10GB SSD 磁盘。
第一步:创建 PV
首先,我们需要创建一个 PV 对象,来代表这个已经存在的 GKE 磁盘。
|# gke-pv.yml apiVersion: v1 kind: PersistentVolume metadata: name: pv1 # PV 的名字 spec: accessModes: - ReadWriteOnce # 访问模式:只允许被单个节点读写挂载 storageClassName: test # 所属的存储类别,用于后续匹配 capacity:
到目前为止,我们看到的都是手动创建 PV 和 PVC 的过程。这对于管理少量存储是可行的,但如果在一个大型环境中,管理员不可能为每一个存储需求都手动创建一个 PV。我们需要一种更动态、更自动化的方式。 这就是 StorageClass 发挥作用的地方。
顾名思义,StorageClass 允许你定义不同“类别”或“等级”的存储。你可以根据后端存储系统的能力来定义这些类别。例如,你可以创建一个 fast 类(使用 SSD 硬盘)、一个 slow 类(使用普通硬盘)和一个 encrypted 类(提供加密功能)。
当一个 StorageClass 被创建后,它就像一个时刻待命的供应机器人。它会持续观察有没有新的 PVC “点名”要使用它。一旦有匹配的 PVC 出现,StorageClass 就会立即指示后端的存储插件,动态地创建一个新的存储卷,并自动为这个新卷创建一个对应的 PV,然后将 PV 和 PVC 绑定。整个过程无需人工干预。
让我们来看一个 StorageClass 的 YAML 定义:
|# google-sc.yml kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: slow # 这个 SC 的名字叫 slow provisioner: kubernetes.io/gce-pd # 指定使用哪个存储插件 parameters: type: pd-standard # 告诉插件,需要标准类型的磁盘(非 SSD) reclaimPolicy: Retain # 动态创建的 PV 的回收策略
这个 YAML 文件定义了一个名为 slow 的 StorageClass,它使用 GCE 的标准磁盘。
现在,使用 StorageClass 的流程就变得简单多了:
storageClassName 为 slow。整个过程中,管理员不再需要关心具体的 PV 创建细节。
现在,让我们通过一个完整的 demo 来体验 StorageClass 的强大。
第一步:清理环境 (如果需要)
如果你跟着之前的例子操作实践了,那么你需要先删除旧的资源。
|$ kubectl delete pod volpod $ kubectl delete pvc pvc1 $ kubectl delete pv pv1
第二步:创建 StorageClass
我们使用刚才介绍的 google-sc.yml 文件来创建一个名为 slow 的 StorageClass。
在这一节中,我们学习了 Kubernetes 强大而灵活的存储子系统。 核心概念是,Kubernetes 通过插件(CSI 是首选) 来集成各种外部存储系统。PV 是外部存储在集群内的代表,而 PVC 是应用申请使用存储的“凭证”。
为了实现自动化,StorageClass 扮演了动态供给者的角色。它使得应用开发者可以按需申请存储,而无需关心后端存储的复杂细节,极大地提高了效率和可扩展性。
这个 YAML 文件定义了一个名为 pv1 的 PV。其中 accessModes 定义了卷的挂载方式,ReadWriteOnce (RWO) 意味着这个卷一次只能被一个节点以读写模式挂载。persistentVolumeReclaimPolicy 决定了当绑定的 PVC 被删除后,这个 PV 和它背后的真实存储何去何从。Retain 策略表示全部保留,数据不会丢失。
PV 的回收策略还有一个 Delete 选项。这个选项非常危险,它会在 PVC 被删除时,不仅删除 Kubernetes 中的 PV 对象,还会删除后端存储上对应的数据卷!请务必谨慎使用。
现在,我们应用这个文件来创建 PV:
|$ kubectl apply -f gke-pv.yml persistentvolume/pv1 created
第二步:创建 PVC
有了代表存储的 PV,应用(Pod)并不能直接使用它。应用需要通过 PVC 来“申请”使用权。
|# gke-pvc.yml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc1 # PVC 的名字 spec: accessModes: - ReadWriteOnce # 访问模式必须与 PV 匹配 storageClassName: test # 存储类别必须与 PV 匹配 resources: requests: storage: 10Gi # 请求的容量,必须小于等于 PV 的容量
要成功绑定,PVC 的 accessModes, storageClassName 和 resources.requests.storage 都必须与目标 PV 的设定相匹配或兼容。
现在,我们创建这个 PVC:
|$ kubectl apply -f gke-pvc.yml persistentvolumeclaim/pvc1 created
创建成功后,Kubernetes 会自动将这个 PVC 与我们之前创建的 pv1 进行绑定。你可以通过 kubectl get pvc pvc1 命令看到它的状态(STATUS)变为 Bound。
第三步:在 Pod 中使用 PVC
最后一步,就是在 Pod 的定义中引用这个 PVC,把它作为数据卷挂载到容器内部。
|# volpod.yml apiVersion: v1 kind: Pod metadata: name: volpod spec: volumes: - name: data # 定义一个卷,名字叫 data persistentVolumeClaim: claimName: pvc1 # 声明使用我们刚刚创建的 pvc1 containers: - name: ubuntu-ctr image: ubuntu:latest command: ["/bin/bash", "-c", "sleep 60m"] volumeMounts: - mountPath: /data # 将名为 data 的卷挂载到容器的 /data 目录下 name: data
部署这个 Pod 后,容器内的 /data 目录就会直接读写到我们最初在 GKE 上创建的那个 “uber-disk” 磁盘上了。
|$ kubectl apply -f google-sc.yml storageclass.storage.k8s.io/slow created
第三步:创建 PVC
现在,我们创建一个 PVC,明确要求使用 slow 这个 StorageClass。
|# google-pvc.yml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pv-ticket spec: accessModes: - ReadWriteOnce storageClassName: slow # 点名要使用 slow 这个 SC resources: requests: storage: 25Gi # 请求 25GB 的空间
部署它:
|$ kubectl apply -f google-pvc.yml persistentvolumeclaim/pv-ticket created
几乎在创建完成后,你立刻去查看 PVC 和 PV,会发现一个奇妙的事情:
|# 查看 PVC,状态已经变成 Bound $ kubectl get pvc pv-ticket NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS pv-ticket Bound pvc-881a23... 25Gi RWO slow # 查看 PV,一个同名的 PV 已经被自动创建出来了! $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM pvc-881a23... 25Gi RWO Retain Bound
我们没有手动创建 PV,但一个容量为 25GB 的 PV 被 slow 这个 StorageClass 自动创建并绑定好了。
第四步:创建 Pod 使用存储
最后,我们创建一个 Pod 来使用这个动态生成的存储。
|# google-pod.yml apiVersion: v1 kind: Pod metadata: name: class-pod spec: volumes: - name: data persistentVolumeClaim: claimName: pv-ticket # 引用我们刚才创建的 PVC containers: - name: ubuntu-ctr image: ubuntu:latest command: ["/bin/bash", "-c", "sleep 60m"] volumeMounts: - mountPath: /data name: data
最后,我们部署这个 Pod,它就能成功挂载并使用那块 25GB 的 GCE 标准磁盘了。