在虚拟化的一系列解决方案中,数据的持久化都是需要我们非常关心的问题,dokcer是这样,Kubernetes也是这样。不过在Kubernetes中,有一个数据卷的概念。
一、Volume简介
我们经常都会说:容器、Pod都是很短暂的!其含义就是容器和Pod的生命周期都是很短暂的,会被频繁地销毁和创建。容器销毁时,保存在容器内部文件系统中的数据都会被清除。
Volume的生命周期独立于容器,Pod中的容器可能被销毁和重启,但Volume会被保留。
Kubernetes Volume主要解决了以下两个问题:
1)数据持久性:通常情况下,容器运行起来后,写到其文件系统的文件是暂时性的。当容器崩溃后,kubelet会将这个容器不断的重启,当达到重启的次数后,容器仍然不可用,那么就会将这个容器kill掉,重新生成新的容器。此时,新运行的容器并没有原容器中的数据,因为容器是由镜像创建的;
2)数据共享:同一个Pod中运行的容器之间,经常会存在共享文件/共享文件夹的需求;
从根本上来说,一个数据卷仅仅是一个可以被Pod访问的目录或文件。这个目录是怎么来的,取决于该数据卷的类型。同一个Pod中的两个容器可以将一个数据卷挂载到不同的目录下。
Volume 提供了对各种 backend 的抽象,容器在使用 Volume 读写数据的时候不需要关心数据到底是存放在本地节点的文件系统中呢还是云硬盘上。对它来说,所有类型的 Volume 都只是一个目录。
二、Volume之emptyDir
1)emptyDir简介
emptyDir 是最基础的 Volume 类型。正如其名字所示,一个 emptyDir Volume 是 Host 上的一个空目录。
emptyDir Volume 对于容器来说是持久的,对于 Pod 则不是。当 Pod 从节点删除时,Volume 的内容也会被删除。但如果只是容器被销毁而 Pod 还在,则 Volume 不受影响。类似于docker数据持久化中的docker manager volume方式!
2)emptyDir使用示例
[root@master yaml]# vim emptyDir.yaml
apiVersion: v1
kind: Pod
metadata:
name: producer-consumer #定义Pod的名称
spec:
containers:
- image: busybox
name: producer #定义容器的名称
volumeMounts:
- mountPath: /producer_dir #指定容器内的路径
name: shared-volume #表示把shared-volume挂载到容器中
args: #当容器运行完成后,执行以下的写操作
- /bin/sh
- -c
- echo "hello k8s" > /producer_dir/hello; sleep 30000
- image: busybox
name: consumer #定义容器的名称
volumeMounts:
- mountPath: /consumer_dir
name: shared-volume #与上一个容器一样
args:
- /bin/sh
- -c
- cat /consumer_dir/hello; sleep 30000
volumes:
- name: shared-volume #定义数据卷的名称,必须与以上挂载的数据卷名称一致
emptyDir: {} #定义一个类型为emptyDir的数据卷,名称为shared-volume
[root@master yaml]# kubectl apply -f emptyDir.yam #生成所需的Pod资源
[root@master yaml]# kubectl exec -it producer-consumer -c producer /bin/sh
#进入第一个容器进行验证
/ # cat /producer_dir/hello
hello k8s
[root@master yaml]# kubectl exec -it producer-consumer -c consumer /bin/sh
#进行第二个容器进行验证
/ # cat /consumer_dir/hello
hello k8s
到此可以看出这个pod中的两个容器指定的目录内容都是一样的,具体是本地的那个目录还需进一步进行验证。
[root@master yaml]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
producer-consumer 2/2 Running 0 7m58s 10.244.2.2 node02
#可以看出这个pod是运行在node02上的
[root@node02 ~]# docker ps | grep busybox #由于容器较多,根据使用的镜像名称进行筛选
4fbd734e1763 busybox "/bin/sh -c 'cat /co…" 8 minutes ago Up 8 minutes k8s_consumer_producer-consumer_default_003a002d-caec-4202-a020-1ae8d6ff7eba_0
b441c2ff2217 busybox "/bin/sh -c 'echo \"h…" 8 minutes ago Up 8 minutes k8s_producer_producer-consumer_default_003a002d-caec-4202-a020-1ae8d6ff7eba_0
[root@node02 ~]# docker inspect 4fbd734e1763 #根据容器的ID查看容器的详细信息
#找到Mounts字段,如下:
"Mounts": [
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/003a002d-caec-4202-a020-1ae8d6ff7eba/volumes/kubernetes.io~empty-dir/shared-volume",
#此处指定的便是docker host本地的目录
"Destination": "/consumer_dir", #容器中的目录
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
[root@node02 ~]# docker inspect b441c2ff2217
"Mounts": [
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/003a002d-caec-4202-a020-1ae8d6ff7eba/volumes/kubernetes.io~empty-dir/shared-volume",
"Destination": "/producer_dir",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
#可以看出这两个容器的源目录是一样,挂载的是docker host本地的同一个目录
[root@node02 ~]# cd /var/lib/kubelet/pods/003a002d-caec-4202-a020-1ae8d6ff7eba/volumes/kubernetes.io~empty-dir/shared-volume
[root@node02 shared-volume]# cat hello
hello k8s
#验证内容
由于是Kubernetes集群的环境,删除一个容器比较麻烦,直接将pod删除,查看docker host本地的数据是否存在!
[root@master yaml]# kubectl delete -f emptyDir.yaml
#master节点将pod删除
[root@node02 ~]# cd /var/lib/kubelet/pods/003a002d-caec-4202-a020-1ae8d6ff7eba/volumes/kubernetes.io~empty-dir/shared-volume
-bash: cd: /var/lib/kubelet/pods/003a002d-caec-4202-a020-1ae8d6ff7eba/volumes/kubernetes.io~empty-dir/shared-volume: 没有那个文件或目录
#node02进行验证,发现目录已经消失
3)emptyDir总结
emptyDir 是Docker Host 上创建的临时目录,其优点是能够方便地为 Pod 中的容器提供共享存储,不需要额外的配置。但它不具备持久性,如果 Pod 不存在了,emptyDir 也就没有了。根据这个特性,emptyDir 特别适合 Pod 中的容器需要临时共享存储空间的场景!
简单来说就是,如果容器被删除,数据依然存在;如果Pod被删除,数据将不会存在!
三、Volume之HostPath
hostPath Volume 的作用是将 Docker Host 文件系统中已经存在的目录 mount 给 Pod 的容器。大部分应用都不会使用 hostPath Volume,因为这实际上增加了 Pod 与节点的耦合,限制了 Pod 的使用。不过那些需要访问 Kubernetes 或 Docker 内部数据(配置文件和二进制库)的应用则需要使用 hostPath。类似于docker数据持久化中的bind mount方式!
当然也可以进行创建,这里就偷个懒,使用Kubernetes集群自带的YAML文件进行介绍!
[root@master yaml]# kubectl edit --namespace=kube-system pod kube-apiserver-master
#查看apiserver组件的yaml文件
如图:
如果 Pod 被销毁了,hostPath 对应的目录也还会被保留,从这点看,hostPath 的持久性比 emptyDir 强。不过一旦 Host 崩溃,hostPath 也就没法访问了。
由于使用场景较少,以上两种方式这里就不详细介绍了!
四、Volume之Persistent Volume
Persistent Volume概述
普通Volume和使用它的Pod之间是一种静态绑定关系,在定义Pod的文件里,同时定义了它使用的Volume。Volume 是Pod的附属品,我们无法单独创建一个Volume,因为它不是一个独立的K8S资源对象。
而Persistent Volume 简称PV是一个K8S资源对象,所以我们可以单独创建一个PV。它不和Pod直接发生关系,而是通过Persistent Volume Claim,简称PVC来实现动态绑定。Pod定义里指定的是PVC,然后PVC会根据Pod的要求去自动绑定合适的PV给Pod使用。
既然有了PV这个概念,那么PVC(PersistentVolumeClaim)这个概念也不得不说一下,PVC代表用户使用存储的请求,应用申请PV持久化空间的一个申请、声明。K8s集群可能会有多个PV,你需要不停的为不同的应用创建多个PV。
如图:
1)PV是集群中的存储资源,通常由集群管理员创建和管理;
2)StorageClass用于对PV进行分类,如果配置正确,Storage也可以根据PVC的请求动态创建PV;
3)PVC是使用该资源的请求,通常由应用程序提出请求,并指定对应的StorageClass和需求的空间大小;
4)PVC可以作为数据卷的一种,被挂载到Pod中使用;
PV与PVC的管理过程如下:
1)在主机上划分出一个单独的目录用于PV使用,并且定义其可用大小;
2)创建PVC这个资源对象,便于申请PV的存储空间;
3)Pod中添加数据卷,数据卷关联到PVC;
4)Pod中包含容器,容器挂载数据卷;
下面通过一个案例来详细了解一下Persistent Volume!
案例实现过程:
1)底层采用NFS存储,然后再NFS的目录下划分1G的容量供PV调度;
2)创建PVC来申请PV的存储空间;
3)创建Pod,使用PVC申请的存储空间来实现数据的持久化;
1)搭建NFS存储
本次案例直接在master节点上创建NFS存储!
[root@master ~]# yum -y install nfs-utils rpcbind
[root@master ~]# vim /etc/exports
/nfsdata *(rw,sync,no_root_squash)
[root@master ~]# systemctl start nfs-server
[root@master ~]# systemctl start rpcbind
[root@master ~]# showmount -e
Export list for master:
/nfsdata *
2)创建PV资源对象
[root@master ~]# vim test-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: test-pv
spec:
capacity:
storage: 1Gi #指定该PV资源分配的容器为1G
accessModes: #指定访问模式
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle #指定回收策略(实验环境,实际环境很少会这样做)
storageClassName: nfs #指定存储类名字
nfs: #需要与存储类名字一致
path: /nfsdata/test-pv //指定NFS的目录
server: 192.168.1.4 //指定NFS的IP地址
上述yaml文件中,主要字段的解释:
1)accessModes(访问模式)
ReadWriteOnce:以读写的方式挂载到单个节点;
ReadWriteMany:以读写的方式挂载到多个节点 ;
ReadOnlyMany:以只读的方式挂载到多个节点;
2)persistentVolumeReclaimPolicy(PV的回收策略)
Recycle:自动清除PV中的数据,自动回收;
Retain:需要手动回收;
Delete:删除云存储资源(云存储专用);
[root@master ~]# kubectl apply -f test-pv.yaml
[root@master ~]# kubectl get pv #查看PV的状态
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
test-pv 1Gi RWO Recycle Available nfs 21s
#注意其PV的状态必须是 Available才可正常使用
3)创建PVC资源对象
[root@master ~]# vim test-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
accessModes: #定义访问模式,必须与PV定义的访问模式一致
- ReadWriteOnce
resources:
requests:
storage: 1Gi #直接请求i使用最大的容量
storageClassName: nfs #定义的名称需与PV定义的名称一致
[root@master ~]# kubectl apply -f test-pvc.yaml
[root@master ~]# kubectl get pvc #查看PVC的状态
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
test-pvc Bound test-pv 1Gi RWO nfs 102s
[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 14m
#注意PV与PVC的状态都是Bound,表示PV与PVC的关联成功
如果,K8s集群中,有很多类似的PV,PVC在向PV申请空间时,不仅会考虑名称以及访问控制模式,还会考虑你申请空间的大小,会分配给你最合适大小的PV!
常见的状态有:
1)Available——>闲置状态,没有被绑定到PVC;
2)Bound——>绑定到PVC;
3)Released——>PVC被删除,资源没有被利用;
4)Failed——>自动回收失败;
4)创建一个Pod
[root@master ~]# vim test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-pod
image: busybox
args:
- /bin/sh
- -c
- sleep 300000
volumeMounts:
- mountPath: /testdata #定义容器中的目录
name: volumedata #保证与卷的名称一致
volumes:
- name: volumedata #定义卷的名称
persistentVolumeClaim:
claimName: test-pvc #指定逻辑卷对应的PVC名称
[root@master ~]# kubectl apply -f test-pod.yaml
[root@master ~]# kubectl get pod #查看pod的状态
NAME READY STATUS RESTARTS AGE
test-pod 0/1 ContainerCreating 0 6m26s
#注意其状态为 ContainerCreating,表示容器正在创建,但是查看时间,这么长时间没有创建好,不太正常
当pod状态不正常时,一般我们可以采用以下三种方式进行排错:
1)使用“ kubectl describe pod pod名称”查看pod的详细信息;
2)使用“kubectl logs pod名称“查看pod的日志信息;
3)使用“cat /var/log/messages | grep kubelet”查看对应节点kubelet系统日志;
本次采用第一种方式排错!
[root@master ~]# kubectl describe pod test-pod
#最后的一条的信息如下:
mount.nfs: mounting 192.168.1.4:/nfsdata/test-pv failed, reason given by server: No such file or directory
#根据消息提示,指定本地需要挂载的目录不存在
[root@master ~]# mkdir -p /nfsdata/test-pv
#创建完成目录后,建议查看pod生成的容器运行的节点,将节点上的kubelet服务进行重启,重启完成后,再次查看Pod的状态
[root@master ~]# kubectl get pod //再次查看pod的状态
NAME READY STATUS RESTARTS AGE
test-pod 1/1 Running 0 32m
如果创建Pod的过程中,Pod不断的重启,主要是因为:
1)swap交换分区没有被关闭,导致集群运行不正常;
2)node节点内存不足,道州集群运行不正常;
以上两种情况都会导致Pod会不断的重启,可以使用“free -h”命令进行查看!
5)测试其数据持久化的效果
[root@master ~]# kubectl exec -it test-pod /bin/sh
/ # echo "test pv pvc" > /testdata/test.txt
#进入容器,创建文件进行测试
[root@master ~]# cat /nfsdata/test-pv/test.txt
test pv pvc
#确认这个文件本地是存在的
[root@master ~]# kubectl delete -f test-pod.yaml
#将pod进行删除
[root@master ~]# cat /nfsdata/test-pv/test.txt
test pv pvc
#再次查看发现pod的测试文件依然存在
[root@master ~]# kubectl delete -f test-pvc.yaml
#将PVC进行删除
[root@master ~]# cat /nfsdata/test-pv/tes
cat: /nfsdata/test-pv/tes: 没有那个文件或目录
#再次查看发现pod的测试文件不见了,因为将PVC删除了
总结:由于我们在创建pv这个资源对象时,采用的回收策略是清除PV中的数据,然后自动回收,而PV这个资源对象是由PVC来申请使用的,所以不管是容器也好,pod也好,它们的销毁并不会影响用于实现数据持久化的nfs本地目录下的数据,但是,一旦这个PVC被删除,那么本地的数据就会随着PVC的销毁而不复存在,也就是说,采用PV这种数据卷来实现数据的持久化,它这个数据持久化的生命周期是和PVC的生命周期是一致的。
五、mysql对数据持久化的应用
可能通过步骤四并不能真正的理解Persistent volume的作用,下面通过创建mysql容器的方式来验证Persistent volume的作用!
1)搭建NFS共享存储
本次案例直接在master节点上创建NFS存储!
[root@master ~]# yum -y install nfs-utils rpcbind
[root@master ~]# vim /etc/exports
/nfsdata *(rw,sync,no_root_squash)
[root@master ~]# systemctl start nfs-server
[root@master ~]# systemctl start rpcbind
[root@master ~]# showmount -e
Export list for master:
/nfsdata *
2)创建PV资源对象
[root@master ~]# vim mysql-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain #注意指定的回收策略为手动回收
storageClassName: nfs
nfs:
path: /nfsdata/mysql-pv
server: 192.168.1.4
[root@master ~]# kubectl apply -f mysql-pv.yaml
[root@master ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
test-pv 1Gi RWO Retain Available nfs 15s
[root@master ~]# mkdir -p /nfsdata/mysql-pv
3)创建PVC资源对象
[root@master ~]# vim mysql-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: nfs
[root@master ~]# kubectl apply -f mysql-pvc.yaml
[root@master ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysql-pvc Bound mysql-pv 1Gi RWO nfs 13s
[root@master ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
mysql-pv 1Gi RWO Retain Bound default/mysql-pvc nfs 8m14s
4)创建pod资源
[root@master ~]# vim mysql-pod.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mysql-pod
spec:
selector: #设置给予等值的标签选择器
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env: #设置环境变量,数据库root用户的密码
- name: MYSQL_ROOT_PASSWORD
value: 123.com
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql #这个目录是数据库存放数据的目录(指定的是容器中的目录)
volumes:
- name: mysql-storage
persistentVolumeClaim:
claimName: mysql-pvc
[root@master ~]# kubectl apply -f mysql-pod.yaml
[root@master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
mysql-pod-6cc889468b-gq4qz 1/1 Running 0 3s
5)测试数据持久化效果
[root@master ~]# kubectl exec -it mysql-pod-6cc889468b-gq4qz -- mysql -u root -p123.com
#直接登录运行mysql数据库的pod中的mysql
#插入数据进行测试
mysql> create database lzj;
mysql> use lzj;
mysql> create table my_id( id int(4) );
mysql> insert my_id values (9527);
mysql> select * from my_id;
+------+
| id |
+------+
| 9527 |
+------+
[root@master ~]# ls /nfsdata/mysql-pv/
auto.cnf ibdata1 ib_logfile0 ib_logfile1 lzj mysql performance_schema
#查看pod对应的NFS的目录,确实有了数据
[root@master ~]# kubectl get pod -o wide #查看pod的详细信息
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mysql-pod-6cc889468b-gq4qz 1/1 Running 0 23m 10.244.2.6 node02
#查看到pod是运行在node02节点上的
#模拟node02宕机,步骤省略,关机、挂起都可以!
[root@node01 ~]# systemctl restart kubelet
#重启node01的kubelet服务
#接下来耐心等待pod的转移,可能需要差不多5分钟
^C[root@master ~]# kubectl get pod -o wide #可以看到pod已经运行在node01上
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mysql-pod-6cc889468b-gdf7k 1/1 Running 0 66s 10.244.1.6 node01
mysql-pod-6cc889468b-gq4qz 1/1 Terminating 0 32m 10.244.2.6 node02
[root@master ~]# kubectl exec -it mysql-pod-6cc889468b-gdf7k -- mysql -u root -p123.com
#再次登录到pod中运行的mysql(注意:pod的名称)
mysql> select * from lzj.my_id; #再次数据是否存在
+------+
| id |
+------+
| 9527 |
+------+
1 row in set (0.01 sec)
#数据依旧存在
——————————————本次到此结束,感谢阅读—————————————