前言

在一切虚拟化解决方案中,数据的持久化都是需要我们非常关心的问题,docker如此,K8s也不例外。在k8s中,有一个数据卷的概念。

k8s数据卷主要解决了以下两方面问题:

  • 数据持久性:通常情况下,容器运行起来后,写入到其文件系统的文件时暂时性的。当容器崩溃后,kebelet将这个容器kill掉,然后生成一个新的容器,此时,新运行的容器将没有原来容器内的文件,因为容器是重新从镜像创建的。
  • 数据共享:同一个pod中运行的容器之间,经常会存在共享文件/文件夹的需求。

在k8s中,Volume(数据卷)存在明确的生命周期(与包含该数据卷的容器组(pod)相同)。因此Volume的生命周期比同一容器组(pod)中任意容器的生命周期要更长,不管容器重启了多少次,数据都被保留下来。当然,如果pod不存在了,数据卷自然退出了。此时,根据pod所使用的数据卷类型不同,数据可能随着数据卷的退出而删除,也可能被真正持久化,并在下次容器组重启时仍然可以使用。

从根本上来说,一个数据卷仅仅是一个可以被pod访问的目录或文件。这个目录是怎么来的,取决于该数据卷的类型(不同类型的数据卷使用不同的存储介质)。同一个pod中的两个容器可以将一个数据卷挂载到不同的目录下。

一、数据卷类型

k8s目前支持28种数据卷类型(其中大多数特定于云环境),这里将写下在k8s中常用的几种数据卷类型。

1、emptyDir

emptyDir类型的数据卷在创建pod时分配给该pod,并且直到pod被移除,该数据卷才被释放。该数据卷初始分配时,始终是一个空目录。同一个pod中的不同容器都可以对该目录执行读写操作,并且共享其中的数据(尽管不同容器可能将该数据卷挂载到容器中的不同路径)。当pod被删除后,emptyDir数据卷中的数据将被永久删除。(PS:容器奔溃时,kubelet并不会删除pod,而仅仅是将容器重启,因此emptyDir中的数据在容器崩溃并重启后,仍然是存在的)。

emptyDir的使用场景如下:

  • 空白的初始空间,例如合并/排序算法中,临时将数据保存在磁盘上。
  • 长时间计算中存储检查点(中间结果),以便容器崩溃时,可以从上一次存储的检查点(中间结果)继续进行,而不是从头开始。
  • 作为两个容器的共享存储,使得第一个内容管理的容器可以将生成的数据存入其中,同时由一个webserver容器对外提供这些页面。
  • 默认情况下,emptyDir数据卷存储在node节点的存储介质(机械硬盘、SSD或网络存储)上。

emptyDir的使用示例

[root@master ~]# vim emtydir.yaml     #pod的yaml文件如下

apiVersion: v1
kind: Pod
metadata:
  name: read-write
spec:
  containers:
  - name: write         #定义一个名为write的容器
    image: busybox
    volumeMounts:
    - mountPath: /write    #当数据持久化类型为emtydir时,这里的路径指的是容器内的路径
      name: share-volume     #指定本地的目录名
    args:        #容器运行后,进行写操作
    - /bin/sh
    - -c
    - echo "emtydir test" > /write/hello;sleep 30000

  - name: read           #定义一个名为read的容器
    image: busybox
    volumeMounts:
    - mountPath: /read
      name: share-volume      #指定本地的目录名
    args:                #容器运行后,进行读操作
    - /bin/sh
    - -c
    - cat /read/hello; sleep 30000
  volumes:               #这里的volumes是指对上面挂载的进行解释
  - name: share-volume    #这里的名字必须和上面pod的mountPath下的name值对应
    emptyDir: {}            #这里表示是个空目录,主要是定义了一个数据持久化的类型
[root@master ~]# kubectl apply -f emtydir.yaml    #执行yaml文件
[root@master ~]# kubectl exec -it read-write -c write /bin/sh  #进入第一个pod
/ # cat /write/hello        #确认yaml文件执行的命令是否生效
emtydir test

[root@master ~]# kubectl exec -it read-write -c read /bin/sh  #进入第二个容器名为read的容器查看
/ # cat /read/hello        #查看指定挂载的目录下是否和write容器中的内容一致
emtydir test
#至此,起码可以确认这两个pod是挂载了同一个本地目录,文件内容都一致。
#那么,现在看看具体挂载的是本地哪个目录?
[root@master ~]# kubectl get pod -o wide    #先通过此命令查看pod是运行在哪个节点上的
#我这里是运行在node01节点的,所以接下来需要到node01节点上进行查看

#node01节点操作如下:
[root@node01 ~]# docker ps             #通过此命令查看出运行的容器ID号
CONTAINER ID        IMAGE              #省略部分内容
6186a08c6d5f        busybox              
5f19986f0879        busybox             
[root@node01 ~]# docker inspect 6186a08c6d5f      #查看第一个容器的详细信息
 "Mounts": [          #找到mount字段
            {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/86b67ff4-9ca0-4f40-86d8-6778cfe949ec/volumes/kubernetes.io~empty-dir/share-volume",
#上面的source就是指定的本地目录
                "Destination": "/read",
                "Mode": "Z",
                "RW": true,
                "Propagation": "rprivate"

[root@node01 ~]# docker inspect 5f19986f0879    #查看第二个容器的详细信息

        "Mounts": [    #同样定位到mount字段
            {
                "Type": "bind",
                "Source": "/var/lib/kubelet/pods/86b67ff4-9ca0-4f40-86d8-6778cfe949ec/volumes/kubernetes.io~empty-dir/share-volume",
#可以看到,上面指定的本地目录和第一个容器指定的是同一个目录
                "Destination": "/write",
                "Mode": "Z",
                "RW": true,
                "Propagation": "rprivate"
            },
#至此,已经可以确定这两个容器的挂载目录共享的是同一个本地目录
[root@node01 ~]# cat /var/lib/kubelet/pods/86b67ff4-9ca0-4f40-86d8-6778cfe949ec/volumes/kubernetes.io~empty-dir/share-volume/hello 
#查看本地该目录下的内容,和pod中的一致
emtydir test

至此,emptyDir的特性就已经验证了,只要这个pod中还有一个容器在运行,那么这个本地的数据就不会丢失,但如果这个pod被删除,那么本地的数据也将不复存在。

验证如下:

node01上删除一个pod并再次查看本地目录:

[root@node01 ~]# docker rm -f 6186a08c6d5f    #删除一个pod
6186a08c6d5f
[root@node01 ~]# cat /var/lib/kubelet/pods/86b67ff4-9ca0-4f40-86d8-6778cfe949ec/volumes/kubernetes.io~empty-dir/share-volume/hello 
#查看本地目录,发现文件还在
emtydir test

在master上将此pod删除,再次去node01节点上查看本地目录是否存在:

#master上删除pod
[root@master ~]# kubectl delete -f emtydir.yaml 
#在node01上再次查看本地目录,会提示不存在这个目录
[root@node01 ~]# cat /var/lib/kubelet/pods/86b67ff4-9ca0-4f40-86d8-6778cfe949ec/volumes/kubernetes.io~empty-dir/share-volume/hello 
cat: /var/lib/kubelet/pods/86b67ff4-9ca0-4f40-86d8-6778cfe949ec/volumes/kubernetes.io~empty-dir/share-volume/hello: 没有那个文件或目录

emptyDir总结:

同个pod里面的不同容器,共享同一个持久化目录,当pod节点删除时,volume的内容也会被删除。但如果仅仅是容器被销毁,pod还在,则volume不会受到任何影响。说白了,emptyDir的数据持久化的生命周期和使用的pod一致。一般是作为临时存储使用。

2、HostPath数据卷类型

hostPath 类型的数据卷将 Pod(容器组)所在节点的文件系统上某一个文件或目录挂载进容器组(容器内部),类似于docker中的bind mount挂载方式。

这种数据持久化的方式,使用场景不多,因为它增加了pod与节点之间的耦合。

绝大多数容器组并不需要使用 hostPath 数据卷,但是少数情况下,hostPath 数据卷非常有用:

适用场景如下:

  • 某容器需要访问 Docker,可使用 hostPath 挂载宿主节点的 /var/lib/docker
  • 在容器中运行 cAdvisor,使用 hostPath 挂载宿主节点的 /sys

总言而之,一般对K8s集群本身的数据持久化和docker本身的数据持久化会使用这种方式。

由于其使用场景比较少,这里就不举例了。

3、Persistent 数据卷类型

PersistentVolume(PV存储卷)是集群中的一块存储空间,由集群管理员管理或者由Storage class(存储类)自动管理,PV和pod、deployment、Service一样,都是一个资源对象。

既然有了PV这个概念,那么PVC(PersistentVolumeClaim)这个概念也不得不说一下,PVC代表用户使用存储的请求,应用申请PV持久化空间的一个申请、声明。K8s集群可能会有多个PV,你需要不停的为不同的应用创建多个PV。

比如说,pod是消耗node节点的计算资源,而PVC存储卷声明是消耗PV的存储资源。Pod可以请求的是特定数量的计算资源(CPU或内存等),而PVC请求的是特定大小或特定访问模式(只能被单节点读写/可被多节点只读/可被多节点读写)的存储资源。

PV和PVC的关系

PV(存储卷)和PVC(存储卷声明)的关系如下图所示:

K8s实现数据持久化_第1张图片

上图中的解释如下:

  • PV是集群中的存储资源,通常由集群管理员创建和管理;
  • StorageClass用于对PV进行分类,如果配置正确,Storage也可以根据PVC的请求动态创建PV;
  • PVC是使用该资源的请求,通常由应用程序提出请求,并指定对应的StorageClass和需求的空间大小;
  • PVC可以作为数据卷的一种,被挂载到pod中使用。

存储卷声明(PVC)的管理过程

PV和PVC的管理过程描述如下:
1、在主机上划分出一个单独的目录用于PV使用,并且定义其可用大小
2、创建PVC这个资源对象,以便请求PV的存储空间
3、pod中添加数据卷,数据卷关联到PVC;
4、Pod中包含容器,容器挂载数据卷

其实上面解释那么多,可能还是云里雾里的,下面是一个使用案例,仅供参考。

案例大概过程如下:
底层存储采用nfs存储,然后在nfs的目录下划分1G的容量供PV调度。然后通过创建PVC来申请PV的存储资源空间,最后创建pod测试,使用PVC声明的存储资源来实现数据的持久化。

1)搭建nfs存储

为了方便操作,我直接在master上搭建nfs存储。

[root@master ~]# yum -y install nfs-utils
[root@master ~]# systemctl enable rpcbind
[root@master ~]# vim /etc/exports
/nfsdata *(rw,sync,no_root_squash)
[root@master ~]# systemctl start nfs-server
[root@master ~]# systemctl enable nfs-server
[root@master ~]# showmount -e
Export list for master:
/nfsdata *

2)创建PV资源对象

[root@master ~]# vim test-pv.yaml    #编辑PV的yaml文件

apiVersion: v1
kind: PersistentVolume
metadata:
  name: test-pv
spec:
  capacity:
    storage: 1Gi          #该PV可分配的容量为1G
  accessModes:
    - ReadWriteOnce              #访问模式为只能以读写的方式挂载到单个节点
  persistentVolumeReclaimPolicy: Recycle   #回收策略为Recycle
  storageClassName: nfs            #定义存储类名字
  nfs:                                #这里和上面定义的存储类名字需要一致
    path: /nfsdata/test-pv       #指定nfs的目录
    server: 192.168.20.6           #nfs服务器的IP
#关于上述的具体解释
#capacity:指定PV的大小
#AccessModes:指定访问模式
    #ReadWriteOnce:只能以读写的方式挂载到单个节点(单个节点意味着只能被单个PVC声明使用)
    #ReadOnlyMany:能以只读的方式挂载到多个节点
    #ReadWriteMany:能以读写的方式挂载到多个节点
#persistentVolumeReclaimPolicy:PV的回收策略
    #Recycle:清除PV中的数据,然后自动回收。
    #Retain:需要手动回收。
    #Delete:删除云存储资源。(云存储专用)
    #PS:注意这里的回收策略是指,在PV被删除后,在这个PV下所存储的源文件是否删除。
#storageClassName:PV和PVC关联的依据。
[root@master ~]# kubectl apply -f test-pv.yaml     #执行yaml文件
[root@master ~]# kubectl get pv test-pv    #既然PV是一个资源对象,那么自然可以通过此方式查看其状态
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
test-pv   1Gi        RWO            Recycle          Available           nfs                     38s
#查看PV的状态必须为Available才可以正常使用

3)创建PVC资源对象

[root@master ~]# vim test-pvc.yaml         #编写yaml文件

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:          #定义访问模式,必须和PV定义的访问模式一致
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi          #直接请求使用最大的容量
  storageClassName: nfs      #这里的名字必须和PV定义的名字一致
[root@master ~]# kubectl apply -f test-pvc.yaml     #执行yaml文件

#再次查看PV及PVC的状态(状态为bound,表示该PV正在被使用)
[root@master ~]# kubectl get pvc      #查看PVC的状态
NAME       STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-pvc   Bound    test-pv   1Gi        RWO            nfs            2m10s
[root@master ~]# kubectl get pv      #查看PV的状态
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
test-pv   1Gi        RWO            Recycle          Bound    default/test-pvc   nfs                     8m24s

4)创建一个Pod

这里创建的pod使用刚刚创建的PV来实现数据的持久化。

[root@master ~]# vim test-pod.yaml       #编写pod的yaml文件
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: test-pod
    image: busybox
    args:
    - /bin/sh
    - -c
    - sleep 30000
    volumeMounts:
    - mountPath: /testdata
      name: volumedata     #这里自定义个名称
  volumes:
    - name: volumedata      #这里的是上面定义的名称解释,这两个名称必须一致
      persistentVolumeClaim:
        claimName: test-pvc
[root@master ~]# kubectl apply -f test-pod.yaml        #执行yaml文件
[root@master ~]# kubectl get pod     #查看pod的状态,发现其一直处于ContainerCreating状态
#怎么回事呢?
NAME       READY   STATUS              RESTARTS   AGE
test-pod   0/1     ContainerCreating   0          23s
#当遇到pod状态不正常时,一般我们可以采用三种方式来排错
#第一就是使用kubectl  describe命令来查看pod的详细信息
#第二就是使用kubectl logs命令来查看pod的日志
#第三就是查看宿主机本机的message日志
#这里我采用第一种方法排错
[root@master ~]# kubectl describe pod test-pod
#输出的最后一条信息如下:
mount.nfs: mounting 192.168.20.6:/nfsdata/test-pv failed, reason given by server: No such file or directory
#原来是我们在挂载nfs存储目录时,指定的目录并不存在
#那就在nfs服务器上(这里是本机)进行创建相关目录咯
[root@master ~]# mkdir -p /nfsdata/test-pv      #创建对应目录
[root@master ~]# kubectl get pod test-pod   #然后再次查看pod的状态
#如果pod的状态还是正在创建,那么就是因为运行该pod的节点上的kubelet组件还没有反应过来
#如果要追求pod的启动速度,可以手动将pod所在节点的kubelet组件进行重启即可。
[root@master ~]# kubectl get pod test-pod    #稍等片刻,再次查看,发现其pod已经running了
NAME       READY   STATUS    RESTARTS   AGE
test-pod   1/1     Running   0          8m

5)测试其数据持久化的效果

[root@master ~]# kubectl exec -it test-pod /bin/sh   #进入pod
/ # echo "test pv pvc" > /testdata/test.txt       #向数据持久化的目录写入测试信息
#回到nfs服务器,查看共享的目录下是否有容器中写入的信息
[root@master ~]# cat /nfsdata/test-pv/test.txt   #确定是有的
test pv pvc
#现在查看到pod容器的所在节点,然后到对应节点将其删除
[root@master ~]# kubectl get pod -o wide       #我这里是运行在node02节点
NAME       READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
test-pod   1/1     Running   0          11m   10.244.2.2   node02              
#在node02节点查看到其pod容器的ID号,然后将其删除
[root@node02 ~]# docker ps      #获取容器的ID号
[root@node02 ~]# docker rm -f dd445dce9530   #删除刚刚创建的容器
#回到nfs服务器,发现其本地目录下的数据还是在的
[root@master ~]# cat /nfsdata/test-pv/test.txt 
test pv pvc
#那么现在测试,将这个pod删除,nfs本地的数据是否还在?
[root@master ~]# kubectl delete -f test-pod.yaml 
[root@master ~]# cat /nfsdata/test-pv/test.txt      #哦吼,数据还在
test pv pvc
#那现在要是将PVC删除呢?
[root@master ~]# kubectl delete -f test-pvc.yaml 
[root@master ~]# cat /nfsdata/test-pv/test.txt       #哦吼,数据不在了。
cat: /nfsdata/test-pv/test.txt: 没有那个文件或目录

总结:由于我们在创建pv这个资源对象时,采用的回收策略是清除PV中的数据,然后自动回收,而PV这个资源对象是由PVC来申请使用的,所以不管是容器也好,pod也好,它们的销毁并不会影响用于实现数据持久化的nfs本地目录下的数据,但是,一旦这个PVC被删除,那么本地的数据就会随着PVC的销毁而不复存在,也就是说,采用PV这种数据卷来实现数据的持久化,它这个数据持久化的生命周期是和PVC的生命周期是一致的。

———————— 本文至此结束,感谢阅读 ————————