前提条件
- 拥有Kubernetes集群环境,可参考:Kubernetes集群搭建
- 理解Kubernetes部署知识,可参考:使用Kubernetes部署第一个应用 、Deloyment控制器
- 拥有NFS服务,可参考:Linux环境搭建NFS服务
- 掌握Kubernetes PV及PVC的使用,可参考:Kubernetes PV及PVC的使用
问题背景
PV 都是静态的,PVC 需要用的存储,就必须手动去创建一个 PV,如果有很多PVC就必须手动创建很多PVC,非常不方便。这种方式在很大程度上并不能满足我们的需求,比如我们有一个应用需要对存储的并发度要求比较高,而另外一个应用对读写速度又要求比较高,特别是对于 StatefulSet 类型的应用简单的来使用静态的 PV 就很不合适了,这种情况下就需要用到动态 PV,也就是 StorageClass。
StorageClass简介
StorageClass 是 Kubernetes 中用于抽象存储供应的一种机制。它允许管理员定义不同类型的存储供应方式,使得用户(通过 PVC)能够以一种更灵活的方式请求存储资源,而无需关心存储的具体实现细节。简单来说,它就像是存储资源的 “模板”,规定了存储是如何被动态分配的。
主要属性和功能
- 存储供应器(Provisioner)
- 这是 StorageClass 的核心组件。存储供应器负责根据 PVC 的请求实际创建 PV。不同的存储供应器对应不同的存储后端或存储技术。例如,在云环境中,有针对云存储服务的供应器;在本地数据中心,可能有针对 Ceph、GlusterFS 等分布式存储系统的供应器。当用户创建一个 PVC 并指定了一个 StorageClass 时,存储供应器会被调用,按照预定的规则创建合适的 PV。
- 参数(Parameters)
- StorageClass 可以包含各种参数,这些参数用于定制存储供应的细节。例如,对于某些存储系统,可能需要指定存储卷的类型(如高性能 SSD、大容量 HDD 等)、存储的加密方式、备份策略等参数。以 Ceph 存储系统为例,参数可能包括 Ceph 存储池(pool)的名称、副本数量等信息。这些参数会被传递给存储供应器,以确保创建的 PV 符合用户期望的存储特性。
- 回收策略(Reclaim Policy)
- 与 PV 的回收策略类似,StorageClass 也可以定义回收策略。不过,它定义的回收策略会应用于通过该 StorageClass 动态创建的 PV。常见的回收策略有
Delete
和Retain
。如果是Delete
策略,当 PVC 被删除后,通过该 StorageClass 创建的 PV 及其数据也会被自动删除;如果是Retain
策略,PV 会被保留,数据也不会被自动清除,需要管理员手动处理。
- 与 PV 的回收策略类似,StorageClass 也可以定义回收策略。不过,它定义的回收策略会应用于通过该 StorageClass 动态创建的 PV。常见的回收策略有
- 动态分配 PV 的功能
- 这是 StorageClass 的一个关键优势。在没有 StorageClass 的情况下,如果没有预先创建足够的 PV,当用户创建 PVC 时可能会因为没有合适的 PV 而导致绑定失败。而有了 StorageClass,当 PVC 被创建并且没有匹配的静态 PV 时,Kubernetes 会根据 PVC 的请求和 StorageClass 的配置自动创建一个 PV。例如,一个应用开发团队创建了一个 PVC,请求 10Gi 的存储容量和
ReadWriteOnce
访问模式,并指定了一个 StorageClass。如果集群中没有合适的静态 PV,系统会根据这个 StorageClass 的供应器和参数自动创建一个新的 PV 来满足 PVC 的需求。
- 这是 StorageClass 的一个关键优势。在没有 StorageClass 的情况下,如果没有预先创建足够的 PV,当用户创建 PVC 时可能会因为没有合适的 PV 而导致绑定失败。而有了 StorageClass,当 PVC 被创建并且没有匹配的静态 PV 时,Kubernetes 会根据 PVC 的请求和 StorageClass 的配置自动创建一个 PV。例如,一个应用开发团队创建了一个 PVC,请求 10Gi 的存储容量和
创建StorageClass
要使用 StorageClass,我们就得安装对应的存储供应器(Provisioner),比如我们这里存储后端使用的是 nfs,那么我们就需要使用到一个 nfs-client 的存储供应器,这个程序使用我们已经配置好的 nfs 服务器,来自动创建持久卷,也就是自动帮我们创建 PV。
-
自动创建的 PV 以
${namespace}-${pvcName}-${pvName}
这样的命名格式创建在 NFS 服务器上的共享数据目录中 -
而当这个 PV 被回收后会以
archieved-${namespace}-${pvcName}-${pvName}
这样的命名格式存在 NFS 服务器上。
当然在部署nfs-client
之前,我们需要先成功安装上 nfs 服务器, 可参考:Linux环境搭建NFS服务
第一步:配置 Deployment,将里面的对应的参数替换成自己的 nfs 配置(nfs-client.yaml)
创建yaml文件
vi nfs-client.yaml
内容如下
kind: Deployment
apiVersion: apps/v1
metadata:
name: nfs-client-provisioner
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: vbouchaud/nfs-client-provisioner:latest
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: fuseim.pri/ifs
- name: NFS_SERVER
value: k8s-node02
- name: NFS_PATH
value: /data/k8s
volumes:
- name: nfs-client-root
nfs:
server: k8s-node02
path: /data/k8s
提示:如果vbouchaud/nfs-client-provisioner:latest镜像下不下来,可以使用registry.cn-hangzhou.aliyuncs.com/my-common-images/nfs-client-provisioner:latest代替
第二步:将环境变量 NFS_SERVER 和 NFS_PATH 替换,当然也包括下面的 nfs 配置,这里使用了一个名为 nfs-client-provisioner 的serviceAccount
,所以我们也需要创建一个 sa,然后绑定上对应的权限:(nfs-client-sa.yaml)
创建nfs-client-sa.yaml
[root@k8s-master01 pvpvctest]# vi nfs-client-sa.yaml
内容如下
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: default
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
新建的一个名为 nfs-client-provisioner 的ServiceAccount
,然后绑定了一个名为 nfs-client-provisioner-runner 的ClusterRole
,而该ClusterRole
声明了一些权限,其中就包括对persistentvolumes
的增、删、改、查等权限,所以可以利用该ServiceAccount
来自动创建 PV。
第三步:创建一个StorageClass
对象:(nfs-client-class.yaml)
创建nfs-client-class.yaml
vi nfs-client-class.yaml
内容如下
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: course-nfs-storage
provisioner: fuseim.pri/ifs # or choose another name, must match deployment's env PROVISIONER_NAME'
声明了一个名为 course-nfs-storage 的StorageClass
对象,注意下面的provisioner
对应的值一定要和上面的Deployment
下面的 PROVISIONER_NAME 这个环境变量的值一样。
前面通过YAML文件声明这些资源对象后,接下来创建这些资源对象:
[root@k8s-master01 pvpvctest]# kubectl create -f nfs-client.yaml deployment.apps/nfs-client-provisioner created [root@k8s-master01 pvpvctest]# kubectl create -f nfs-client-sa.yaml serviceaccount/nfs-client-provisioner created clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created [root@k8s-master01 pvpvctest]# kubectl create -f nfs-client-class.yaml storageclass.storage.k8s.io/course-nfs-storage created
创建完成后查看下资源状态:
[root@k8s-master01 pvpvctest]# kubectl get pods NAME READY STATUS RESTARTS AGE nfs-client-provisioner-8557778f8-7vfw4 1/1 Running 0 63s $ kubectl get storageclass NAME PROVISIONER AGE course-nfs-storage fuseim.pri/ifs 11s [root@k8s-master01 pvpvctest]# kubectl get storageclass NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE course-nfs-storage fuseim.pri/ifs Delete Immediate false 11m
新建PVC
上面把StorageClass
资源对象创建成功了,接下来通过一个示例测试下动态 PV,首先创建一个 PVC 对象:(test-pvc.yaml)
创建
[root@k8s-master01 pvpvctest]# vi test-pvc.yaml
内容如下
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi
这里声明了一个 PVC 对象,采用 ReadWriteMany 的访问模式,请求 1Mi 的空间,但是可以看到上面的 PVC 文件我们没有标识出任何和 StorageClass 相关联的信息,那么如果现在直接创建这个 PVC 对象能够自动绑定上合适的 PV 对象吗?显然是不能的(前提是没有合适的 PV),这里有两种方法可以来利用上面创建的 StorageClass 对象来自动创建一个合适的 PV:
-
第一种方法:在这个 PVC 对象中添加一个声明 StorageClass 对象的标识,这里可以利用一个 annotations 属性来标识,如下:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
annotations:
volume.beta.kubernetes.io/storage-class: "course-nfs-storage"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi
-
第二种方法:可以设置这个 course-nfs-storage 的 StorageClass 为 Kubernetes 的默认存储后端,可以用 kubectl patch 命令来更新:
$ kubectl patch storageclass course-nfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
上面这两种方法都是可以的,当然为了不影响系统的默认行为,这里还是采用第一种方法,直接创建即可:
[root@k8s-master01 pvpvctest]# kubectl create -f test-pvc.yaml Warning: metadata.annotations[volume.beta.kubernetes.io/storage-class]: deprecated since v1.8; use "storageClassName" attribute instead persistentvolumeclaim/test-pvc created [root@k8s-master01 pvpvctest]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE ... test-pvc Bound pvc-d0806802-e3c1-481f-b205-7535732981b5 1Mi RWX course-nfs-storage <unset> 19s
可以看到一个名为 test-pvc 的 PVC 对象创建成功了,状态已经是 Bound 了,也产生了一个对应的 VOLUME 对象,最重要的一栏是 STORAGECLASS,也有值了,就是刚刚创建的 StorageClass 对象 course-nfs-storage。
然后查看下 PV 对象呢:
[root@k8s-master01 pvpvctest]# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE ... pvc-d0806802-e3c1-481f-b205-7535732981b5 1Mi RWX Delete Bound default/test-pvc course-nfs-storage <unset> 58s
可以看到是不是自动生成了一个关联的 PV 对象,访问模式是 RWX,回收策略是 Delete,这个 PV 对象并不是手动创建的,这是通过上面的 StorageClass 对象自动创建的。这就是 StorageClass 的创建方法。
测试
接下来用一个示例来测试下上面用 StorageClass 方式声明的 PVC 对象吧:(test-pod.yaml)
创建yaml文件
[root@k8s-master01 pvpvctest]# vi test-pod.yaml
内容如下
kind: Pod
apiVersion: v1
metadata:
name: test-pod
spec:
containers:
- name: test-pod
image: busybox
imagePullPolicy: IfNotPresent
command:
- "/bin/sh"
args:
- "-c"
- "touch /mnt/SUCCESS && exit 0 || exit 1"
volumeMounts:
- name: nfs-pvc
mountPath: "/mnt"
restartPolicy: "Never"
volumes:
- name: nfs-pvc
persistentVolumeClaim:
claimName: test-pvc
上面这个 Pod 非常简单,就是用一个 busybox 容器,在 /mnt 目录下面新建一个 SUCCESS 的文件,然后把 /mnt 目录挂载到上面我们新建的 test-pvc 这个资源对象上面了,要验证很简单,只需要去查看下nfs 服务器上面的共享数据目录下面是否有 SUCCESS 这个文件即可:
[root@k8s-master01 pvpvctest]# kubectl create -f test-pod.yaml pod/test-pod created
然后可以在 nfs 服务器的共享数据目录下面查看下数据:
[root@k8s-node02 ~]# ls /data/k8s/ default-test-pvc-pvc-d0806802-e3c1-481f-b205-7535732981b5 nginxpvc-test test.txt
看到下面有名字很长的文件夹,这个文件夹的命名方式是不是和我们上面的规则:${namespace}-${pvcName}-${pvName}
是一样的,再看下这个文件夹下面是否有其他文件:
[root@k8s-node02 ~]# ls /data/k8s/default-test-pvc-pvc-d0806802-e3c1-481f-b205-7535732981b5/ SUCCESS
看到下面有一个 SUCCESS 的文件,就证明上面的验证是成功的。
另外可以看到我们这里是手动创建的一个 PVC 对象,在实际工作中,使用 StorageClass 更多的是 StatefulSet 类型的服务,StatefulSet 类型的服务也可以通过一个 volumeClaimTemplates 属性来直接使用 StorageClass,如下:(test-statefulset-nfs.yaml)
创建test-statefulset-nfs.yaml
[root@k8s-master01 pvpvctest]# vi test-statefulset-nfs.yaml
内容如下
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nfs-web
spec:
serviceName: "nginx"
replicas: 3
selector:
matchLabels:
app: nfs-web
template:
metadata:
labels:
app: nfs-web
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: nginx:1.16.1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
annotations:
volume.beta.kubernetes.io/storage-class: course-nfs-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
实际上 volumeClaimTemplates 下面就是一个 PVC 对象的模板,就类似于我们这里 StatefulSet 下面的 template,实际上就是一个 Pod 的模板,不单独创建成 PVC 对象,而用这种模板就可以动态的去创建了对象了,这种方式在 StatefulSet 类型的服务下面使用得非常多。
直接创建上面的对象:
[root@k8s-master01 pvpvctest]# kubectl create -f test-statefulset-nfs.yaml statefulset.apps/nfs-web created [root@k8s-master01 pvpvctest]# kubectl get pods NAME READY STATUS RESTARTS AGE nfs-client-provisioner-8557778f8-7vfw4 1/1 Running 0 14m nfs-web-0 1/1 Running 0 33s nfs-web-1 1/1 Running 0 31s nfs-web-2 1/1 Running 0 27s test-pod 0/1 Completed 0 4m38s
创建完成后可以看到上面的3个 名为的nfs-web开头的Pod 已经运行成功,然后查看下 PVC 对象:
[root@k8s-master01 pvpvctest]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE pvc-nfs Bound pv1 1Gi RWO <unset> 106m test-pvc Bound pvc-d0806802-e3c1-481f-b205-7535732981b5 1Mi RWX course-nfs-storage <unset> 8m20s www-nfs-web-0 Bound pvc-e670792b-4e2f-4ea2-8ab9-88838c444184 1Gi RWO course-nfs-storage <unset> 93s www-nfs-web-1 Bound pvc-673a974d-fc48-4c64-a055-8a8334dcc952 1Gi RWO course-nfs-storage <unset> 91s www-nfs-web-2 Bound pvc-a904ff97-5b9d-45fc-84c2-784f54065a4d 1Gi RWO course-nfs-storage <unset> 87s
可以看到是不是也生成了3个www-nfs-web开头的 PVC 对象,名称由模板名称 name 加上 Pod 的名称组合而成,这3个CLAIM名为default/www-nfs-web
开头的 PVC 对象也都是 绑定状态了,很显然我们查看 PV 也可以看到对应的3个 PV 对象:
[root@k8s-master01 pvpvctest]# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE pv1 1Gi RWO Recycle Bound default/pvc-nfs <unset> 122m pvc-673a974d-fc48-4c64-a055-8a8334dcc952 1Gi RWO Delete Bound default/www-nfs-web-1 course-nfs-storage <unset> 2m18s pvc-a904ff97-5b9d-45fc-84c2-784f54065a4d 1Gi RWO Delete Bound default/www-nfs-web-2 course-nfs-storage <unset> 2m14s pvc-d0806802-e3c1-481f-b205-7535732981b5 1Mi RWX Delete Bound default/test-pvc course-nfs-storage <unset> 9m7s pvc-e670792b-4e2f-4ea2-8ab9-88838c444184 1Gi RWO Delete Bound default/www-nfs-web-0 course-nfs-storage <unset> 2m20s
查看 nfs 服务器上面的共享数据目录:
[root@k8s-node02 ~]# ls /data/k8s/ default-test-pvc-pvc-d0806802-e3c1-481f-b205-7535732981b5 default-www-nfs-web-2-pvc-a904ff97-5b9d-45fc-84c2-784f54065a4d default-www-nfs-web-0-pvc-e670792b-4e2f-4ea2-8ab9-88838c444184 nginxpvc-test default-www-nfs-web-1-pvc-673a974d-fc48-4c64-a055-8a8334dcc952 test.txt
有对应的3个default-www-nfs-web相关的数据目录,这就是 StorageClass 的使用方法,对于 StorageClass 多用于 StatefulSet 类型的服务。
查看其中一个文件为空
[root@k8s-node02 ~]# ls /data/k8s/default-www-nfs-web-0-pvc-e670792b-4e2f-4ea2-8ab9-88838c444184/ [root@k8s-node02 ~]#