Kubernetes运维-持久化存储卷实践与管理

存储卷介绍

Kubernetes 使用存储的原因

  • Kubernetes 中的副本控制器保证了pod的始终存储,却保证不了pod中的数据。只有启动一个新pod的,之前pod中的数据会随着容器的删掉而丢失!

共享存储机制

  • Kubernetes 对于有状态的容器应用或者对数据需要持久化的应用,不仅需要将容器内的目录挂载到宿主机的目录或者emptyDir临时存储卷,而且需要更加可靠的存储来保存应用产生的重要数据,以便容器应用在重建之后,仍然可以使用之前的数据。Kubernetes引入持久化存储卷(PV)持久化存储声明(PVC)两个资源对象实现对存储的管理子系统。

概念

PV 的全称是:PersistentVolume(持久化卷),是对底层共享存储的一种抽象,PV 由管理员进行创建和配置,是一个全局资源,包含存储的类型,存储的大小和访问模式等。它和具体的底层的共享存储技术的实现方式有关,比如 CephGlusterFSNFShostPath 等,都是通过插件机制完成与共享存储的对接。

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

  • 应用场景

    • 实现pod内容器之间数据共享
  • 特点

    • 随着pod被删除,该卷也会被删除

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
# vim volume-emptydir.yml
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
# vim pv-hostpath.yaml
apiVersion: v1
kind: PersistentVolume # 类型为PersistentVolume(pv)
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。

不过需要注意的是,目前只有 NFSHostPath 两种类型支持回收策略,当然一般来说还是设置为 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
# vim pvc-hostpath.yaml
apiVersion: v1
kind: PersistentVolumeClaim # 类型为PersistentVolumeClaim(pvc)
metadata:
name: pvc-hostpath # pvc的名称
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,比如 storageClassNameaccessModes 以及容量这些是否满足要求,如果满足要求就会将 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
# vim pv-hostpath-pod.yaml
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
# pv-local.yaml
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 # node1节点上的目录
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
# pvc-local.yaml
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
# local-storageclass.yaml
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
# pv-local-pod.yaml
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/localpv
test.txt
$ cat /data/volumes/localpv/test.txt
Hello 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/html
test.txt
# cat /usr/share/nginx/html/test.txt
Hello 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
# vim nfs-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
nfs:
path: /data/nfs-volume/ # 指定nfs的挂载点
server: 10.1.1.11 # 指定nfs服务地址
---
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
# vim nfs-pod.yaml
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' *
1
2
3
4
5
6
7
8
# vim  storageclass-nfs.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass # 类型
metadata:
name: nfs-client # 名称,
provisioner: boysec.cn/nfs-external-provisioner # 动态供给插件
parameters:
archiveOnDelete: "false" # 删除数据时是否存档,false表示不存档,true表示存档
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
# replace with namespace where provisioner is deployed
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
# replace with namespace where provisioner is deployed
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
# replace with namespace where provisioner is deployed
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
# replace with namespace where provisioner is deployed
namespace: kube-system
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
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
# vim  deployment-nfs.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
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
# vim provisioner-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
storageClassName: nfs-client # 关联storageClass对象,通过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
# vim nfs-provisioner-pod.yaml
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.