第9章 数据管理
本章将讨论Kubernetes如何管理存储资源。
首先我们会学习Volume,以及Kubernetes如何通过Volume为集群中的容器提供存储;然后我们会实践几种常用的Volume类型并理解它们各自的应用场景;最后,我们会讨论Kubernetes如何通过Persistent Volume和Persistent Volume Claim分离集群管理员与集群用户的职责,并实践Volume的静态供给和动态供给。
9.1 Volume
本节我们讨论Kubernetes的存储模型Volume,学习如何将各种持久化存储映射到容器。
我们经常会说:容器和Pod是短暂的。其含义是它们的生命周期可能很短,会被频繁地销毁和创建。容器销毁时,保存在容器内部文件系统中的数据都会被清除。
为了持久化保存容器的数据,可以使用Kubernetes Volume。
Volume的生命周期独立于容器,Pod中的容器可能被销毁和重建,但Volume会被保留。
本质上,Kubernetes Volume是一个目录,这一点与Docker Volume类似。当Volume被mount到Pod,Pod中的所有容器都可以访问这个Volume。Kubernetes Volume也支持多种backend类型,包括emptyDir、hostPath、GCE Persistent Disk、AWS Elastic Block Store、NFS、Ceph等,完整列表可参考https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes。
Volume提供了对各种backend的抽象,容器在使用Volume读写数据的时候不需要关心数据到底是存放在本地节点的文件系统中还是云硬盘上。对它来说,所有类型的Volume都只是一个目录。
我们将从最简单的emptyDir开始学习Kubernetes Volume。
9.1.1 emptyDir
emptyDir是最基础的Volume类型。正如其名字所示,一个emptyDir Volume是Host上的一个空目录。
emptyDir Volume对于容器来说是持久的,对于Pod则不是。当Pod从节点删除时,Volume的内容也会被删除。但如果只是容器被销毁而Pod还在,则Volume不受影响。
也就是说:emptyDir Volume的生命周期与Pod一致。
Pod中的所有容器都可以共享Volume,它们可以指定各自的mount路径。下面通过例子来实践emptyDir,配置文件如图9-1所示。

图9-1
这里我们模拟了一个producer-consumer场景。Pod有两个容器producer和consumer,它们共享一个Volume。producer负责往Volume中写数据,consumer则是从Volume读取数据。
① 文件最底部volumes定义了一个emptyDir类型的Volume shared-volume。
② producer容器将shared-volume mount到/producer_dir目录。
③ producer通过echo将数据写到文件hello里。
④ consumer容器将shared-volume mount到/consumer_dir目录。
⑤ consumer通过cat从文件hello读数据。
执行命令创建Pod,如图9-2所示。

图9-2
kubectl logs显示容器consumer成功读到了producer写入的数据,验证了两个容器共享emptyDir Volume。
因为emptyDir是Docker Host文件系统里的目录,其效果相当于执行了docker run -v/producer_dir和docker run -v /consumer_dir。通过docker inspect查看容器的详细配置信息,我们发现两个容器都mount了同一个目录,如图9-3、图9-4所示。

图9-3

图9-4
这里/var/lib/kubelet/pods/3e6100eb-a97a-11e7-8f72-0800274451ad/volumes/kubernetes. io~empty-dir/shared-volume就是emptyDir在Host上的真正路径。
emptyDir是Host上创建的临时目录,其优点是能够方便地为Pod中的容器提供共享存储,不需要额外的配置。它不具备持久性,如果Pod不存在了,emptyDir也就没有了。根据这个特性,emptyDir特别适合Pod中的容器需要临时共享存储空间的场景,比如前面的生产者消费者用例。
9.1.2 hostPath
hostPath Volume的作用是将Docker Host文件系统中已经存在的目录mount给Pod的容器。大部分应用都不会使用hostPath Volume,因为这实际上增加了Pod与节点的耦合,限制了Pod的使用。不过那些需要访问Kubernetes或Docker内部数据(配置文件和二进制库)的应用则需要使用hostPath。
比如kube-apiserver和kube-controller-manager就是这样的应用,通过kubectl edit--namespace=kube-system pod kube-apiserver-k8s-master查看kube-apiserver Pod的配置,Volume的相关部分如图9-5所示。

图9-5
这里定义了三个hostPath:volume k8s、certs和pki,分别对应Host目录/etc/kubernetes、/etc/ssl/certs和/etc/pki。
如果Pod被销毁了,hostPath对应的目录还是会被保留,从这一点来看,hostPath的持久性比emptyDir强。不过一旦Host崩溃,hostPath也就无法访问了。
接下来我们将学习具备真正持久性的Volume。
9.1.3 外部Storage Provider
如果Kubernetes部署在诸如AWS、GCE、Azure等公有云上,可以直接使用云硬盘作为Volume。下面给出一个AWS Elastic Block Store的例子,如图9-6所示。

图9-6
要在Pod中使用ESB volume,必须先在AWS中创建,然后通过volume-id引用。其他云硬盘的使用方法可参考各公有云厂商的官方文档。
Kubernetes Volume也可以使用主流的分布式存储,比如Ceph、GlusterFS等。下面给出一个Ceph的例子,如图9-7所示。

图9-7
Ceph文件系统的/some/path/in/side/cephfs目录被mount到容器路径/test-ceph。
相对于emptyDir和hostPath,这些Volume类型的最大特点就是不依赖Kubernetes。Volume的底层基础设施由独立的存储系统管理,与Kubernetes集群是分离的。数据被持久化后,即使整个Kubernetes崩溃也不会受损。
当然,运维这样的存储系统通常不是一项简单的工作,特别是对可靠性、可用性和扩展性有较高要求的时候。
9.2 PersistentVolume & PersistentVolumeClaim
Volume提供了非常好的数据持久化方案,不过在可管理性上还有不足。
拿前面的AWS EBS例子来说,要使用Volume,Pod必须事先知道如下信息:
(1)当前Volume来自AWS EBS。
(2)EBS Volume已经提前创建,并且知道确切的volume-id。
Pod通常是由应用的开发人员维护,而Volume则通常是由存储系统的管理员维护。开发人员要获得上面的信息,要么询问管理员,要么自己就是管理员。
这样就带来一个管理上的问题:应用开发人员和系统管理员的职责耦合在一起了。如果系统规模较小或者对于开发环境,这样的情况还可以接受,当集群规模变大,特别是对于生成环境,考虑到效率和安全性,这就成了必须要解决的问题。
Kubernetes给出的解决方案是PersistentVolume和PersistentVolumeClaim。
PersistentVolume(PV)是外部存储系统中的一块存储空间,由管理员创建和维护。与Volume一样,PV具有持久性,生命周期独立于Pod。
PersistentVolumeClaim(PVC)是对PV的申请(Claim)。PVC通常由普通用户创建和维护。需要为Pod分配存储资源时,用户可以创建一个PVC,指明存储资源的容量大小和访问模式(比如只读)等信息,Kubernetes会查找并提供满足条件的PV。
有了PersistentVolumeClaim,用户只需要告诉Kubernetes需要什么样的存储资源,而不必关心真正的空间从哪里分配、如何访问等底层细节信息。这些Storage Provider的底层信息交给管理员来处理,只有管理员才应该关心创建PersistentVolume的细节信息。
Kubernetes支持多种类型的PersistentVolume,比如AWS EBS、Ceph、NFS等,完整列表请参考https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes。
下面我们用NFS来体会PersistentVolume的使用方法。
9.2.1 NFS PersistentVolume
作为准备工作,我们已经在k8s-master节点上搭建了一个NFS服务器,目录为/nfsdata,如图9-8所示。

图9-8
下面创建一个PV mypv1,配置文件nfs-pv1.yml如图9-9所示。

图9-9
① capacity指定PV的容量为1GB。
② accessModes指定访问模式为ReadWriteOnce,支持的访问模式有3种:ReadWriteOnce表示PV能以read-write模式mount到单个节点,ReadOnlyMany表示PV能以read-only模式mount到多个节点,ReadWriteMany表示PV能以read-write模式mount到多个节点。
③ persistentVolumeReclaimPolicy指定当PV的回收策略为Recycle,支持的策略有3种:Retain表示需要管理员手工回收;Recycle表示清除PV中的数据,效果相当于执行rm -rf/thevolume/*;Delete表示删除Storage Provider上的对应存储资源,例如AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume等。
④ storageClassName指定PV的class为nfs。相当于为PV设置了一个分类,PVC可以指定class申请相应class的PV。
⑤ 指定PV在NFS服务器上对应的目录。
创建mypv1,如图9-10所示。

图9-10
STATUS为Available,表示mypv1就绪,可以被PVC申请。
接下来创建PVC mypvc1,配置文件nfs-pvc1.yml如图9-11所示。

图9-11
PVC就很简单了,只需要指定PV的容量、访问模式和class即可。
创建mypvc1,如图9-12所示。

图9-12
从kubectl get pvc和kubectl get pv的输出可以看到mypvc1已经Bound到mypv1,申请成功。
接下来就可以在Pod中使用存储了,Pod配置文件pod1.yml如图9-13所示。

图9-13
与使用普通Volume的格式类似,在volumes中通过persistentVolumeClaim指定使用mypvc1申请的Volume。
创建mypod1,如图9-14所示。

图9-14
验证PV是否可用,如图9-15所示。

图9-15
可见,在Pod中创建的文件/mydata/hello确实已经保存到了NFS服务器目录/nfsdata/pv1中。
9.2.2 回收PV
当不需要使用PV时,可用删除PVC回收PV,如图9-16所示。

图9-16
当PVC mypvc1被删除后,我们发现Kubernetes启动了一个新Pod recycler-for-mypv1,这个Pod的作用就是清除PV mypv1的数据。此时mypv1的状态为Released,表示已经解除了与mypvc1的Bound,正在清除数据,不过此时还不可用。
当数据清除完毕,mypv1的状态重新变为Available,此时可以被新的PVC申请,如图9-17所示。

图9-17
/nfsdata/pv1中的hello文件已经被删除了。
因为PV的回收策略设置为Recycle,所以数据会被清除,但这可能不是我们想要的结果。如果我们希望保留数据,可以将策略设置为Retain,如图9-18所示。

图9-18
通过kubectl apply更新PV,如图9-19所示。

图9-19
回收策略已经变为Retain,通过下面的步骤验证其效果,如图9-20所示。

图9-20
① 重新创建mypvc1。
② 在mypv1中创建文件hello。
③ mypv1状态变为Released。
④ Kubernetes并没有启动Pod recycler-for-mypv1。
⑤ PV中的数据被完整保留。
虽然mypv1中的数据得到了保留,但其PV状态会一直处于Released,不能被其他PVC申请。为了重新使用存储资源,可以删除并重新创建mypv1。删除操作只是删除了PV对象,存储空间中的数据并不会被删除。
新建的mypv1状态为Available,如图9-21所示,已经可以被PVC申请。

图9-21
PV还支持Delete的回收策略,会删除PV在Storage Provider上对应的存储空间。NFS的PV不支持Delete,支持Delete的Provider有AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume等。
9.2.3 PV动态供给
在前面的例子中,我们提前创建了PV,然后通过PVC申请PV并在Pod中使用,这种方式叫作静态供给(Static Provision)。
与之对应的是动态供给(Dynamical Provision),即如果没有满足PVC条件的PV,会动态创建PV。相比静态供给,动态供给有明显的优势:不需要提前创建PV,减少了管理员的工作量,效率高。
动态供给是通过StorageClass实现的,StorageClass定义了如何创建PV,下面给出两个例子。
(1)StorageClass standard,如图9-22所示。

图9-22
(2)StorageClass slow,如图9-23所示。

图9-23
这两个StorageClass都会动态创建AWS EBS,不同点在于standard创建的是gp2类型的EBS,而slow创建的是io1类型的EBS。不同类型的EBS支持的参数可参考AWS官方文档。
StorageClass支持Delete和Retain两种reclaimPolicy,默认是Delete。
与之前一样,PVC在申请PV时,只需要指定StorageClass、容量以及访问模式即可,如图9-24所示。

图9-24
除了AWS EBS,Kubernetes还支持其他多种动态供给PV的Provisioner,完整列表请参考https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner。
9.3 一个数据库例子
本节演示如何为MySQL数据库提供持久化存储,步骤为:
(1)创建PV和PVC。
(2)部署MySQL。
(3)向MySQL添加数据。
(4)模拟节点宕机故障,Kubernetes将MySQL自动迁移到其他节点。
(5)验证数据一致性。
首先创建PV和PVC,配置说明如下。

图9-25

图9-26
创建mysql-pv和mysql-pvc,如图9-27所示。

图9-27
接下来部署MySQL,配置文件如图9-28所示。

图9-28
PVC mysql-pvc Bound的PV mysql-pv将被mount到MySQL的数据目录var/lib/mysql,如图9-29所示。

图9-29
MySQL被部署到k8s-node2,下面通过客户端访问Service mysql,如图9-30所示。
kubectl run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql-h mysql -ppassword

图9-30
更新数据库,如图9-31所示。

图9-31
① 切换到数据库mysql。
② 创建数据库表my_id。
③ 插入一条数据。
④ 确认数据已经写入。
关闭k8s-node2,模拟节点宕机故障,如图9-32所示。

图9-32
一段时间后,Kubernetes将MySQL迁移到k8s-node1,如图9-33所示。

图9-33
验证数据的一致性,如图9-34所示。

图9-34
MySQL服务恢复,数据也完好无损。
9.4 小结
本章我们讨论了Kubernetes如何管理存储资源。
emptyDir和hostPath类型的Volume很方便,但可持久性不强,Kubernetes支持多种外部存储系统的Volume。
PV和PVC分离了管理员和普通用户的职责,更适合生产环境。我们还学习了如何通过StorageClass实现更高效的动态供给。
最后,我们演示了如何在MySQL中使用PersistentVolume实现数据持久性。