参考:Volumes | KubernetesPersistent Volumes | KubernetesKubernetes 基础入门实战

简单来说,存储卷是定义在Pod资源之上可被其内部的所有容器挂载的共享目录,该目录关联至宿主机或某外部的存储设备之上的存储空间,可由Pod内的多个容器同时挂载使用。Pod存储卷独立于容器自身的文件系统,因而也独立于容器的生命周期,它存储的数据可于容器重启或重建后继续使用。

存储卷并非Kubernetes上一种独立的API资源类型,它隶属于Pod资源,且与所属的特定Pod对象有着相同的生命周期。

PV(PersistentVolume)与PVC(PersistentVolumeClaim)就是在用户与存储服务之间添加的一个中间层,管理员事先根据PV支持的存储卷插件及适配的存储方案(目标存储系统)细节定义好可以支撑存储卷的底层存储空间,而后由用户通过PVC声明要使用的存储特性来绑定符合条件的最佳PV定义存储卷,从而实现存储系统的使用与管理职能的解耦,大大简化了用户使用存储的方式。

前置知识

关于卷(Volume)

Container 中的文件在磁盘上是临时存放的,这给 Container 中运行的较重要的应用程序带来一些问题。 问题之一是当容器崩溃时文件丢失。 kubelet 会重新启动容器,但容器会以干净的状态(clean state)重启。 第二个问题会在同一 Pod 中运行多个容器并共享文件时出现。 Kubernetes 卷(Volume)这一抽象概念能够解决这两个问题。

Docker 也有卷(Volume) 的概念,但对它只有少量且松散的管理。 Docker 卷是磁盘上或者另外一个容器内的一个目录。 Docker 提供卷驱动程序,但是其功能非常有限。

Kubernetes 支持很多类型的卷。 Pod 可以同时使用任意数目的卷类型。 临时卷类型的生命周期与 Pod 相同,但持久卷可以比 Pod 的存活期长。 当 Pod 不再存在时,Kubernetes 也会销毁临时卷;不过 Kubernetes 不会销毁持久卷。 对于给定 Pod 中任何类型的卷,在容器重启期间数据都不会丢失。

卷的核心是一个目录,其中可能存有数据,Pod 中的容器可以访问该目录中的数据。 所采用的特定的卷类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放的内容。

概述

kubernetes 抽象出来了几个 Resource 概念:

  • PersistentVolumeClaim:持久卷申领,简称 PVC,用于描述应用对存储资源的需求,比如大小等。
  • PersistentVolume:持久卷,简称 PV,提供具体的存储,跟具体环境有关。比如 Hostpath、GCE PD、Azure Disk、AWS EBS 等资源。
  • StorageClass:存储类。PV 是静态创建好的存储资源,StorageClass 是描述了生成 PV 的方法,可以用于动态地创建 PV,也与具体环境有关。

可以看到,Kubernetes 的 Volume 架构是分层的,将存储的使用者与提供者解耦。这样更贴合一般的使用场景,因为一般的环境中,会有不同的使用者存在,主要有以下两类:

  • 管理者:比如运维人员、系统管理员,负责具体的底层设施的维护。
  • 使用者:普通的开发测试人员等,是这些资源的使用者。

角色的分工就对应着资源的分工。前者管理比较重量级的 PV / StorageClass 等资源,配置参数,调整大小等,后者只需提出存储需求。

同时要注意的是,PersistentVolume / StorageClass 是一个 cluster 级别的资源,不属于任何的 namespace,而 PVC 属于对应的 namespace。这也对应着上面的分工,比较重量级的、与环境关系密切的,属于 cluster 级别的资源,而与环境关系不大,轻量级的则属于 namespace 级别的资源,也方便迁移。

配合这种 Volume 架构的最常见的 Workload 是 StatefulSet,与 DaemonSet 和 Deployment 不同的是,StatefulSet 在设计之初就是面向有状态服务的,它有以下特点:

  • Pod 的启动和停止都是有序的,不是并行的
  • 每个 Pod 有自己的标示。像 DaemonSet 和 Deployment 管理的 Pod 都以随机数做后缀,但 StatefulSet 是以 -0,-1 为后缀。
  • 搭配 Headless Service,每个 Pod 也有自己的固定的域名,可以单独访问。

有状态服务大多都是不对称架构(不同的实例的角色不一样),StatefulSet 的特性就是为了解决此问题而生。

PV

PV(PersistentVolume)是由集群管理员于全局级别配置的预挂载存储空间,它通过支持的存储卷插件及给定的配置参数关联至某个存储系统上可用数据存储的一段空间,这段存储空间可能是Ceph存储系统上的一个存储映像、一个文件系统(CephFS)或其子目录,也可能是NFS存储系统上的一个导出目录等。PV将存储系统之上的存储空间抽象为Kubernetes系统全局级别的API资源,由集群管理员负责管理和维护。

目前,存储大小是可以设置和请求的唯一资源。 未来可能会包含 IOPS、吞吐量等属性。

类型

PV 持久卷是用插件的形式来实现的。Kubernetes 目前支持以下插件:

  • awsElasticBlockStore - AWS 弹性块存储(EBS)
  • azureDisk - Azure Disk
  • azureFile - Azure File
  • cephfs - CephFS volume
  • csi - 容器存储接口 (CSI)
  • fc - Fibre Channel (FC) 存储
  • gcePersistentDisk - GCE 持久化盘
  • glusterfs - Glusterfs 卷
  • hostPath - HostPath 卷 (仅供单节点测试使用;不适用于多节点集群; 请尝试使用 local 卷作为替代)
  • iscsi - iSCSI (SCSI over IP) 存储
  • local - 节点上挂载的本地存储设备
  • nfs - 网络文件系统 (NFS) 存储
  • portworxVolume - Portworx 卷
  • rbd - Rados 块设备 (RBD) 卷
  • vsphereVolume - vSphere VMDK 卷

Volume Mode

针对 PV 持久卷,Kubernetes 支持两种卷模式(volumeModes):Filesystem(文件系统)Block(块)volumeMode 是一个可选的 API 参数。 如果该参数被省略,默认的卷模式是 Filesystem

volumeMode 属性设置为 Filesystem 的卷会被 Pod 挂载(Mount) 到某个目录。 如果卷的存储来自某块设备而该设备目前为空,Kuberneretes 会在第一次挂载卷之前 在设备上创建文件系统。

你可以将 volumeMode 设置为 Block,以便将卷作为原始块设备来使用。 这类卷以块设备的方式交给 Pod 使用,其上没有任何文件系统。 这种模式对于为 Pod 提供一种使用最快可能方式来访问卷而言很有帮助,Pod 和 卷之间不存在文件系统层。

访问模式

PersistentVolume 卷可以用资源提供者所支持的任何方式挂载到宿主系统上。 如下表所示,提供者(驱动)的能力不同,每个 PV 卷的访问模式都会设置为 对应卷所支持的模式值。 例如,NFS 可以支持多个读写客户,但是某个特定的 NFS PV 卷可能在服务器 上以只读的方式导出。每个 PV 卷都会获得自身的访问模式集合,描述的是 特定 PV 卷的能力。

访问模式有:

  • ReadWriteOnce:卷可以被一个节点以读写方式挂载。 ReadWriteOnce 访问模式也允许运行在同一节点上的多个 Pod 访问卷。
  • ReadOnlyMany:卷可以被多个节点以只读方式挂载。
  • ReadWriteMany:卷可以被多个节点以读写方式挂载。
  • ReadWriteOncePod:卷可以被单个 Pod 以读写方式挂载。 如果你想确保整个集群中只有一个 Pod 可以读取或写入该 PVC, 请使用ReadWriteOncePod 访问模式。这只支持 CSI 卷以及需要 Kubernetes 1.22 以上版本。

在命令行接口(CLI)中,访问模式也使用以下缩写形式:

  • RWO - ReadWriteOnce
  • ROX - ReadOnlyMany
  • RWX - ReadWriteMany
  • RWOP - ReadWriteOncePod

👀注意:每个卷同一时刻只能以一种访问模式挂载,即使该卷能够支持多种访问模式。

回收策略

目前的回收策略有:

  • Retain – 手动回收
  • Recycle – 基本擦除 (rm -rf /thevolume/*)
  • Delete – 诸如 AWS EBS、GCE PD、Azure Disk 或 OpenStack Cinder 卷这类关联存储资产也被删除

目前,仅 NFS 和 HostPath 支持回收(Recycle)。 AWS EBS、GCE PD、Azure Disk 和 Cinder 卷都支持删除(Delete)。

挂载选项(Mount Options)

Kubernetes 管理员可以指定持久卷被挂载到节点上时使用的附加挂载选项。

以下卷类型支持挂载选项:

  • awsElasticBlockStore
  • azureDisk
  • azureFile
  • cephfs
  • cinder (已弃用于 v1.18)
  • gcePersistentDisk
  • glusterfs
  • iscsi
  • nfs
  • quobyte (已弃用于 v1.22)
  • rbd
  • storageos (已弃用于 v1.22)
  • vsphereVolume

阶段(Phase)

每个卷会处于以下阶段(Phase)之一:

  • Available(可用)– 卷是一个空闲资源,尚未绑定到任何申领;
  • Bound(已绑定)– 该卷已经绑定到某申领;
  • Released(已释放)– 所绑定的申领已被删除,但是资源尚未被集群回收;
  • Failed(失败)– 卷的自动回收操作失败。

命令行接口能够显示绑定到某 PV 卷的 PVC 对象。

示例

新建pv.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: PersistentVolume
metadata:
name: task-pv-volume
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
hostPath:
path: '/mnt/data'

说明:

  • hostPath:表示这是一个 hostPath 类型的 Volume。

    • path:主机路径。
  • capacity:容量。

    • storage:总共 10Gi 大小。
  • accessModes:访问模式,共三种。

    • ReadWriteOnce:意思是这个 volume 只能被一个节点绑定为读写模式。
  • storageClassName:使用的 storage class name,这个是为了 PVC 使用的时候匹配,二者必须一致。

执行以下命令:

1
2
3
4
5
[root@master test]# kubectl create -f pv.yaml 
persistentvolume/task-pv-volume created
[root@master test]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
task-pv-volume 10Gi RWO Retain Available manual 6s

可以在 PV 的列表页看到 PV,并且多了一些状态信息:

  • Reclaim Policy:回收策略。应用在使用完 volume 之后(比如被删除),对应的 PVC 一般也会被删除。这时候之前申请的 volume 就不用了,需要考虑怎么回收使用。Reclaim Policy 就是定义回收策略的。

    • Retain:PVC 删除后,PV 不能给其它的应用使用了。可以手工删除 volume,清理数据,并新建 volume。

    • Delete:PVC 删除后,删除 PV,并且删除底层的存储结构。

    • Recycle:删除数据,PV 还可以继续给其它的应用使用。

  • Status:PV 的状态。

    • Available:可以被 PVC 绑定。
    • Bound:已经与 PVC 绑定。
    • Released:PVC 已删除,但是 PV 暂时还未被回收。
    • Failed:失败。

PVC

PVC(PersistentVolumeClaims)是一个 namespace scope 的 Resource,主要用来描述应用对于存储的需求。

PersistentVolume 卷的绑定是排他性的。 由于 PersistentVolumeClaim 是名字空间(namespace)作用域的对象,使用 “Many” 模式(ROXRWX)来挂载申领的操作只能在同一名字空间内进行。

示例

创建pvc.yaml:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: task-pv-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi

其中 storageClassName 和 accessModes 需要与 PV 的对应,与 PV 的 capacity 相反,PVC 的是 requests,表明存储需求。,这个 yaml 里申请了 3Gi 大小的存储。

👀注意:当我们申请 PVC 的容量大于 PV 的容量是无法进行绑定的。

执行以下命令:

1
2
3
4
5
[root@master test]# kubectl create -f pvc.yaml 
persistentvolumeclaim/task-pv-claim created
[root@master test]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
task-pv-claim Bound task-pv-volume 10Gi RWO manual 3s

创建完之后,相应的 PVC 也会有一个状态:

  • Bound: 已经与某个 PV 绑定。
  • Pending: 未与 PV 绑定。
  • Lost: 绑定后,PV 异常丢失。

因为之前已经有符合条件的 PV,所以现在是 Bound 状态。

这时候我们再来看 PV 的状态也已经变成 Bound 了:

1
2
3
[root@master test]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
task-pv-volume 10Gi RWO Retain Bound default/task-pv-claim manual

Volume Mounts

示例

有了 PV/PVC,我们就可以在应用中使用了。测试环境默认是部署了两个 node,为了方便实验,我们期望将应用只部署到固定的一个 node 上。用之前提到的 cordon 命令来将 node-2 标记为不可调度状态:

1
2
3
4
5
6
7
[root@master test]# kubectl cordon node-2
node/node-2 cordoned
[root@master test]# kubectl get node
NAME STATUS ROLES AGE VERSION
master Ready control-plane,master 41d v1.21.5
node-1 Ready worker 41d v1.21.5
node-2 Ready,SchedulingDisabled worker 41d v1.21.5

新建pv-pod.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kind: Pod
apiVersion: v1
metadata:
name: task-pv-pod
spec:
volumes:
- name: task-pv-storage
persistentVolumeClaim:
claimName: task-pv-claim
containers:
- name: task-pv-container
image: nginx
ports:
- containerPort: 80
name: 'http-server'
volumeMounts:
- mountPath: '/usr/share/nginx/html'
name: task-pv-storage

在 volumes 字段中,我们可以看到 volume 的来源变成了 persistentVolumeClaim,而属性就只有一个 claimName 名字,这体现了 PVC 的一个优势,就是将存储的细节隐藏了起来,应用无需知道具体的 volume 的来源。

执行以下命令:

1
2
kubectl create -f pv-pod.yaml
kubectl describe pod task-pv-pod

image-20220324161641343

在这个过程中有两个挂载的操作:

  • task-pv-volume: 就是我们在 yaml 中指定的那个。
  • kube-api-access-mbcqj: 这个是一个 secret 挂载,是 Kubernetes 自动加上的,为了便于用户应用访问 kubernetes api。一般情况下都用不到,可以忽略。

我们可以通过在 node-1 上创建一个文件来验证,首先因为 pod 部署在 node-1 上:

1
echo 'Hello from Kubernetes storage' > /mnt/data/index.html

创建完之后,我们在 volume 中创建的文件,在 pod 里也应该是可以看到的:

1
2
3
4
5
[root@master test]# kubectl exec -it task-pv-pod -- bash
root@task-pv-pod:/# ls /usr/share/nginx/html/
index.html
root@task-pv-pod:/# cat /usr/share/nginx/html/index.html
Hello from Kubernetes storage

在使用完之后,我们可以将 pvc pod 都删除,看看 PV 的状态:

1
2
3
4
5
6
7
[root@master test]# kubectl delete pods task-pv-pod
pod "task-pv-pod" deleted
[root@master test]# kubectl delete pvc task-pv-claim
persistentvolumeclaim "task-pv-claim" deleted
[root@master test]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
task-pv-volume 10Gi RWO Retain Released default/task-pv-claim manual 10m

StorageClass

虽然 PV 提供了便捷的挂载不同的 volume 的方式,但是在使用上仍然是有诸多不便的,特别是当应用很多的时候,我们不可能这样一个一个去创建 PV,而且它的存储大小是固定的,如果需求不是完美地匹配,很容易造成资源浪费。

一般在同一个集群环境下,使用的 volume 的类型都是一样的,因此类比面向对象的概念,可以有一个类似于 class 的概念来不断地生成 object(PV) ,这样就能省去重复创建 PV 的步骤,全部交由 kubernetes 来做,StorageClass 正是为此而生。

StorageClass的定义主要包括名称、后端存储的提供者(provisioner)和后端存储的相关参数配置。StorageClass一旦被创建出来,则将无法修改。如需更改,则只能删除原StorageClass的定义重建。

与 PV/PVC 不同,StorageClass 自身并不能完整地工作。它只是定义了一个概念,具体如何地生成 PV,回收 PV,需要有具体的实施者。所以一个完整的 StorageClass 需要由以下几部分组成:

  • StorageClass 的定义。
  • 负责执行 PV 管理的程序。
  • RBAC: kubernetes 有完整的 rbac 定义,需要给 PV 管理的程序以操作 PV 的权限。

每个 StorageClass 都包含 provisionerparametersreclaimPolicy 字段, 这些字段会在 StorageClass 需要动态分配 PersistentVolume 时会使用到。

StorageClass 对象的命名很重要,用户使用这个命名来请求生成一个特定的类。 当创建 StorageClass 对象时,管理员设置 StorageClass 对象的命名和其他参数,一旦创建了对象就不能再对其更新。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
reclaimPolicy: Retain
allowVolumeExpansion: true
mountOptions:
- debug
volumeBindingMode: Immediate

总结

PV和PVC分离了管理员和普通用户的职责,通过StorageClass实现更高效的动态供给。