多读书多实践,勤思考善领悟

kubernetes持久化存储方案PV和PVC、StorageClass的使用

本文于2049天之前发表,文中内容可能已经过时。

1. PV 和 PVC 的使用

通过 hostPath 或者 emptyDir 的方式来持久化我们的数据,但是显然我们还需要更加可靠的存储来保存应用的持久化数据,这样容器在重建后,依然可以使用之前的数据。但是显然存储资源和 CPU 资源以及内存资源有很大不同,为了屏蔽底层的技术实现细节,让用户更加方便的使用,Kubernetes 便引入了 PVPVC 两个重要的资源对象来实现对存储的管理。

概念

PV 的全称是:PersistentVolume(持久化卷),是对底层的共享存储的一种抽象,PV 由管理员进行创建和配置,它和具体的底层的共享存储技术的实现方式有关,比如 Ceph、GlusterFS、NFS 等,都是通过插件机制完成与共享存储的对接。

PVC 的全称是:PersistentVolumeClaim(持久化卷声明),PVC 是用户存储的一种声明,PVC 和 Pod 比较类似,Pod 消耗的是节点,PVC 消耗的是 PV 资源,Pod 可以请求 CPU 和内存,而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用 PVC 即可。

但是通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求,而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。

NFS

我们这里为了演示方便,决定使用相对简单的 NFS 这种存储资源,接下来我们在节点10.151.30.57上来安装 NFS 服务,数据目录:/data/k8s/

  1. 关闭防火墙
1
2
$ systemctl stop firewalld.service
$ systemctl disable firewalld.service
  1. 安装配置 nfs
1
$ yum -y install nfs-utils rpcbind

共享目录设置权限:

1
$ chmod 755 /data/k8s/

配置 nfs,nfs 的默认配置文件在 /etc/exports 文件下,在该文件中添加下面的配置信息:

1
2
$ vi /etc/exports
/data/k8s *(rw,sync,no_root_squash)

配置说明:

  • /data/k8s:是共享的数据目录
  • *:表示任何人都有权限连接,当然也可以是一个网段,一个 IP,也可以是域名
  • rw:读写的权限
  • sync:表示文件同时写入硬盘和内存
  • no_root_squash:当登录 NFS 主机使用共享目录的使用者是 root 时,其权限将被转换成为匿名使用者,通常它的 UID 与 GID,都会变成 nobody 身份

当然 nfs 的配置还有很多,感兴趣的同学可以在网上去查找一下。

  1. 启动服务 nfs 需要向 rpc 注册,rpc 一旦重启了,注册的文件都会丢失,向他注册的服务都需要重启

注意启动顺序,先启动 rpcbind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ systemctl start rpcbind.service
$ systemctl enable rpcbind
$ systemctl status rpcbind
● rpcbind.service - RPC bind service
Loaded: loaded (/usr/lib/systemd/system/rpcbind.service; disabled; vendor preset: enabled)
Active: active (running) since Tue 2018-07-10 20:57:29 CST; 1min 54s ago
Process: 17696 ExecStart=/sbin/rpcbind -w $RPCBIND_ARGS (code=exited, status=0/SUCCESS)
Main PID: 17697 (rpcbind)
Tasks: 1
Memory: 1.1M
CGroup: /system.slice/rpcbind.service
└─17697 /sbin/rpcbind -w

Jul 10 20:57:29 master systemd[1]: Starting RPC bind service...
Jul 10 20:57:29 master systemd[1]: Started RPC bind service.

看到上面的 Started 证明启动成功了。

然后启动 nfs 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ systemctl start nfs.service
$ systemctl enable nfs
$ systemctl status nfs
● nfs-server.service - NFS server and services
Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; vendor preset: disabled)
Drop-In: /run/systemd/generator/nfs-server.service.d
└─order-with-mounts.conf
Active: active (exited) since Tue 2018-07-10 21:35:37 CST; 14s ago
Main PID: 32067 (code=exited, status=0/SUCCESS)
CGroup: /system.slice/nfs-server.service

Jul 10 21:35:37 master systemd[1]: Starting NFS server and services...
Jul 10 21:35:37 master systemd[1]: Started NFS server and services.

同样看到 Started 则证明 NFS Server 启动成功了。

另外我们还可以通过下面的命令确认下:

1
2
3
4
5
6
7
$ rpcinfo -p|grep nfs
100003 3 tcp 2049 nfs
100003 4 tcp 2049 nfs
100227 3 tcp 2049 nfs_acl
100003 3 udp 2049 nfs
100003 4 udp 2049 nfs
100227 3 udp 2049 nfs_acl

查看具体目录挂载权限:

1
2
$ cat /var/lib/nfs/etab
/data/k8s *(rw,sync,wdelay,hide,nocrossmnt,secure,no_root_squash,no_all_squash,no_subtree_check,secure_locks,acl,no_pnfs,anonuid=65534,anongid=65534,sec=sys,secure,no_root_squash,no_all_squash)

到这里我们就把 nfs server 给安装成功了,接下来我们在节点10.151.30.62上来安装 nfs 的客户端来验证下 nfs

  1. 安装 nfs 当前也需要先关闭防火墙: shell $ systemctl stop firewalld.service $ systemctl disable firewalld.service

然后安装 nfs

1
$ yum -y install nfs-utils rpcbind

安装完成后,和上面的方法一样,先启动 rpc、然后启动 nfs:

1
2
3
4
$ systemctl start rpcbind.service
$ systemctl enable rpcbind.service
$ systemctl start nfs.service
$ systemctl enable nfs.service
  1. 挂载数据目录 客户端启动完成后,我们在客户端来挂载下 nfs 测试下:

首先检查下 nfs 是否有共享目录:

1
2
3
$ showmount -e 10.151.30.57
Export list for 10.151.30.57:
/data/k8s *

然后我们在客户端上新建目录:

1
$ mkdir -p /root/course/kubeadm/data

将 nfs 共享目录挂载到上面的目录:

1
$ mount -t nfs 10.151.30.57:/data/k8s /root/course/kubeadm/data

挂载成功后,在客户端上面的目录中新建一个文件,然后我们观察下 nfs 服务端的共享目录下面是否也会出现该文件:

1
$ touch /root/course/kubeadm/data/test.txt

然后在 nfs 服务端查看:

1
2
3
$ ls -ls /data/k8s/
total 4
4 -rw-r--r--. 1 root root 4 Jul 10 21:50 test.txt

如果上面出现了 test.txt 的文件,那么证明我们的 nfs 挂载成功了。

PV

有了上面的 NFS 共享存储,下面我们就可以来使用 PV 和 PVC 了。PV 作为存储资源,主要包括存储能力、访问模式、存储类型、回收策略等关键信息,下面我们来新建一个 PV 对象,使用 nfs 类型的后端存储,1G 的存储空间,访问模式为 ReadWriteOnce,回收策略为 Recyle,对应的 YAML 文件如下:(pv1-demo.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv1
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
nfs:
path: /data/k8s
server: 10.151.30.57

Kubernetes 支持的 PV 类型有很多,比如常见的 Ceph、GlusterFs、NFS,甚至 HostPath也可以,不过 HostPath 我们之前也说过仅仅可以用于单机测试,更多的支持类型可以前往 Kubernetes PV 官方文档进行查看,因为每种存储类型都有各自的特点,所以我们在使用的时候可以去查看相应的文档来设置对应的参数。

然后同样的,直接使用 kubectl 创建即可:

1
2
3
4
5
$ kubectl create -f pv1-demo.yaml
persistentvolume "pv1" created
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv1 1Gi RWO Recycle Available 12s

我们可以看到 pv1 已经创建成功了,状态是 Available,表示 pv1 就绪,可以被 PVC 申请。我们来分别对上面的属性进行一些解读。

Capacity(存储能力)

一般来说,一个 PV 对象都要指定一个存储能力,通过 PV 的 capacity属性来设置的,目前只支持存储空间的设置,就是我们这里的 storage=1Gi,不过未来可能会加入 IOPS、吞吐量等指标的配置。

AccessModes(访问模式)

AccessModes 是用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:

  • ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载
  • ReadOnlyMany(ROX):只读权限,可以被多个节点挂载
  • ReadWriteMany(RWX):读写权限,可以被多个节点挂载

注意:一些 PV 可能支持多种访问模式,但是在挂载的时候只能使用一种访问模式,多种访问模式是不会生效的。

下图是一些常用的 Volume 插件支持的访问模式:

volume-accessmodes

persistentVolumeReclaimPolicy(回收策略)

我这里指定的 PV 的回收策略为 Recycle,目前 PV 支持的策略有三种:

  • Retain(保留)- 保留数据,需要管理员手工清理数据
  • Recycle(回收)- 清除 PV 中的数据,效果相当于执行 rm -rf /thevoluem/*
  • Delete(删除)- 与 PV 相连的后端存储完成 volume 的删除操作,当然这常见于云服务商的存储服务,比如 ASW EBS。

不过需要注意的是,目前只有 NFS 和 HostPath 两种类型支持回收策略。当然一般来说还是设置为 Retain 这种策略保险一点。

状态

一个 PV 的生命周期中,可能会处于4中不同的阶段:

  • Available(可用):表示可用状态,还未被任何 PVC 绑定
  • Bound(已绑定):表示 PV 已经被 PVC 绑定
  • Released(已释放):PVC 被删除,但是资源还未被集群重新声明
  • Failed(失败): 表示该 PV 的自动回收失败

我们平时真正使用的资源其实是 PVC,就类似于我们的服务是通过 Pod 来运行的,而不是 Node,只是 Pod 跑在 Node 上而已,所以接下来我们就来给大家讲解下 PVC 的使用方法。

准备工作

在使用 PVC 之前,我们还得把其他节点上的 nfs 客户端给安装上,比如我们这里:

1
2
3
4
5
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready master 61d v1.10.0
node01 Ready <none> 61d v1.10.0
node03 Ready <none> 41d v1.10.0

我们需要在所有节点安装 nfs 客户端程序,安装方法和上面的安装方法一样的。必须在所有节点都安装 nfs 客户端,否则可能会导致 PV 挂载不上的问题

新建 PVC

同样的,我们来新建一个数据卷声明,我们来请求 1Gi 的存储容量,访问模式也是 ReadWriteOnce,YAML 文件如下:(pvc-nfs.yaml)

1
2
3
4
5
6
7
8
9
10
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-nfs
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

我们可以看到我们这里的声明方法几乎和新建 PV 是一样的,在新建 PVC 之前,我们可以看下之前创建的 PV 的状态:

1
2
3
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs 1Gi RWO Recycle Available 19m

我们可以看到当前 pv-nfs 是在 Available 的一个状态,所以这个时候我们的 PVC 可以和这个 PV 进行绑定:

1
2
3
4
5
$ kubectl create -f pvc-nfs.yaml
persistentvolumeclaim "pvc-nfs" created
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nfs Bound pv-nfs 1Gi RWO 12s

我们可以看到 pvc-nfs 创建成功了,状态是 Bound 状态了,这个时候我们再看下 PV 的状态呢:

1
2
3
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs 1Gi RWO Recycle Bound default/pvc-nfs 23m

同样我们可以看到 PV 也是 Bound 状态了,对应的声明是 default/pvc-nfs,就是 default 命名空间下面的 pvc-nfs,证明我们刚刚新建的 pvc-nfs 和我们的 pv-nfs 绑定成功了。

有的同学可能会觉得很奇怪,我们并没有在 pvc-nfs 中指定关于 pv 的什么标志,它们之间是怎么就关联起来了的呢?其实这是系统自动帮我们去匹配的,他会根据我们的声明要求去查找处于 Available 状态的 PV,如果没有找到的话那么我们的 PVC 就会一直处于 Pending 状态,找到了的话当然就会把当前的 PVC 和目标 PV 进行绑定,这个时候状态就会变成 Bound 状态了。比如我们新建一个 PVC,如下:(pvc2-nfs.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc2-nfs
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
selector:
matchLabels:
app: nfs

我们这里声明一个 PV 资源的请求,邀请访问模式是 ReadWriteOnce,存储容量是 2Gi,最后我们还要求匹配具有标签 app=nfs 的 PV,这样要求的 PV 有吗?我们先查看下当前系统的所有 PV:

1
2
3
4
5
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs 1Gi RWO Recycle Bound default/pvc-nfs 43m
pv001 1Gi RWO Recycle Bound default/www-web-0 13d
pv002 1Gi RWO Recycle Bound default/www-web-1 13d

都是 Bound 状态,并没有 Available 状态的 PV,所以我们可以想象到我们上面新建的 PVC 是没办法选择到合适的 PV 的,我们创建一下看看:

1
2
3
4
5
6
$ kubectl create -f pvc2-nfs.yaml
persistentvolumeclaim "pvc2-nfs" created
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nfs Bound pv-nfs 1Gi RWO 23m
pvc2-nfs Pending 14s

很显然是 Pending 状态,因为并没有合适的 PV 给你使用,现在我们来新建一个 PV,让上面的 PVC 有合适的 PV 使用:(pv2-nfs.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv2-nfs
labels:
app: nfs
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
nfs:
server: 10.151.30.57
path: /data/k8s

我们这里新建一个名为 pv2-nfs 的 PV,具有标签 app=nfs,容量也是 2Gi,访问模式是 ReadWraiteOnce,看上去这一切都很适合上面的 PVC,新建试一试:

1
2
3
4
5
6
$ kubectl create -f pv2-nfs.yaml
persistentvolume "pv2-nfs" created
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs 1Gi RWO Recycle Bound default/pvc-nfs 51m
pv2-nfs 2Gi RWO Recycle Bound default/pvc2-nfs 12s

创建完 pv2-nfs 后,是不是很快就发现该 PV 是 Bound 状态了,对应的 PVC 是 default/pvc2-nfs,证明上面的 pvc2-nfs 终于找到合适的 PV 进行绑定上了:

1
2
3
4
5
$ kubectl get pvc
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nfs Bound pv-nfs 1Gi RWO 30m
pvc2-nfs Bound pv2-nfs 2Gi RWO 7m

成功了,对吧!有的同学可能又会说了,我们的 pv2-nfs 声明的容量是 2Gi,如果我 pvc2-nfs 这里声明的容量是 1Gi 的话呢?还能正常绑定吗?如果可以正常绑定的话,那剩下的 1Gi 容量还能使用吗?其实我也不清楚,怎么办?我们去实际测试下就知道了吧,先删除上面的 pvc2-nfs,然后我们把该 PVC 里面的容量改成 1Gi,再新建试一试呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl delete pvc pvc2-nfs
persistentvolumeclaim "pvc2-nfs" deleted
$ cat pvc2-nfs.yaml
...
resources:
requests:
storage: 1Gi
...
$ kubectl create -f pvc2-nfs.yaml
persistentvolumeclaim "pvc2-nfs" created
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc2-nfs Bound pv2-nfs 2Gi RWO 7s

我们可以看到上面的 PVC 依然可以正常的绑定,仔细看 CAPACITY 这一列的数据:2Gi,也就是说我们声明的 1Gi 是没什么用的,我 PV 是 2Gi,你这里声明 1Gi 是不行的,你必须得使用 2Gi。

如果我们这里容量声明是 3Gi 呢?还可以正常绑定吗?大家可以思考一下,如果声明的容量大于了 PV 里面的容量的话,是没办法进行绑定的,大家可以下去自己测试一下。

使用 PVC

上面我们已经知道怎么创建 PV 和 PVC 了,现在我们就来使用下我们的 PVC,这里我们同样使用之前的 nginx 的镜像来测试下:(nfs-pvc-deploy.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nfs-pvc
spec:
replicas: 3
template:
metadata:
labels:
app: nfs-pvc
spec:
containers:
- name: nginx
image: nginx:1.7.9
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumes:
- name: www
persistentVolumeClaim:
claimName: pvc2-nfs

---

apiVersion: v1
kind: Service
metadata:
name: nfs-pvc
labels:
app: nfs-pvc
spec:
type: NodePort
ports:
- port: 80
targetPort: web
selector:
app: nfs-pvc

我们这里使用 nginx 镜像,将容器的 /usr/share/nginx/html 目录通过 volume 挂载到名为 pvc2-nfs 的 PVC 上面,然后创建一个 NodePort 类型的 Service 来暴露服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ kubectl create -f nfs-pvc-deploy.yaml
deployment.extensions "nfs-pvc" created
service "nfs-pvc" created
$ kubectl get pods
kubectl get pods
NAME READY STATUS RESTARTS AGE
...
nfs-pvc-57c9945bd9-5r4r6 1/1 Running 0 19s
nfs-pvc-57c9945bd9-gz6p9 1/1 Running 0 19s
nfs-pvc-57c9945bd9-x6mvc 1/1 Running 0 19s
...
$ kubectl get svc
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
...
nfs-pvc NodePort 10.98.246.155 <none> 80:30769/TCP 1m
...

然后我们就可以通过任意节点的 IP:30769 端口来访问我们这里的 Nginx 服务了,但是这个时候我们来访问会出现403,这是为什么?我们再去看看 nfs 共享数据目录下面有没有数据呢?

nginx 403
nginx 403
1
$ ls /data/k8s

我们发现并没有任何数据,这是因为我们把容器目录/user/share/nginx/html和挂载到了pvc2-nfs这个 PVC 上面,这个 PVC 就是对应着我们上面的 nfs 的共享数据目录的,该目录下面还没有任何数据,所以我们访问就出现了403,现在我们在/data/k8s这个目录下面新建一个 index.html 的文件:

1
2
3
$ echo "<h1>Hello Kubernetes~</h1>" >> /data/k8s/index.html
$ ls /data/k8s/
index.html

我们可以看到共享数据目录中已经有一个 index.html 的文件了,由于我们挂载了 pvc2-nfs 到上面的 nginx 容器中去,是不是这个时候容器目录/user/share/nginx/html下面也有index.html这个文件了啊?所以这个时候我们再来访问下服务,任一节点IP:30769:

nginx 200
nginx 200

现在是不是正常了啊,但是我们可以看到我们容器中的数据是直接放到共享数据目录根目录下面的,如果以后我们又有一个新的 nginx 容器也做了数据目录的挂载,是不是就会有冲突了啊,所以这个时候就不太好区分了,这个时候我们可以在 Pod 中使用一个新的属性:subPath,该属性可以来解决这个问题,我们只需要更改上面的 Pod 的 YAML 文件即可:

1
2
3
4
5
6
...
volumeMounts:
- name: www
subPath: nginxpvc-test
mountPath: /usr/share/nginx/html
...

更改完 YAML 文件后,我们重新更新即可:

1
2
3
4
5
$ kubectl apply -f nfs-pvc-deploy.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
deployment.extensions "nfs-pvc" configured
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
service "nfs-pvc" configured

更新完后,我们再去看看 nfs 的数据共享目录:

1
2
3
$ ls /data/k8s/
index.html nginxpvc-test
$ ls /data/k8s/nginxpvc-test/

我们可以预想到现在我们访问上面的服务,是不是又会得到403的结果啊,因为nginxpvc-test目录下面还没有任何文件呢,我们把根目录下面的 index.html 文件一到到 nginxpvc-test 目录下面去是不是又可以访问了:

1
$ mv /data/k8s/index.html /data/k8s/nginxpvc-test/

现在快去验证下吧,看看能不能得到正确结果。

到这里我们就算完整的使用了一次 PVC 了,现在我们再来验证下我们的数据是否会丢失,怎么验证?首先我们把上面的 Deployment 删除掉,这样是不是他下面管理的3个 Pod 也会被一起删除掉啊:

1
2
$ kubectl delete deployment nfs-pvc
deployment.extensions "nfs-pvc" deleted

Deployment 被删除掉了,但是 nfs 的数据共享目录下面的数据呢?

1
2
$ ls /data/k8s/nginxpvc-test/
index.html

还在吧?当然了如果不在了,我们用他就没有任何意义了吧,现在我们再来重新创建上面的 Deployment,看看访问服务还能得到上面的正常输出结果吗:

1
2
3
$ kubectl create -f nfs-pvc-deploy.yaml
deployment.extensions "nfs-pvc" created
Error from server (AlreadyExists): error when creating "nfs-pvc-deploy.yaml": services "nfs-pvc" already exists

可以看到 nfs-pvc 这个 Deployment 创建成功了,由于 Service 我们之前没有删除掉,所以这里提示已经存在,我们忽略就可以了,现在同样我们用任一节点 IP:30769 来访问我们这里的服务,是不是依然可以在页面上看到Hello Kubernetes~这里的输出信息啊,这证明我们的数据持久化是成功的吧!

注意事项

上面我们演示了数据持久化,如果这个时候我们把 PV 给删除了,上面持久化的数据还会存在吗?如果是删除的 PVC 呢?在实际使用的工程中,是很有可能出现这种情况的吧?下面我们来实际验证下。

我们先删除上面使用的 PV:

1
2
$ kubectl delete pv pv2-nfs
persistentvolume "pv2-nfs" deleted

然后再看看之前创建的 PVC 还会存在吗:

1
2
3
4
5
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
...
pvc2-nfs Bound pv2-nfs 2Gi RWO 1h
...

是不是觉得很奇怪,pvc2-nfs 仍然是 Bound 的状态,也就意外着我们还可以正常使用这个 PVC,但是如果我们有一个新的 Pod 来使用这个 PVC 会是怎样的情况呢?大家下去自己验证下

如有 Pod 正在使用此 pvc2-nfs 这个 PVC 的话,那么新建的 Pod 则仍可使用,如无 Pod 使用,则创建 Pod 挂载此 PVC 时会出现失败。大家自己去验证下吧

现在我们在恢复到最开始的状态,把 PV 和 PVC 添加回来,如果现在我们把使用 pvc2-nfs 关联的 Pod 都删除,然后再删除该 PVC 的话,那么我们的持久化数据还存在吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ kubectl delete -f nfs-pvc-deploy.yaml
deployment.extensions "nfs-pvc" deleted
service "nfs-pvc" deleted
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
...
pvc2-nfs Bound pv2-nfs 2Gi RWO 5m
...
$ kubectl delete pvc pvc2-nfs
persistentvolumeclaim "pvc2-nfs" deleted
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
...
pv2-nfs 2Gi RWO Recycle Released default/pvc2-nfs 6m
...
$ ls /data/k8s/

我们可以看到 pv2-nfs 这个 PV 的状态已经变成了 Released 状态了,这个状态是不是表示 PVC 已经被释放了,现在可以被重新绑定了,由于我们设置的 PV 的回收策略是 Recycle,所以我们可以很明显的发现 nfs 的共享数据目录下面已经没有了数据了,这是因为我们把 PVC 给删除掉了,然后回收了数据。

不过大家要注意,并不是所有的存储后端的表现结果都是这样的,我们这里使用的是 nfs,其他存储后端肯能会有不一样的结果。

大家在使用 PV 和 PVC 的时候一定要注意这些细节,不然一不小心就把数据搞丢了。

2. StorageClass 的使用

前面介绍了 PVPVC 的使用方法,但是前面的 PV 都是静态的,什么意思?就是我要使用的一个 PVC 的话就必须手动去创建一个 PV,我们也说过这种方式在很大程度上并不能满足我们的需求,比如我们有一个应用需要对存储的并发度要求比较高,而另外一个应用对读写速度又要求比较高,特别是对于 StatefulSet 类型的应用简单的来使用静态的 PV 就很不合适了,这种情况下我们就需要用到动态 PV,也就是现在要讲解的 StorageClass

创建 Provisioner

要使用 StorageClass,我们就得安装对应的自动配置程序,比如我们这里存储后端使用的是 nfs,那么我们就需要使用到一个 nfs-client 的自动配置程序,我们也叫它 Provisioner,这个程序使用我们已经配置好的 nfs 服务器,来自动创建持久卷,也就是自动帮我们创建 PV。

  • 自动创建的 PV 以${namespace}-${pvcName}-${pvName}这样的命名格式创建在 NFS 服务器上的共享数据目录中
  • 而当这个 PV 被回收后会以archieved-${namespace}-${pvcName}-${pvName}这样的命名格式存在 NFS 服务器上。

当然在部署nfs-client之前,我们需要先成功安装上 nfs 服务器,前面的课程中我们已经过了,服务地址是10.151.30.57,共享数据目录是/data/k8s/,然后接下来我们部署 nfs-client 即可,我们也可以直接参考nfs-client 的文档,进行安装即可。

第一步:配置 Deployment,将里面的对应的参数替换成我们自己的 nfs 配置(nfs-client.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
27
28
29
30
31
32
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: nfs-client-provisioner
spec:
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: quay.io/external_storage/nfs-client-provisioner:latest
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: fuseim.pri/ifs
- name: NFS_SERVER
value: 10.151.30.57
- name: NFS_PATH
value: /data/k8s
volumes:
- name: nfs-client-root
nfs:
server: 10.151.30.57
path: /data/k8s

第二步:将环境变量 NFS_SERVER 和 NFS_PATH 替换,当然也包括下面的 nfs 配置,我们可以看到我们这里使用了一个名为 nfs-client-provisioner 的serviceAccount,所以我们也需要创建一个 sa,然后绑定上对应的权限:(nfs-client-sa.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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。

第三步:nfs-client 的 Deployment 声明完成后,我们就可以来创建一个StorageClass对象了:(nfs-client-class.yaml)

1
2
3
4
5
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 这个环境变量的值一样。

现在我们来创建这些资源对象吧:

1
2
3
$ kubectl create -f nfs-client.yaml
$ kubectl create -f nfs-client-sa.yaml
$ kubectl create -f nfs-client-class.yaml

创建完成后查看下资源状态:

1
2
3
4
5
6
7
8
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
...
nfs-client-provisioner-7648b664bc-7f9pk 1/1 Running 0 7h
...
$ kubectl get storageclass
NAME PROVISIONER AGE
course-nfs-storage fuseim.pri/ifs 11s

新建 StorageClass

上面把StorageClass资源对象创建成功了,接下来我们来通过一个示例测试下动态 PV,首先创建一个 PVC 对象:(test-pvc.yaml)

1
2
3
4
5
6
7
8
9
10
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属性来标识,如下
1
2
3
4
5
6
7
8
9
10
11
12
kind: PersistentVolumeClaim
apiVersion: v1
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命令来更新: yaml $ kubectl patch storageclass course-nfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

上面这两种方法都是可以的,当然为了不影响系统的默认行为,我们这里还是采用第一种方法,直接创建即可:

1
2
3
4
5
6
7
$ kubectl create -f test-pvc.yaml
persistentvolumeclaim "test-pvc" created
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
...
test-pvc Bound pvc-73b5ffd2-8b4b-11e8-b585-525400db4df7 1Mi RWX course-nfs-storage 2m
...

我们可以看到一个名为 test-pvc 的 PVC 对象创建成功了,状态已经是Bound了,是不是也产生了一个对应的VOLUME 对象,最重要的一栏是STORAGECLASS,现在是不是也有值了,就是我们刚刚创建的StorageClass对象 course-nfs-storage。

然后查看下 PV 对象呢:

1
2
3
4
5
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
...
pvc-73b5ffd2-8b4b-11e8-b585-525400db4df7 1Mi RWX Delete Bound default/test-pvc course-nfs-storage 8m
...

可以看到是不是自动生成了一个关联的 PV 对象,访问模式是RWX,回收策略是 Delete,这个 PV 对象并不是我们手动创建的吧,这是通过我们上面的 StorageClass 对象自动创建的。这就是 StorageClass 的创建方法。

测试

接下来我们还是用一个简单的示例来测试下我们上面用 StorageClass 方式声明的 PVC 对象吧:(test-pod.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 这个文件即可:

1
2
$ kubectl create -f test-pod.yaml
pod "test-pod" created

然后我们可以在 nfs 服务器的共享数据目录下面查看下数据:

1
2
$ ls /data/k8s/
default-test-pvc-pvc-73b5ffd2-8b4b-11e8-b585-525400db4df7

我们可以看到下面有名字很长的文件夹,这个文件夹的命名方式是不是和我们上面的规则:${namespace}-${pvcName}-${pvName}是一样的,再看下这个文件夹下面是否有其他文件:

1
2
$ ls /data/k8s/default-test-pvc-pvc-73b5ffd2-8b4b-11e8-b585-525400db4df7
SUCCESS

我们看到下面有一个 SUCCESS 的文件,是不是就证明我们上面的验证是成功的啊。

另外我们可以看到我们这里是手动创建的一个 PVC 对象,在实际工作中,使用 StorageClass 更多的是 StatefulSet 类型的服务,StatefulSet类型的服务我们也可以通过一个volumeClaimTemplates属性来直接使用 StorageClass,如下:(test-statefulset-nfs.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
27
28
29
30
31
32
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: nfs-web
spec:
serviceName: "nginx"
replicas: 3
template:
metadata:
labels:
app: nfs-web
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: nginx:1.7.9
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 类型的服务下面使用得非常多。

直接创建上面的对象:

1
2
3
4
5
6
7
8
9
$ kubectl create -f test-statefulset-nfs.yaml
statefulset.apps "nfs-web" created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
...
nfs-web-0 1/1 Running 0 1m
nfs-web-1 1/1 Running 0 1m
nfs-web-2 1/1 Running 0 33s
...

创建完成后可以看到上面的3个 Pod 已经运行成功,然后查看下 PVC 对象:

1
2
3
4
5
6
7
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
...
www-nfs-web-0 Bound pvc-cc36b3ce-8b50-11e8-b585-525400db4df7 1Gi RWO course-nfs-storage 2m
www-nfs-web-1 Bound pvc-d38285f9-8b50-11e8-b585-525400db4df7 1Gi RWO course-nfs-storage 2m
www-nfs-web-2 Bound pvc-e348250b-8b50-11e8-b585-525400db4df7 1Gi RWO course-nfs-storage 1m
...

我们可以看到是不是也生成了3个 PVC 对象,名称由模板名称 name 加上 Pod 的名称组合而成,这3个 PVC 对象也都是 绑定状态了,很显然我们查看 PV 也可以看到对应的3个 PV 对象:

1
2
3
4
5
6
7
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
... 1d
pvc-cc36b3ce-8b50-11e8-b585-525400db4df7 1Gi RWO Delete Bound default/www-nfs-web-0 course-nfs-storage 4m
pvc-d38285f9-8b50-11e8-b585-525400db4df7 1Gi RWO Delete Bound default/www-nfs-web-1 course-nfs-storage 4m
pvc-e348250b-8b50-11e8-b585-525400db4df7 1Gi RWO Delete Bound default/www-nfs-web-2 course-nfs-storage 4m
...

查看 nfs 服务器上面的共享数据目录:

1
2
3
4
5
6
$ ls /data/k8s/
...
default-www-nfs-web-0-pvc-cc36b3ce-8b50-11e8-b585-525400db4df7
default-www-nfs-web-1-pvc-d38285f9-8b50-11e8-b585-525400db4df7
default-www-nfs-web-2-pvc-e348250b-8b50-11e8-b585-525400db4df7
...

是不是也有对应的3个数据目录,这就是我们的 StorageClass 的使用方法,对于 StorageClass 多用于 StatefulSet 类型的服务,在后面的课程中我们还学不断的接触到。