存储卷介绍 Kubernetes
使用存储的原因
Kubernetes
中的副本控制器保证了pod的始终存储,却保证不了pod中的数据。只有启动一个新pod的,之前pod中的数据会随着容器的删掉而丢失!共享存储机制
Kubernetes
对于有状态的容器应用或者对数据需要持久化的应用,不仅需要将容器内的目录挂载到宿主机的目录或者emptyDir临时存储卷,而且需要更加可靠的存储来保存应用产生的重要数据,以便容器应用在重建之后,仍然可以使用之前的数据。Kubernetes引入持久化存储卷(PV)
和持久化存储声明(PVC)
两个资源对象实现对存储的管理子系统。概念 PV
的全称是:PersistentVolume
(持久化卷),是对底层共享存储的一种抽象,PV 由管理员进行创建和配置,是一个全局资源,包含存储的类型,存储的大小和访问模式等。它和具体的底层的共享存储技术的实现方式有关,比如 Ceph
、GlusterFS
、NFS
、hostPath
等,都是通过插件机制完成与共享存储的对接。
PVC
的全称是:PersistentVolumeClaim
(持久化卷声明),PVC 是用户存储的一种声明,PVC 和 Pod 比较类似,Pod 消耗的是节点,PVC 消耗的是 PV 资源,Pod 可以请求 CPU 和内存,而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用 PVC 即可。
存储卷的分类 kubernetes支持的存储卷类型非常丰富,使用下面的命令查看:
1 # kubectl explain pod.spec.volumes
或者参考: https://kubernetes.io/docs/concepts/storage/
kubernetes支持的存储卷列表如下:
在Kubernetes中,存储卷按照其类型可以简单地分为本地存储卷、网络存储卷、分布式存储和云存储。这些不同类型的存储提供了灵活的选择,以满足各种应用程序的持久化存储需求。
本地存储卷 :emptyDir :可用于临时数据存储,在Pod删除时数据也会被清除。hostPath :允许将宿主机目录映射到Pod中,提供直接的本地存储支持。网络存储卷 :NAS类 :如NFS等,提供基于网络的文件系统共享。SAN类 :包括iSCSI、FC等,提供块级存储访问。分布式存储 :涵盖了诸如GlusterFS、CephFS、RBD、Cinder等技术,支持横向扩展的分布式存储解决方案。云存储 :代表各大云平台所提供的存储服务,如AWS、Azure File等。本文将着重探讨Kubernetes中的本地存储卷和网络存储卷(特别是NFS),以及它们在实际应用中的使用方法和最佳实践。
本地存储卷 emptyDir 1.创建yaml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 apiVersion: v1 kind: Pod metadata: name: volume-emptydir spec: containers: - name: write image: centos imagePullPolicy: IfNotPresent command: ["bash" ,"-c" ,"echo haha > /data/1.txt ; sleep 6000" ] volumeMounts: - name: data mountPath: /data - name: read image: centos imagePullPolicy: IfNotPresent command: ["bash" ,"-c" ,"cat /data/1.txt; sleep 6000" ] volumeMounts: - name: data mountPath: /data volumes: - name: data emptyDir: {}
2.基于yaml文件创建pod
1 2 $ kubectl apply -f volume-emptydir.yml pod/volume-emptydir created
3.查看pod启动情况
1 2 3 $ kubectl get pods |grep volume-emptydir NAME READY STATUS RESTARTS AGE volume-emptydir 2/2 Running 0 15s
4.查看pod描述信息
1 2 3 4 5 6 7 8 9 10 11 12 $ kubectl describe pod volume-emptydir | tail -10 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 50s default-scheduler Successfully assigned default/volume-emptydir to k8s-worker1 Normal Pulling 50s kubelet Pulling image "centos:centos7" Normal Pulled 28s kubelet Successfully pulled image "centos:centos7" in 21.544912361s Normal Created 28s kubelet Created container write Normal Started 28s kubelet Started container write Normal Pulled 28s kubelet Container image "centos:centos7" already present on machine Normal Created 28s kubelet Created container read Normal Started 28s kubelet Started container read
5.验证
1 2 3 4 $ kubectl logs volume-emptydir -c write $ kubectl logs volume-emptydir -c read haha
hostPath 应用场景
pod内与集群节点目录映射(pod中容器想访问节点上数据,例如监控,只有监控访问到节点主机文件才能知道集群节点主机状态) 缺点
如果集群节点挂掉,控制器在另一个集群节点拉起容器,数据就会变成另一台集群节点主机的了(无法实现数据共享) 创建一个 hostPath
类型的 PersistentVolume
。Kubernetes 支持 hostPath 类型的 PersistentVolume 使用节点上的文件或目录来模拟附带网络的存储,但是需要注意的是在生产集群中,我们不会使用 hostPath,集群管理员会提供网络存储资源,比如 NFS 共享卷或 Ceph 存储卷,集群管理员还可以使用 StorageClasses
来设置动态提供存储。因为 Pod 并不是始终固定在某个节点上面的,所以要使用 hostPath 的话我们就需要将 Pod 固定在某个节点上,这样显然就大大降低了应用的容错性。
比如我们这里将测试的应用固定在节点 k8s-node1 上面,首先在该节点上面创建一个 /data/volumes/hostpath
的目录,然后在该目录中创建一个 index.html
的文件:
1 2 mkdir -p /data/volumes/hostpath echo -e '<h1>Hello from Kubernetes hostpath storage</h1>\n<h1>王先森博客:https://www.boysec.cn </h1>' > /data/volumes/hostpath/index.html
接下来创建一个 hostPath 类型的 PV 资源对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: PersistentVolume metadata: name: pv-hostpath labels: type: local spec: storageClassName: manual capacity: storage: 10Gi accessModes: - ReadWriteOnce hostPath: path: '/data/volumes/hostpath'
下面是关于 PV 的这些配置属性的一些说明:
Capacity(存储能力):一般来说,一个 PV 对象都要指定一个存储能力,通过 PV 的 capacity
属性来设置的,目前只支持存储空间的设置,就是我们这里的 storage=10Gi
,不过未来可能会加入 IOPS
、吞吐量等指标的配置。 AccessModes(访问模式):用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载 ReadOnlyMany(ROX):只读权限,可以被多个节点挂载 ReadWriteMany(RWX):读写权限,可以被多个节点挂载 注意:一些 PV 可能支持多种访问模式,但是在挂载的时候只能使用一种访问模式,多种访问模式是不会生效的。
下图是一些常用的 Volume 插件支持的访问模式:
直接创建上面的资源对象:
1 2 $ kubectl apply -f pv-hostpath.yaml persistentvolume/pv-hostpath created
创建完成后查看 PersistentVolume 的信息,输出结果显示该 PersistentVolume
的状态(STATUS) 为 Available
。 这意味着它还没有被绑定给 PersistentVolumeClaim
:
1 2 3 $ kubectl get pv pv-hostpath NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-hostpath 10Gi RWO Retain Available manual 58s
其中有一项 RECLAIM POLICY
的配置,同样我们可以通过 PV 的 persistentVolumeReclaimPolicy
(回收策略)属性来进行配置,目前 PV 支持的策略有三种:
Retain(保留):保留数据,需要管理员手工清理数据 Recycle(回收):清除 PV 中的数据,效果相当于执行 rm -rf /thevoluem/*
Delete(删除):与 PV 相连的后端存储完成 volume 的删除操作,当然这常见于云服务商的存储服务,比如 ASW EBS。 不过需要注意的是,目前只有 NFS
和 HostPath
两种类型支持回收策略,当然一般来说还是设置为 Retain
这种策略保险一点。
注意
Recycle
策略会通过运行一个 busybox 容器来执行数据删除命令,默认定义的 busybox 镜像是:gcr.io/google_containers/busybox:latest
,并且 imagePullPolicy: Always
,如果需要调整配置,需要增加kube-controller-manager
启动参数:--pv-recycler-pod-template-filepath-hostpath
来进行配置。
关于 PV 的状态,实际上描述的是 PV 的生命周期的某个阶段,一个 PV 的生命周期中,可能会处于 4 种不同的阶段:
Available(可用):表示可用状态,还未被任何 PVC 绑定 Bound(已绑定):表示 PV 已经被 PVC 绑定 Released(已释放):PVC 被删除,但是资源还未被集群重新声明 Failed(失败): 表示该 PV 的自动回收失败 现在我们创建完成了 PV,如果我们需要使用这个 PV 的话,就需要创建一个对应的 PVC 来和他进行绑定了,就类似于我们的服务是通过 Pod 来运行的,而不是 Node,只是 Pod 跑在 Node 上而已。
现在我们来创建一个 PersistentVolumeClaim
,Pod 使用 PVC 来请求物理存储,我们这里创建的 PVC 请求至少 3G 容量的卷,该卷至少可以为一个节点提供读写访问,下面是 PVC 的配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-hostpath spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 3Gi
同样我们可以直接创建这个 PVC 对象:
1 2 $ kubectl create -f pvc-hostpath.yaml persistentvolumeclaim/pvc-hostpath created
创建 PVC 之后,Kubernetes 就会去查找满足我们声明要求的 PV,比如 storageClassName
、accessModes
以及容量这些是否满足要求,如果满足要求就会将 PV 和 PVC 绑定在一起。
我们现在再次查看 PV 的信息:
1 2 3 $ kubectl get pv -l type =local NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-hostpath 10Gi RWO Retain Bound default/pvc-hostpath manual 5m40s
现在输出的 STATUS 为 Bound
,查看 PVC 的信息:
1 2 3 $ kubectl get pvc pvc-hostpath NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-hostpath Bound pv-hostpath 10Gi RWO manual 111s
输出结果表明该 PVC 绑定了到了上面我们创建的 pv-hostpath
这个 PV 上面了,我们这里虽然声明的 3G 的容量,但是由于 PV 里面是 10G,所以显然也是满足要求的。
PVC 准备好过后,接下来我们就可以来创建 Pod 了,该 Pod 使用上面我们声明的 PVC 作为存储卷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 apiVersion: v1 kind: Pod metadata: name: pv-hostpath-pod spec: volumes: - name: pv-hostpath persistentVolumeClaim: claimName: pvc-hostpath nodeSelector: kubernetes.io/hostname: node1 containers: - name: task-pv-container image: nginx:alpine ports: - containerPort: 80 volumeMounts: - mountPath: '/usr/share/nginx/html' name: pv-hostpath
这里需要注意的是,由于我们创建的 PV 真正的存储在节点 node1 上面,所以我们这里必须把 Pod 固定在这个节点下面,另外可以注意到 Pod 的配置文件指定了 PersistentVolumeClaim
,但没有指定 PersistentVolume
,对 Pod 而言,PVC
就是一个存储卷。直接创建这个 Pod 对象即可:
1 2 3 4 5 $ kubectl create -f pv-hostpath-pod.yaml pod/pv-hostpath-pod created $ kubectl get pod pv-hostpath-pod NAME READY STATUS RESTARTS AGE pv-hostpath-pod 1/1 Running 0 105s
运行成功后,我们可以打开一个 shell 访问 Pod 中的容器:
1 2 3 [root@k8s-node2 ~]# curl 172.17.120.17 <h1>Hello from Kubernetes hostpath storage</h1> <h1>王先森博客:https://www.boysec.cn </h1>
可以看到输出结果是我们前面写到 hostPath 卷种的 index.html 文件中的内容,同样我们可以把 Pod 删除,然后再次重建再测试一次,可以发现内容还是我们在 hostPath 种设置的内容。可通过修改本地存储文件来实现页面更新等操作:
1 2 3 4 5 $ echo -e '<h1>Hello from Kubernetes hostpath storage</h1>\n<h1>王先森博客:https://www.boysec.cn </h1>\n<h1>Version: v1</h1>' > /data/volumes/hostpath/index.html$ curl 172.17.120.17 <h1>Hello from Kubernetes hostpath storage</h1> <h1>王先森博客:https://www.boysec.cn </h1> <h1>Version: v1</h1>
Local PV 使用 hostPath 有一个局限性就是,Pod 不能随便漂移,需要固定到一个节点上,因为一旦漂移到其他节点上去了宿主机上面就没有对应的数据了,所以在使用 hostPath 的时候都会搭配 nodeSelector 来进行使用。
所以在 hostPath 的基础上,Kubernetes 依靠 PV、PVC 实现了一个新的特性,这个特性的名字叫作:Local Persistent Volume
,也就是我们说的 Local PV
。
其实 Local PV
实现的功能就非常类似于 hostPath
加上 nodeAffinity
,比如,一个 Pod 可以声明使用类型为 Local 的 PV,而这个 PV 其实就是一个 hostPath 类型的 Volume。如果这个 hostPath 对应的目录,已经在节点 A 上被事先创建好了,那么,我只需要再给这个 Pod 加上一个 nodeAffinity=nodeA
,不就可以使用这个 Volume 了吗?理论上确实是可行的,但是事实上,我们绝不应该把一个宿主机上的目录当作 PV 来使用,因为本地目录的存储行为是完全不可控,它所在的磁盘随时都可能被应用写满,甚至造成整个宿主机宕机。所以,一般来说 Local PV
对应的存储介质是一块额外挂载在宿主机的磁盘或者块设备,我们可以认为就是“一个 PV 一块盘”
。
另外一个 Local PV
和普通的 PV 有一个很大的不同在于 Local PV
可以保证 Pod 始终能够被正确地调度到它所请求的 Local PV
所在的节点上面,对于普通的 PV 来说,Kubernetes 都是先调度 Pod 到某个节点上,然后再持久化节点上的 Volume 目录,进而完成 Volume 目录与容器的绑定挂载,但是对于 Local PV
来说,节点上可供使用的磁盘必须是提前准备好的,因为它们在不同节点上的挂载情况可能完全不同,甚至有的节点可以没这种磁盘,所以,这时候,调度器就必须能够知道所有节点与 Local PV
对应的磁盘的关联关系,然后根据这个信息来调度 Pod,实际上就是在调度的时候考虑 Volume 的分布。
接下来来测试下 Local PV
的使用,当然按照上面我们的分析我们应该给宿主机挂载并格式化一个可用的磁盘,我们这里就暂时将 k8s-node1 节点上的 /data/volumes/localpv
这个目录看成是挂载的一个独立的磁盘。现在我们来声明一个 Local PV
类型的 PV,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: v1 kind: PersistentVolume metadata: name: pv-local spec: capacity: storage: 5Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Delete storageClassName: local-storage local: path: /data/volumes/localpv nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node1
和前面我们定义的 PV 不同,我们这里定义了一个 local
字段,表明它是一个 Local PV
,而 path 字段,指定的正是这个 PV 对应的本地磁盘的路径,即:/data/volumes/localpv
,这也就意味着如果 Pod 要想使用这个 PV,那它就必须运行在 node1 节点上。所以,在这个 PV 的定义里,添加了一个节点亲和性 nodeAffinity
字段指定 node1 这个节点。这样,调度器在调度 Pod 的时候,就能够知道一个 PV 与节点的对应关系,从而做出正确的选择。
直接创建上面的资源对象:
1 2 3 4 5 $ kubectl apply -f pv-local.yaml persistentvolume/pv-local created $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-local 5Gi RWO Delete Available local-storage 24s
可以看到,这个 PV 创建后,进入了 Available
(可用)状态。这个时候如果按照前面提到的,我们要使用这个 Local PV
的话就需要去创建一个 PVC 和他进行绑定:
1 2 3 4 5 6 7 8 9 10 11 12 kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc-local spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi storageClassName: local-storage
同样要注意声明的这些属性需要和上面的 PV 对应,直接创建这个资源对象:
1 2 3 4 5 $ kubectl apply -f pvc-local.yaml persistentvolumeclaim/pvc-local created $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-local Bound pv-local 5Gi RWO local-storage 38s
可以看到现在 PVC 和 PV 已经处于 Bound
绑定状态了。但实际上这是不符合我们的需求的,比如现在我们的 Pod 声明使用这个 pvc-local,并且我们也明确规定,这个 Pod 只能运行在 node2 这个节点上,如果按照上面我们这里的操作,这个 pvc-local 是不是就和我们这里的 pv-local 这个 Local PV
绑定在一起了,但是这个 PV 的存储卷又在 node1 这个节点上,显然就会出现冲突了,那么这个 Pod 的调度肯定就会失败了,所以我们在使用 Local PV
的时候,必须想办法延迟这个“绑定”
操作。
要怎么来实现这个延迟绑定呢?我们可以通过创建 StorageClass
来指定这个动作,在 StorageClass 种有一个 volumeBindingMode=WaitForFirstConsumer
的属性,就是告诉 Kubernetes 在发现这个 StorageClass 关联的 PVC 与 PV 可以绑定在一起,但不要现在就立刻执行绑定操作(即:设置 PVC 的 VolumeName 字段),而是要等到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。
所以我们需要创建对应的 StorageClass
对象:
1 2 3 4 5 6 7 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer
这个 StorageClass
的名字,叫作 local-storage,也就是我们在 PV 中声明的,需要注意的是,在它的 provisioner
字段,我们指定的是 no-provisioner
。这是因为我们这里是手动创建的 PV,所以不需要动态来生成 PV,另外这个 StorageClass 还定义了一个 volumeBindingMode=WaitForFirstConsumer
的属性,它是 Local PV
里一个非常重要的特性,即:延迟绑定 。通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。
现在我们来创建这个 StorageClass 资源对象:
1 2 $ kubectl apply -f local-storageclass.yaml storageclass.storage.k8s.io/local-storage created
现在我们重新删除上面声明的 PVC 对象,重新创建:
1 2 3 4 5 6 7 $ kubectl delete -f pvc-local.yaml persistentvolumeclaim "pvc-local" deleted $ kubectl create -f pvc-local.yaml persistentvolumeclaim/pvc-local created $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-local Pending local-storage 3s
我们可以发现这个时候,集群中即使已经存在了一个可以与 PVC 匹配的 PV 了,但这个 PVC 依然处于 Pending
状态,也就是等待绑定的状态,这就是因为上面我们配置的是延迟绑定,需要在真正的 Pod 使用的时候才会来做绑定。
同样我们声明一个 Pod 来使用这里的 pvc-local 这个 PVC,资源对象如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: v1 kind: Pod metadata: name: pv-local-pod spec: volumes: - name: example-pv-local persistentVolumeClaim: claimName: pvc-local containers: - name: example-pv-local image: nginx ports: - containerPort: 80 volumeMounts: - mountPath: /usr/share/nginx/html name: example-pv-local
直接创建这个 Pod:
1 2 $ kubectl apply -f pv-local-pod.yaml pod/pv-local-pod created
创建完成后我们这个时候去查看前面我们声明的 PVC,会立刻变成 Bound
状态,与前面定义的 PV 绑定在了一起:
1 2 3 $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-local Bound pv-local 5Gi RWO local-storage 4m59s
这时候,我们可以尝试在这个 Pod 的 Volume 目录里,创建一个测试文件,比如:
1 2 3 $ kubectl exec -it pv-local-pod /bin/sh # cd /usr/share/nginx/html# echo "Hello from Kubernetes local pv storage" > test.txt
然后,登录到 node1 这台机器上,查看一下它的 /data/volumes/localpv
目录下的内容,你就可以看到刚刚创建的这个文件:
1 2 3 4 5 # 在k8s-node1节点上 $ ls /data/volumes/localpvtest.txt $ cat /data/volumes/localpv/test.txtHello from Kubernetes local pv storage
如果重新创建这个 Pod 的话,就会发现,我们之前创建的测试文件,依然被保存在这个持久化 Volume 当中:
1 2 3 4 5 6 7 8 $ kubectl delete -f pv-local-pod.yaml $ kubectl apply -f pv-local-pod.yaml $ kubectl exec -it pv-local-pod /bin/sh # ls /usr/share/nginx/htmltest.txt # cat /usr/share/nginx/html/test.txtHello from Kubernetes local pv storage #
到这里就说明基于本地存储的 Volume 是完全可以提供容器持久化存储功能的,对于 StatefulSet 这样的有状态的资源对象,也完全可以通过声明 Local 类型的 PV 和 PVC,来管理应用的存储状态。
需要注意的是,我们上面手动创建 PV 的方式,即静态的 PV 管理方式,在删除 PV 时需要按如下流程执行操作:
删除使用这个 PV 的 Pod 从宿主机移除本地磁盘 删除 PVC 删除 PV 如果不按照这个流程的话,这个 PV 的删除就会失败。
网络存储卷 NFS安装 安装配置 nfs
1 2 3 4 5 # 服务端安装软件 $ yum -y install nfs-utils rpcbind # 所有node节点安装nfs客户端相关软件包 $ yum -y install nfs-utils
共享目录设置权限:
1 2 $ mkdir -p /data/nfs-volume/$ chmod 755 /data/nfs-volume/
配置 nfs,nfs 的默认配置文件在 /etc/exports
文件下,在该文件中添加下面的配置信息:
1 2 $ vi /etc/exports /data/nfs-volume/ 10.1.1.0/24(rw,sync,no_root_squash)
配置说明:
/data/nfs-volume/
:是共享的数据目录*:表示任何人都有权限连接,当然也可以是一个网段,一个 IP,也可以是域名 rw:读写的权限 sync:表示文件同时写入硬盘和内存 no_root_squash:当登录 NFS 主机使用共享目录的使用者是 root 时,其权限将被转换成为匿名使用者,通常它的 UID 与 GID,都会变成 nobody 身份 1 2 3 4 5 $ systemctl enable --now nfs-server $ rpcinfo -p|grep nfs $ showmount -e 10.1.1.11 Export list for 10.1.1.11: /data/nfs-volume 10.1.1.0/24
使用NFS 同样创建一个如下所示 nfs 类型的 PV 资源对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: storageClassName: manual capacity: storage: 1Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain nfs: path: /data/nfs-volume/ server: 10.1 .1 .11 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
要知道用户真正使用的是 PVC,而要使用 PVC 的前提就是必须要先和某个符合条件的 PV 进行一一绑定,比如存储容器、访问模式,以及 PV 和 PVC 的 storageClassName 字段必须一样,这样才能够进行绑定,当 PVC 和 PV 绑定成功后就可以直接使用这个 PVC 对象了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 apiVersion: v1 kind: Pod metadata: name: nfs-volumes spec: volumes: - name: nfs persistentVolumeClaim: claimName: nfs-pvc containers: - name: web image: nginx:alpine ports: - name: web containerPort: 80 volumeMounts: - name: nfs subPath: nfs-volumes mountPath: '/usr/share/nginx/html'
直接创建上面的资源对象即可:
1 2 3 4 5 6 7 8 9 10 11 $ kubectl apply -f nfs-volume.yaml $ kubectl apply -f nfs-pod.yaml $ kubectl get pv nfs-pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE nfs-pv 1Gi RWO Retain Bound default/nfs-pvc manual 2m35s $ kubectl get pvc nfs-pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE nfs-pvc Bound nfs-pv 1Gi RWO manual 3m18s $ kubectl get pods nfs-volumes -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nfs-volumes 1/1 Running 0 2m53s 172.17.130.16 k8s-node2 <none> <none>
我们可以在 PV 目录中添加一些内容:
1 2 3 4 5 6 7 # 在 nfs 服务器上面执行 echo -e "by wangxiansen\nnfs pv content." > /data/nfs-volume/nfs-volumes/index.html # 在集群中请求这个pods curl 172.17.130.16 by wangxiansen nfs pv content.
NFS存储动态供给 PV对存储系统的支持可通过其插件来实现,目前,Kubernetes支持如下类型的插件。
官方地址:https://kubernetes.io/docs/concepts/storage/storage-classes/
官方插件是不支持NFS动态供给的,但是我们可以用第三方的插件来实现
第三方插件地址: https://github.com/kubernetes-sigs/sig-storage-lib-external-provisioner
1 2 3 4 # 批量下载文件: for file in class.yaml deployment.yaml rbac.yaml ; do wget https://raw.githubusercontent.com/kubernetes-sigs/nfs-subdir-external-provisioner/master/deploy/$file ; done # 修改默认安装名称空间 sed -i 's/namespace: default/namespace: kube-system/g' *
StorageClass RBAC Deployment 1 2 3 4 5 6 7 8 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: boysec.cn/nfs-external-provisioner parameters: archiveOnDelete: "false"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 vim storageclass-nfs-rbac.yaml apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner namespace: kube-system --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: nfs-client-provisioner-runner rules: - apiGroups: ["" ] resources: ["nodes" ] verbs: ["get" , "list" , "watch" ] - 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: ["create" , "update" , "patch" ] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: run-nfs-client-provisioner subjects: - kind: ServiceAccount name: nfs-client-provisioner namespace: kube-system roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.io --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: leader-locking-nfs-client-provisioner namespace: kube-system rules: - apiGroups: ["" ] resources: ["endpoints" ] verbs: ["get" , "list" , "watch" , "create" , "update" , "patch" ] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: leader-locking-nfs-client-provisioner namespace: kube-system subjects: - kind: ServiceAccount name: nfs-client-provisioner namespace: kube-system roleRef: kind: Role name: leader-locking-nfs-client-provisioner apiGroup: rbac.authorization.k8s.io
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 apiVersion: apps/v1 kind: Deployment metadata: name: nfs-client-provisioner labels: app: nfs-client-provisioner namespace: kube-system 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: registry.cn-beijing.aliyuncs.com/pylixm/nfs-subdir-external-provisioner:v4.0.0 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: boysec.cn/nfs-external-provisioner - name: NFS_SERVER value: 10.1 .1 .11 - name: NFS_PATH value: /data/nfs-volume/ volumes: - name: nfs-client-root nfs: server: 10.1 .1 .11 path: /data/nfs-volume/
应用资源配置清单文件
1 2 3 4 5 6 7 8 9 10 11 kubectl apply -f storageclass-nfs.yaml kubectl apply -f storageclass-nfs-rbac.yaml kubectl apply -f deployment-nfs.yaml $ kubectl get storageclass NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE nfs-client boysec.cn/nfs-external-provisioner Delete Immediate false 7m26s # RECLAIMPOLICY pv回收策略,pod或pvc被删除后,pv是否删除还是保留。 # VOLUMEBINDINGMODE Immediate 模式下PVC与PV立即绑定,主要是不等待相关Pod调度完成,不关心其运行节点,直接完成绑定。相反的 WaitForFirstConsumer模式下需要等待Pod调度完成后进行PV绑定。 # ALLOWVOLUMEEXPANSION pvc扩容
测试存储动态供给 一旦在Kubernetes集群中成功安装了NFS动态存储,下面是进行验证和测试其功能的步骤。
步骤一:创建NFS动态存储的PVC
首先,可以创建一个NFS动态存储的持久化存储声明(PVC)。确保PVC能够成功地与NFS服务器建立连接并分配所需的存储资源。
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc spec: storageClassName: nfs-client accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
应用这个pvc资源
1 2 3 4 5 6 7 8 9 10 11 $ kubectl apply -f provisioner-pvc.yaml persistentvolumeclaim/nfs-provisioner-pvc created $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE nfs-provisioner-pvc Bound pvc-edb5db37-853a-49f6-9d31-310f89a753b5 1Gi RWO nfs-client 4s # 可以看到创建成功后自动就生成新的pv资源并绑定到这个pv上。 $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-edb5db37-853a-49f6-9d31-310f89a753b5 1Gi RWO Delete Bound default/nfs-provisioner-pvc nfs-client 82s
步骤二:部署Pod并使用NFS存储
随后,在Kubernetes中部署一个测试Pod,并将其连接到之前创建的NFS动态存储。通过在Pod中写入、读取数据来验证NFS存储是否正常工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: v1 kind: Pod metadata: name: nfs-provisioner-volumes spec: volumes: - name: nfs persistentVolumeClaim: claimName: nfs-provisioner-pvc containers: - name: web image: nginx:alpine ports: - name: web containerPort: 80 volumeMounts: - name: nfs mountPath: '/usr/share/nginx/html'
应用测试Pod,检测pod是否正常。
1 2 3 4 $ kubectl apply -f nfs-provisioner-pod.yaml $ kubectl get pods nfs-provisioner-volumes -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nfs-provisioner-volumes 1/1 Running 0 60s 172.17.130.19 k8s-node2 <none> <none>
在 PV 目录中添加一些内容:
1 2 3 4 5 6 7 # 在 nfs 服务器上面执行,注意后面目录是随机生成的,每个都不一样 echo -e "by wangxiansen\nNFS存储动态供给YYDS." > /data/nfs-volume/default-nfs-provisioner-pvc-pvc-edb5db37-853a-49f6-9d31-310f89a753b5/index.html # 在集群中请求这个pods curl 172.17.130.19 by wangxiansen NFS存储动态供给YYDS.