1.无状态应用与有状态应用
应用的有状态和无状态是根据应用是否有持久化保存数据的需求而言的,即持久化保存数据的应用为有状态的应用,反之则为无状态的应用。常见的系统多是有状态的。比如微博微信,用户发布的内容是需要永久保存的。但是有的系统是由一系列微服务或模块组成的,而有的微服务或模块并没有数据持久化的要求。比如一个博客系统,总体而言是一个有状态应用,但可以将博客系统拆分成一个无状态应用的前端和一个有状态应用的后端。从数量上来说,无状态的应用要更多一些,因为对大多数的系统而言,读请求的数量往往要远远高于写请求的数量。
1.1.非持久化的容器
容器的一个特点是当容器退出后,其内部所有的数据和状态就会丢失。同样的镜像再次启动一个新的容器实例,该实例默认不会继承之前实例的状态。这对无状态应用来不是问题,相反是一个很好的特性,可以很好地保证无状态应用的一致性。但对于有状态的应用来说则是很大的障碍。
1.2.容器数据持久化
不可避免地用户会在容器中运行有状态的应用,因此,在容器引擎的层面必须满足数据持久化的需求。Docker在容器引擎的层面提供了卷(Volume)的概念。用户可以建立数据卷容器来为容器提供持久化的支持。容器实例需要将持久化的数据写入数据卷容器中保存。当应用容器退出时,数据仍然安然地存储于数据卷容器中。
此外,Docker以插件的形式支持多种存储方式。通过卷插件(Volume Plugin),目前Docker容器可以对接主机的目录、软件定义存储(如GlusterFS及Ceph)、云存储(如AWS及GCE等公有云提供的存储)及存储管理解决方案(如Flocker等)。
2.持久化卷与持久化卷请求
Docker在容器引擎层面提供了卷的机制来满足容器数据持久化的需求。在多主机的环境下,容器云的场景中需要考虑的细节会更多。比如一个有状态应用的容器实例从一个主机漂移到另一台主机上时,如何保证其所挂载的卷仍然可以被正确对接。此外,在云平台上,用户需要以一种简单方式获取和消费存储这一资源,而无需过度关系底层的实现细节。比如,用户有一个应用,需要一个100GB的高速存储空间存储大量的零碎文件,用户需要做的是向云平台提交资源申请,然后获取并消费这个存储资源,而不需要操心这个存储究竟是具体来自哪一台存储服务器的哪一块磁盘。
为了满足容器用户在云环境存储的需求,Kubernetes在容器编排的层面提供了持久化卷(Persistent Volume,PV)及持久化卷请求(Persistent Volume Claim,PVC)的概念。持久化卷定义了具体的存储的连接信息,如NFS服务器的地址和端口、卷的位置、卷的大小及访问方式。在OpenShift中,集群管理员会定义一系列的持久化卷,形成一个持久化的资源池。当用户需要部署有持久化需求的容器应用时,用户需要创建一个持久化卷请求。在这个请求中,用户申明所需存储的大小及访问方式。Kubernetes将负责根据用户的持久化卷请求找到匹配需求的持久化卷进行对接。最终的结果是容器启动后,持久化卷定义的后端存储将会被挂载到容器的指定目录。OpenShift在框架上基于Kubernetes,因此用户可以在OpenShift中使用Kubernetes的持久化卷与持久化卷请求的存储供给模式,以满足数据持久化的需求。
2.1持久化卷的生命周期
持久化卷的生命周期一共分为供给、绑定、使用、释放和回收。
2.1.1供给
在Kubernetes中,存储资源的供给方式分为两种:静态供给和动态供给。对于静态供给,集群管理员会创建一些列的持久化卷,形成一个持久化卷的资源池。动态供给是集群所在的基础设施云根据需求动态地创建持久化卷。如Openstack、Amazon WebService。
访问方式是描述持久化卷的访问特性。比如是只读还是可以读写。是只能被一个Node节点挂载,还是可以被多个Node节点使用。目前有三种访问方式可供选择。
ReadWriteOnce:可读可写,只能被一个Node节点挂载;
ReadWriteMany:可读可写,可以被对个Node节点挂载;
ReadOnlyMany:只读,能被对个Node节点挂载;
注:访问方式和后端使用的存储有很大关系,并不是将一个持久化卷设置为ReadWriteMany,这个持久化卷就可以被多个Node节点挂载。比如OpenStack的Cinder和Ceph RDB这些块设备就不支持ReadWriteMany这种模式。
2.1.2.绑定
用户在部署容器应用时会定义持久化卷请求持久化卷请求。用户在持久化卷请求中声明需要的存储资源的特性,如大小和访问方式。Kubernetes负责在持久化卷的资源池中寻找匹配的持久化卷对象,并将持久化卷请求与目标持久化卷进行对接。这时持久化卷和持久化卷请求的状态都将变成Bound,即绑定状态。
2.1.3.使用
在用户部署容器时会在Deployment Config的容器定义中指定Volume的挂载点,并将这个挂载点和持久化卷请求关联。当容器启动时,持久化卷指定的后端存储被挂载到容器定义的挂载点上。应用在容器内部运行,数据通过挂载点最终写入后端存储中,从而实现持久化。
注:挂载点与挂载目录
2.1.4.释放
当应用下线不再使用存储时,可以删除相关的持久化卷请求,这样持久化卷的状态就会变成released,即释放。
2.1.5.回收
当持久化卷的状态变为released后,Kubernetes将根据持久化卷定义的回收策略回收持久化卷。当前支持的回收策略有三种:
Retian:保留数据,人工回收持久化卷;
Recycle:通过执行rm -rf删除卷上的所有数据。目前只有NFS几Host Path支持这种方式;
Delete:动态地删除后端存储。该模式需要下层IaaS的支持,目前AWS EBS、GCE PD及OpenShift Cinder支持这种模式。
注:在Kubernetes1.3中,持久化卷和持久化卷请求引入了标签的概念,这给了用户极大的灵活性。用户可以在持久化卷请求中定义相应的标签选择器,从而获得更精确匹配应用需求的后端持久化卷。
3.持久化卷与存储
Kubernetes的持久化卷支持的后端存储的类型很多,包括宿主机的本地目录(Host Path)、网络文件系统(NFS)、OpenStack Cinder分布式存储(如GlusterFS、Ceph RBD及CephFS)及云存储(如AWS Elastic Block Store或GCE Persistent Disk)等。
4.存储资源定向匹配
不同用户对存储的需求不尽相同,除了大小和访问方式外,可能对磁盘的速度、储存所在的数据中心等有特殊的要求。为了灵活满足储存需求和储存资源的对接,Kubernetes支持为持久化卷打上不同的标签(Lable),在持久化卷请求侧则通过定义标签选择器来申明该持久化卷请求具体需要与什么样的持久化卷匹配。通过标签和标签选择器(Selector),Kubernetes为持久化卷与持久化卷请求实现了定向匹配。以下以nfs为例。
4.1.创建持久化卷
[root@master ~]# vi pv2.json{ "apiVersion": "v1", "kind": "PersistentVolume", "metadata":{ "name": "pv0002" }, "spec": { "capacity": { "storage": "5Gi" }, "accessModes" : ["ReadWriteOnce"], "nfs": { "path": "/exports/pv0002", "server": "10.16.3.35" }, "persistentVolumeReclaimPolicy": "Retain" } } [root@master ~]# oc create -f pv2.json persistentvolume "pv0002" created [root@master ~]# oc get pvNAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE pv0002 5Gi RWO Retain Available 1m
4.2.标记标签
[root@master ~]# oc label pv pv0002 disktype=ssdpersistentvolume "pv0002" labeled
再次查看持久化卷的标签,可以看到pv0002已经打上了disktype=ssd的标签了。
[root@master ~]# oc get pv --show-labelsNAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE LABELS pv0001 5Gi RWO Retain Bound default/docker-registry-claim 40m <none> pv0002 5Gi RWO Retain Available 2m disktype=ssd
4.3.创建持久化卷请求
创建一个带标签选择器的持久化请求。如下面的定义所示,这个持久化卷请求的储存空间大小为1Gi,访问方式是只读共享RWO。标签选择器的类型为matchLabels,定义值为"disktype": "ssd",即表示与该持久化卷请求匹配的持久化卷必须要带有"disktype": "ssd"标签。
{ "apiVersion": "v1", "kind": "PersistentVolumeClaim", "metadata":{ "name": "pvc0002", "creationTimestamp": null }, "spec": { "accessModes" : ["ReadWriteOnce"], "selector": { "matchLabels":{ "disktype": "ssd" } }, "resources": { "requests": { "storage": "1Gi" } } }, "status": {} } [root@master ~]# oc create -f pvc2.json persistentvolumeclaim "pvc0002" created
4.4.请求与资源定向匹配
持久化卷请求创建完后,查看持久化卷的状态,可以看到虽然pv0001和pv0002在空间大小和访问方式上都满足pvc0002的要求,但是pvc0002最终匹配上的是带有目标标签的pv0002。
[root@master ~]# oc get pv --show-labelsNAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE LABELS pv0001 5Gi RWO Retain Bound default/docker-registry-claim 49m <none> pv0002 5Gi RWO Retain Bound default/pvc0002 12m disktype=ssd
4.5.标签选择器
目前,持久化卷请求支持两种标签选择器:matchLabels及matchExpressions。matchLabels选择器可以精确匹配一个或多个标签。例如:
... "selector": { "matchLabels": { "disktype": "ssd" } }, ...
matchExpressions选择器支持标签的模糊匹配。用户可以使用操作符In或者NotIn对标签的值进行模糊匹配。
... matchExpressions: - {key: region, operator: In, values: [shenzhen]} - {key: env, operator: NotIn, values: [testing]} ...
5.持久化的镜像仓库
5.1.检查挂载点
以集群管理员的身份登录OpenShift
[root@master ~]# oc login -u system:admin
切换到default项目,查看Registry的容器状态
[root@master ~]# oc project defaultNow using project "default" on server "https://192.168.1.x:8443". [root@master ~]# oc get podNAME READY STATUS RESTARTS AGE docker-registry-1-2kxx2 0/1 Evicted 0 5d docker-registry-1-flpn8 1/1 Running 0 18h router-1-zvm87 1/1 Running 5 5d
通过oc volume命令可以查看系统对象关于volume的相关定义。执行oc volumes命令查看Registry组件的Deployment Config关于Volume的定义。可以看到Registry组件的定义中已经创建了一个Volume Mounts对象registry-storage,这个挂载点指向了/registry目录。当前这个Volume Mounts使用的empty directory的卷,即将数据保存在计算节点上。我们需要做的就是给registry-storage这个挂载点挂上一个持久化后端。
[root@master ~]# oc volumes dc/docker-registry --alldeploymentconfigs/docker-registry empty directory as registry-storage mounted at /registry
5.2.备份数据
查看当前容器下/registry内容。
[root@master ~]# oc rsh docker-registry-1-flpn8 'du' '-sh' '/registry'229M /registry
备份当前容器下/registry内容。
[root@master ~]# mkdir /root/backup[root@master ~]# oc rsync docker-registry-1-flpn8:/registry /root/backup...忽略输出... sent 671 bytes received 239679269 bytes 28197640.00 bytes/sec total size is 239645190 speedup is 1.00
5.3.创建存储
以NFS为例。
[root@master ~]# mkdir -p /exports/pv0001[root@master ~]# yum install -y nfs-utils rpcbind[root@master ~]# chown nfsnobody:nfsnobody /exports/ -R[root@master ~]# echo "/exports/pv0001 *(rw,sync,all_squash)" >> /etc/exports[root@master ~]# systemctl start rpcbind[root@master ~]# exportfs -r[root@master ~]# systemctl start nfs-server
为了测试方便,暂时先关闭SELinux。
[root@master ~]# setenforce 0
测试挂载该共享目录,并尝试创建一个文件。
[root@master ~]# mount 192.168.1.x:/exports/pv0001 /mnt/[root@master ~]# touch /mnt/test[root@master ~]# ls /mnt/[root@master ~]# rm -rf /mnt/test [root@master ~]# umount /mnt/
5.4.创建持久化卷
根据NFS信息,创建持久化卷。
{ "apiVersion": "v1", "kind": "PersistentVolume", "metadata":{ "name": "pv0001" }, "spec": { "capacity": { "storage": "5Gi" }, "accessModes" : ["ReadWriteOnce"], "nfs": { "path": "/exports/pv0001", "server": "192.168.1.x" }, "persistentVolumeReclaimPolicy": "Retain" } }
执行oc create创建持久化卷。
[root@master ~]# oc create -f pv.json persistentvolume "pv0001" created
通过oc get pv可以查看刚才创建成功的持久化卷,此时状态为Available,即可用。
[root@master ~]# oc get pvNAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE pv0001 5Gi RWO Retain Available 1m
5.5.创建持久化卷请求
创建持久化请求,声明应用的存储需求。创建pvc.json文件,输入如下内容。这里声明了需要3GB的后端存储,访问方式为ReadWriteOnce。
{ "apiVersion": "v1", "kind": "PersistentVolumeClaim", "metadata":{ "name": "docker-registry-claim" }, "spec": { "accessModes" : ["ReadWriteOnce"], "resources": { "requests": { "storage": "3Gi" } } } }
执行oc create命令,创建持久化卷请求。
[root@master ~]# oc create -f pvc.json persistentvolumeclaim "docker-registry-claim" created
查看持久化卷请求和持久化卷状态,会发现系统已将他们连接起来了。持久化卷请求和持久化卷的状态都已经变成Bound。
[root@master ~]# oc get pvcNAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS AGE docker-registry-claim Bound pv0001 5Gi RWO 1m [root@master ~]# oc get pvNAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE pv0001 5Gi RWO Retain Bound default/docker-registry-claim 14m
5.6.关联持久化卷请求
将备份的数据复制到前文创建的NFS目录中。
[root@master ~]# mv /root/backup/registry/* /exports/pv0001/[root@master ~]# chown nfsnobody:nfsnobody /exports/ -R
此时,可以删除Registry容器,Replication Controller将重新创建它。
[root@master ~]# oc delete pod docker-registry-1-flpn8pod "docker-registry-1-flpn8" deleted [root@master ~]# oc get podNAME READY STATUS RESTARTS AGE docker-registry-1-2kxx2 0/1 Evicted 0 5d docker-registry-1-8qjkn 1/1 Running 0 5s docker-registry-1-flpn8 0/1 Terminating 1 22h router-1-zvm87 1/1 Running 6 5d [root@master ~]# oc get podNAME READY STATUS RESTARTS AGE docker-registry-1-2kxx2 0/1 Evicted 0 5d docker-registry-1-8qjkn 1/1 Running 0 13s router-1-zvm87 1/1 Running 6 5d
容器启动后,再次检查容器/registry目录,会发现目录的数据消失。因为容器默认是不持久化数据的。
[root@master ~]# oc rsh docker-registry-1-8qjkn 'du' '-sh' '/registry'0 /registry
为Registry容器定义添加持久化请求docker-registry-claim,并与挂载点registry-storage相关联。
[root@master ~]# oc volume dc/docker-registry --add --name=registry-storage -t pvc --claim-name=docker-registry-claim --overwritedeploymentconfig "docker-registry" updated
Deploymengt Config的容器定义修改后,OpenShift会创建新的容器实例。检查容器/registry目录,会发现目录的数据恢复了。
[root@master ~]# oc get podNAME READY STATUS RESTARTS AGE docker-registry-1-2kxx2 0/1 Evicted 0 5d docker-registry-1-8qjkn 0/1 Terminating 0 6m docker-registry-2-7fsq7 1/1 Running 0 16s router-1-zvm87 1/1 Running 6 5d [root@master ~]# oc get podNAME READY STATUS RESTARTS AGE docker-registry-1-2kxx2 0/1 Evicted 0 5d docker-registry-2-7fsq7 1/1 Running 0 21s router-1-zvm87 1/1 Running 6 5d
至此我们成功地 将Registry组件挂接上了持久化存储。
本例的配置是基于NFS持久化卷的实现,使用GlusterFS或者Ceph持久化卷的过程类似,只是持久化卷的定义需要稍作修改。
此时,再次查看Volume Mounts对象registry-storage,发现使用的是pvc/docker-registry-cliam这个卷了。
[root@master ~]# oc volumes dc/docker-registry --alldeploymentconfigs/docker-registry pvc/docker-registry-claim (allocated 10GiB) as registry-storage mounted at /registry
进系统看看挂载情况,/registry挂载的正是192.168.1.x:/exports/pv0001。
[root@master ~]# oc rsh docker-registry-1-8qjkn 'df' '-h'Filesystem Size Used Avail Use% Mounted onoverlay 8.6G 4.4G 4.2G 52% /tmpfs 1.9G 0 1.9G 0% /devtmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup192.168.1.x:/exports/pv0001 99G 293M 94G 1% /registry/dev/vda3 8.6G 4.4G 4.2G 52% /etc/hostsshm 64M 0 64M 0% /dev/shmtmpfs 1.9G 16K 1.9G 1% /run/secrets/kubernetes.io/serviceaccounttmpfs 1.9G 0 1.9G 0% /proc/scsitmpfs 1.9G 0 1.9G 0% /sys/firmware
奇怪的是,pv创建的是10Gi,而此处显示的大小是99G。经过验证发现,Size显示的大小并非pv赋予的大小,而是192.168.1.x:/exports/pv0001所在路径挂载的盘的大小。
作者:四冶
链接:https://www.jianshu.com/p/117afe14f5cc