4. PV和PVC的生命周期
我们可以将PV看作可用的存储资源,PVC则是对存储资源的需求,PV和PVC的相互关系遵循如图8.1所示的生命周期。
4.1 资源供应
Kubernetes支持两种资源的供应模式:静态模式(Static)和动态模式(Dynamic)。资源供应的结果就是创建好的PV。
◎ 静态模式:集群管理员手工创建许多PV,在定义PV时需要将后端存储的特性进行设置。
◎ 动态模式:集群管理员无须手工创建PV,而是通过StorageClass的设置对后端存储进行描述,标记为某种类型。此时要求PVC对存储的类型进行声明,系统将自动完成PV的创建及与PVC的绑定。PVC可以声明Class为"",说明该PVC禁止使用动态模式。
图8.3描述了在动态资源供应模式下,通过StorageClass和PVC完成资源动态绑定(系统自动生成PV),并供Pod使用的存储管理机制。
PV和PVC的生命周期
Available(可用)——一块空闲资源还没有被任何声明绑定 Bound(已绑定)——卷已经被声明绑定 Released(已释放)——声明被删除,但是资源还未被集群重新声明 Failed(失败)——该卷的自动回收失败
在 Kubernetes 中,实际上存在着一个专门处理持久化存储的控制器,叫作 Volume Controller。这个 Volume Controller 维护着多个控制循环,其中有一个循环,扮演的就是撮合 PV 和 PVC 的“红娘”的角色。它的名字叫作 PersistentVolumeController。PersistentVolumeController 会不断地查看当前每一个 PVC,是不是已经处于 Bound(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与这个“单身”的 PVC 进行绑定。这样,Kubernetes 就可以保证用户提交的每一个 PVC,只要有合适的 PV 出现,它就能够很快进入绑定状态,从而结束“单身”之旅。而所谓将一个 PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上。所以,接下来 Kubernetes 只要获取到这个 PVC 对象,就一定能够找到它所绑定的 PV。
5. StorageClass
在了解了 Kubernetes 的 Volume 处理机制之后,我再来为你介绍这个体系里最后一个重要概念:StorageClass。
我在前面介绍 PV 和 PVC 的时候,曾经提到过,PV 这个对象的创建,是由运维人员完成的。但是,在大规模的生产环境里,这其实是一个非常麻烦的工作。这是因为,一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC,这就意味着运维人员必须得事先创建出成千上万个 PV。更麻烦的是,随着新的 PVC 不断被提交,运维人员就不得不继续添加新的、能满足条件的 PV,否则新的 Pod 就会因为 PVC 绑定不到 PV 而失败。在实际操作中,这几乎没办法靠人工做到。
所以,Kubernetes 为我们提供了一套可以自动创建 PV 的机制,即:Dynamic Provisioning。相比之下,前面人工管理 PV 的方式就叫作 Static Provisioning。Dynamic Provisioning 机制工作的核心,在于一个名叫 StorageClass 的 API 对象。
而 StorageClass 对象的作用,其实就是创建 PV 的模板。
具体地说,StorageClass 对象会定义如下两个部分内容:
第一,PV 的属性。比如,存储类型、Volume 的大小等等。
第二,创建这种 PV 需要用到的存储插件。比如,Ceph 等等。
有了这样两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 StorageClass 了。然后,Kubernetes 就会调用该 StorageClass 声明的存储插件,创建出需要的 PV。
每个 StorageClass 都包含 provisioner、parameters 和 reclaimPolicy 字段, 这些字段会在 StorageClass 需要动态分配 PersistentVolume 时会使用到StorageClass作为对存储资源的抽象定义,对用户设置的PVC申请屏蔽后端存储的细节,一方面减少了用户对于存储资源细节的关注,另一方面减轻了管理员手工管理PV的工作,由系统自动完成PV的创建和绑定,实现了动态的资源供应。基于StorageClass的动态资源供应模式将逐步成为云平台的标准存储配置模式。StorageClass的定义主要包括名称、后端存储的提供者(provisioner)和后端存储的相关参数配置。StorageClass一旦被创建出来,则将无法修改。如需更改,则只能删除原StorageClass的定义重建。下例定义了一个名为standard的StorageClass,提供者为aws-ebs,其
参数设置了一个type,值为gp2:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: standard provisioner: kubernetes.io/aws-ebs parameters: type: gp2 reclaimPolicy: Retain allowVolumeExpansion: true mountOptions: - debug volumeBindingMode: Immediate
5.1 StorageClass的关键配置参数
提供者(Provisioner)
描述存储资源的提供者,也可以看作后端存储驱动。目前Kubernetes支持的Provisioner都以“kubernetes.io/”为开头,用户也可以使用自定义的后端存储提供者。为了符合StorageClass的用法,自定义Provisioner需要符合存储卷的开发规范。
参数(Parameters)
后端存储资源提供者的参数设置,不同的Provisioner包括不同的参数设置。某些参数可以不显示设定,Provisioner将使用其默认值。接下来通过几种常见的Provisioner对StorageClass的定义进行详细说明。
5.2 厂商与类型
GCE
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: block-service provisioner: kubernetes.io/gce-pd parameters: type: pd-ssd
在这个 YAML 文件里,我们定义了一个名叫 block-service 的 StorageClass。这个 StorageClass 的 provisioner 字段的值是:kubernetes.io/gce-pd,这正是 Kubernetes 内置的 GCE PD 存储插件的名字。
而这个 StorageClass 的 parameters 字段,就是 PV 的参数。比如:上面例子里的 type=pd-ssd,指的是这个 PV 的类型是“SSD 格式的 GCE 远程磁盘”。
ceph
如果你想使用我们之前部署在本地的 Kubernetes 集群以及 Rook 存储服务的话,你的 StorageClass 需要使用如下所示的 YAML 文件来定义:
apiVersion: ceph.rook.io/v1beta1 kind: Pool metadata: name: replicapool namespace: rook-ceph spec: replicated: size: 3 --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: block-service provisioner: ceph.rook.io/block parameters: pool: replicapool #The value of "clusterNamespace" MUST be the same as the one in which your rook cluster exist clusterNamespace: rook-ceph
在这个 YAML 文件中,我们定义的还是一个名叫 block-service 的 StorageClass,只不过它声明使的存储插件是由 Rook 项目。有了 StorageClass 的 YAML 文件之后,运维人员就可以在 Kubernetes 里创建这个 StorageClass 了:
$ kubectl create -f sc.yaml
这时候,作为应用开发者,我们只需要在 PVC 里指定要使用的 StorageClass 名字即可,如下所示:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: claim1 spec: accessModes: - ReadWriteOnce storageClassName: block-service resources: requests: storage: 30Gi
可以看到,我们在这个 PVC 里添加了一个叫作 storageClassName 的字段,用于指定该 PVC 所要使用的 StorageClass 的名字是:block-service。
$ kubectl create -f pvc.yaml $ kubectl describe pvc claim1 Name: claim1 Namespace: default StorageClass: block-service Status: Bound Volume: pvc-e5578707-c626-11e6-baf6-08002729a32b Labels: <none> Capacity: 30Gi Access Modes: RWO No Events. $ kubectl describe pv pvc-e5578707-c626-11e6-baf6-08002729a32b Name: pvc-e5578707-c626-11e6-baf6-08002729a32b Labels: <none> StorageClass: block-service Status: Bound Claim: default/claim1 Reclaim Policy: Delete Access Modes: RWO Capacity: 30Gi ... No events.
此外,你还可以看到,这个自动创建出来的 PV 的 StorageClass 字段的值,也是 block-service。这是因为,Kubernetes 只会将 StorageClass 相同的 PVC 和 PV 绑定起来。
AWS EBS存储卷
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: slow provisioner: kubernetes.io/aws-ebs parameters: type: io1 iopsPerGB: "10" fsType: ext4
type:io1,gp2,sc1,st1。详细信息参见 AWS 文档。默认值:gp2。
zone(弃用):AWS 区域。如果没有指定 zone 和 zones, 通常卷会在 Kubernetes集群节点所在的活动区域中轮询调度分配。 zone 和 zones 参数不能同时使用。
zones(弃用):以逗号分隔的 AWS 区域列表。 如果没有指定 zone 和 zones,通常卷会在 Kubernetes集群节点所在的 活动区域中轮询调度分配。zone和zones参数不能同时使用。
iopsPerGB:只适用于 io1 卷。每 GiB 每秒 I/O 操作。 AWS 卷插件将其与请求卷的大小相乘以计算 IOPS 的容量,并将其限制在 20000 IOPS(AWS 支持的最高值,请参阅 AWS 文档。 这里需要输入一个字符串,即 “10”,而不是 10。
fsType:受 Kubernetes 支持的文件类型。默认值:“ext4”。
encrypted:指定 EBS 卷是否应该被加密。合法值为 “true” 或者 “false”。 这里需要输入字符串,即 “true”,而非 true。
kmsKeyId:可选。加密卷时使用密钥的完整 Amazon 资源名称。 如果没有提供,但 encrypted 值为 true,AWS生成一个密钥。关于有效的 ARN 值,请参阅 AWS 文档。
Glusterfs
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: slow provisioner: kubernetes.io/glusterfs parameters: resturl: "http://127.0.0.1:8081" clusterid: "630372ccdc720a92c681fb928f27b53f" restauthenabled: "true" restuser: "admin" secretNamespace: "default" secretName: "heketi-secret" gidMin: "40000" gidMax: "50000" volumetype: "replicate:3"
resturl:制备 gluster 卷的需求的 Gluster REST 服务/Heketi 服务 url。 通用格式应该是 IPaddress:Port,这是 GlusterFS 动态制备器的必需参数。 如果 Heketi 服务在
OpenShift/kubernetes 中安装并暴露为可路由服务,则可以使用类似于http://heketi-storage-project.cloudapps.mystorage.com 的格式,其中 fqdn是可解析的 heketi 服务网址。
restauthenabled:Gluster REST 服务身份验证布尔值,用于启用对 REST 服务器的身份验证。 如果此值为’true’,则必须填写 restuser 和 restuserkey 或 secretNamespace + secretName。此选项已弃用,当在指定 restuser、restuserkey、secretName 或 secretNamespace时,身份验证被启用。
restuser:在 Gluster 可信池中有权创建卷的 Gluster REST服务/Heketi 用户。
restuserkey:Gluster REST 服务/Heketi 用户的密码将被用于对 REST 服务器进行身份验证。此参数已弃用,取而代之的是 secretNamespace + secretName。
secretNamespace,secretName:Secret 实例的标识,包含与 Gluster REST服务交互时使用的用户密码。 这些参数是可选的,secretNamespace 和 secretName 都省略时使用空密码。 所提供的Secret 必须将类型设置为 “kubernetes.io/glusterfs”,例如以这种方式创建:
kubectl create secret generic heketi-secret \ --type="kubernetes.io/glusterfs" --from-literal=key='opensesame' \ --namespace=default
Secret 的例子可以在 glusterfs-provisioning-secret.yaml 中找到。
clusterid:630372ccdc720a92c681fb928f27b53f 是集群的 ID,当制备卷时, Heketi 将会使用这个文件。它也可以是一个 clusterid 列表,例如: “8452344e2becec931ece4e33c4674e4e,42982310de6c63381718ccfa6d8cf397”。这个是可选参数。
gidMin,gidMax:storage class GID 范围的最小值和最大值。在此范围(gidMin-gidMax)内的唯一值(GID)将用于动态制备卷。这些是可选的值。 如果不指定,所制备的卷为一个2000-2147483647 之间的值,这是 gidMin 和 gidMax 的默认值。
volumetype:卷的类型及其参数可以用这个可选值进行配置。如果未声明卷类型,则 由制备器决定卷的类型。 例如:
'Replica volume': volumetype: replicate:3 其中 '3' 是 replica 数量. 'Disperse/EC volume': volumetype: disperse:4:2 其中 '4' 是数据,'2' 是冗余数量. 'Distribute volume': volumetype: none
有关可用的卷类型和管理选项,请参阅 管理指南。
更多相关的参考信息,请参阅 如何配置 Heketi。
当动态制备持久卷时,Gluster 插件自动创建名为 gluster-dynamic-<claimname> 的端点和无头服务。在 PVC 被删除时动态端点和无头服务会自动被删除。
本地
FEATURE STATE: Kubernetes v1.14 [stable]
kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer
本地卷还不支持动态制备,然而还是需要创建 StorageClass 以延迟卷绑定, 直到完成 Pod 的调度。这是由 WaitForFirstConsumer 卷绑定模式指定的。
延迟卷绑定使得调度器在为 PersistentVolumeClaim 选择一个合适的 PersistentVolume 时能考虑到所有 Pod 的调度限制。
5.3 设置默认的StorageClass
a. 修改kube-apiserver参数
要在系统中设置一个默认的StorageClass,则首先需要启用名为
DefaultStorageClass的admission controller,即在kube-apiserver的命令行
参数–admission-control中增加:
--admission-control=...,DefaultStorageClass
b. 标记默认 StorageClass 非默认
$ kubectl get storageclass NAME PROVISIONER AGE standard (default) kubernetes.io/gce-pd 1d gold kubernetes.io/gce-pd 1d
kubectl patch storageclass standard -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
c. 标记一个 StorageClass 为默认
在StorageClass的定义中设置一个annotation
:
或者
kubectl patch storageclass <your-class-name> -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl get storageclass 输出类似这样: NAME PROVISIONER AGE standard kubernetes.io/gce-pd 1d gold (default) kubernetes.io/gce-pd 1d
有了 Dynamic Provisioning 机制,运维人员只需要在 Kubernetes 集群里创建出数量有限的 StorageClass 对象就可以了。这就好比,运维人员在 Kubernetes 集群里创建出了各种各样的 PV 模板。这时候,当开发人员提交了包含 StorageClass 字段的 PVC 之后,Kubernetes 就会根据这个 StorageClass 创建出对应的 PV。
需要注意的是,StorageClass 并不是专门为了 Dynamic Provisioning 而设计的。
比如,在本篇一开始的例子里,我在 PV 和 PVC 里都声明了 storageClassName=manual。而我的集群里,实际上并没有一个名叫 manual 的 StorageClass 对象。这完全没有问题,这个时候 Kubernetes 进行的是 Static Provisioning,但在做绑定决策的时候,它依然会考虑 PV 和 PVC 的 StorageClass 定义。
而这么做的好处也很明显:这个 PVC 和 PV 的绑定关系,就完全在我自己的掌控之中。
这里,你可能会有疑问,我在之前讲解 StatefulSet 存储状态的例子时,好像并没有声明 StorageClass 啊?
实际上,如果你的集群已经开启了名叫 DefaultStorageClass 的 Admission Plugin,它就会为 PVC 和 PV 自动添加一个默认的 StorageClass;否则,PVC 的 storageClassName 的值就是“”,这也意味着它只能够跟 storageClassName 也是“”的 PV 进行绑定。
总结
StorageClass 到底是干什么用的。这些概念之间的关系,可以用如下所示的一幅示意图描述:
从图中我们可以看到,在这个体系中:
PVC 描述的,是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等。
PV 描述的,则是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等。
而 StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 StorageClass 的 PV 和 PVC,才可以绑定在一起。
当然,StorageClass 的另一个重要作用,是指定 PV 的 Provisioner(存储插件)。这时候,如果你的存储插件支持 Dynamic Provisioning 的话,Kubernetes 就可以自动为你创建 PV 了。参考: