通过本期的docker实战后,切身体会到容器在实际业务领域应用的快捷性,部署方便,发布应用方便,但这一切的基础,就是docker的持久化存储,只有实现了持久化存储,容器才能在负载均衡和调度中始终保持业务的稳定性。
以下内容为转载:
什么是持久化存储
我们知道所谓容器挂载卷就是将宿主机的目录挂载到容器中的某个目录。而持久化则意味着这个目录里面的内容不会因为容器被删除而清除,也不会和当前宿主机有什么直接关系,而是一个外部的。这样当POD重建以后或者在其他主机节点上启动后依然可以访问这些内容。不过之前说过hostPath和emptyDir则推荐使用,因为前者和当前宿主机有必然联系而后者就是一个随POD删除而被删除的临时目录。
宿主机是如何挂载远程目录的
挂载过程会有不同,这取决于远程存储的类型,它是块设备存储还是文件设备存储。但是不管怎么样POD有这样一个目录/var/lib/kubelet/pods//volumes/kubernetes.io~/这个目录是POD被调度到该节点之后,由kubelet为POD创建的。因为它一定会被创建,因为系统中的默认secret就会被挂载到这里。之后就要根据存储设备类型的不同做不同处理。
文件存储设备
以nfs这种文件设备存储来说。我们依然启动之前的容器继续使用之前的PVC。
由于这个POD运行在node01节点,我们登陆node01节点,查看这个目录
/var/lib/kubelet/pods//volumes/kubernetes.io~/当你创建POD的时候它由于它被调度到node01节点,所以会创建这个目录,而且根据YAML中的定义就也会在这个目录中创建你在volumesMount中定义的目录,如下图:
通过命令查看在本地宿主机的挂载情况
由于创建了必要的目录,那么kubelet就直接使用mount命令把nfs目录挂载到这个目录上volumes/kubernetes.io~/,注意这时候仅仅是把这个远程存储挂载到宿主机目录上,要想让容器使用还需要做调用相关接口来把这个宿主机上的目录挂载到容器上。所以当准备好之后启动容器的时候就是利用CRI里的mounts参数把这个宿主机的目录挂载到容器中指定的目录上,就相当于执行docker run -v。
不过需要注意的是由于nfs文件存储不是一个块设备,所以宿主机系统需要扮演的就是nfs客户端角色,kubelet就是调用这个客户端工具来完成挂载的。
块存储设备
块存储设备你可以理解为一个磁盘。这个的处理要稍微复杂一点,就好像你为Linux服务器添加一块磁盘一样,你得先安装然后分区格式化之后挂载到某个目录使用。
/var/lib/kubelet/pods//volumes/kubernetes.io~/这个目录依然会创建。当POD被调度到该节点上会做如下操作
首先要安装一个块设备存储到宿主机(不是物理安装,而是通过API来安装),如何安装取决于不同块存储设备的API,很多云厂商有这种块存储设备比如Google的GCE。
格式化磁盘,
把格式化好的磁盘设备挂载到宿主机上的目录
启动容器挂载宿主机上的目录到容器中
相对于文件设备存储来说块设备要稍微复杂一点,不过上面这些过程都是自动的有kubelet来完成。
小结
负责把PVC绑定到PV的是一个持久化存储卷控制循环,这个控制器也是kube-manager-controller的一部分运行在master上。而真正把目录挂载到容器上的操作是在POD所在主机上发生的,所以通过kubelet来完成。而且创建PV以及PVC的绑定是在POD被调度到某一节点之后进行的,完成这些操作,POD就可以运行了。下面梳理一下挂载一个PV的过程:
1、用户提交一个包含PVC的POD
2、调度器把根据各种调度算法把该POD分配到某个节点,比如node01
3、Node01上的kubelet等待Volume Manager准备存储设备
4、PV控制器调用存储插件创建PV并与PVC进行绑定
5、Attach/Detach Controller或Volume Manager通过存储插件实现设备的attach。(这一步是针对块设备存储)
6、Volume Manager等待存储设备变为可用后,挂载该设备到/var/lib/kubelet/pods//volumes/kubernetes.io~/目录上
7、Kubelet被告知卷已经准备好,开始启动POD,通过映射方式挂载到容器中
StorageClass
PV是运维人员来创建的,开发操作PVC,可是大规模集群中可能会有很多PV,如果这些PV都需要运维手动来处理这也是一件很繁琐的事情,所以就有了动态供给概念,也就是Dynamic Provisioning。而我们上面的创建的PV都是静态供给方式,也就是Static Provisioning。而动态供给的关键就是StorageClass,它的作用就是创建PV模板。
创建StorageClass里面需要定义PV属性比如存储类型、大小等;另外创建这种PV需要用到存储插件。最终效果是,用户提交PVC,里面指定存储类型,如果符合我们定义的StorageClass,则会为其自动创建PV并进行绑定。
我们这里演示一下NFS的动态PV创建
kubernetes本身支持的动态PV创建不包括nfs,所以需要使用额外插件实现。nfs-client
我这里就按照网站的例子来创建,里面的内容毫无修改,当然你需要自己准备NFS服务器。由于用于提供动态创建PV的程序是运行在POD中,所以你需要保证你的Kubernetes节点到NFS的网络通畅,我这里就在我的Kubernetes集群的某个节点上建立的NFS服务。下面是PVC文件
当你应用这个PVC的时候,由于例子中的storageClassName也是managed-nfs-storage(当然这个名字你可以修改)就会去自动创建PV。
基于这种形式,我们只需要根据我们有的存储系统来定义StorageClass,通过名称来标识不同种类的存储,比如SSD、block-device这种名称,而不需要定义具体大小。那么使用人员就可以根据需要通过StorageClass的名字来使用,从而实现动态创建PV的过程。
这里有个要求就是你的存储系统需要提供某种接口来让controller可以调用并传递进去PVC的参数去创建PV,很多云存储都支持。可是也有不支持的,比如NFS就不支持所以我们需要一个单独的插件来完成这个工作。也就是例子中使用的quay.io/external_storage/nfs-client-provisioner镜像,但是创建PV也需要相关权限,也就是例子中rabc.yaml部分。在定义StorageClass中有一个叫做provisioner: fuseim.pri/ifs这个就是插件的名称,这个名称其实也就是官方例子中deployment中设置的名字,这个名字你可以修改。
当然我们说过有些本身就支持,比如下面的kubernetes官网中的一个AWS的例子:
kubernetes.io/aws-ebs就是kubernetes内置的存储插件名称,如果你使用aws就用这个名称就好。因为kubernetes就会去调用AWS的API来创建存储然后在创建PV。
这里你可能会有个疑问,为什么开篇的例子里面也用了storageClassName: normal,可是我们并没有定义任何StorageClass。其实虽然我们使用了,但是系统上并没有一个叫做normal的存储类,这时候还是静态绑定,只是绑定的时候它会考虑你的PV和PVC中的存储类名称是否一致。当然如果是静态绑定你可以不写storageClassName,因为如果开起一个的叫做DefaultStorageClassplugin插件就会默认有这样一个存储类,它会自动添加到你的任何没有明确声明storageClassName的PV和PVC中。
本地持久化存储
本地持久化存储(Local Persistent Volume)就是把数据存储在POD运行的宿主机上,我们知道宿主机有hostPath和emptyDir,由于这两种的特定不适用于本地持久化存储。那么本地持久化存储必须能保证POD被调度到具有本地持久化存储的节点上。
为什么需要这种类型的存储呢?有时候你的应用对磁盘IO有很高的要求,网络存储性能肯定不如本地的高,尤其是本地使用了SSD这种磁盘。
但这里有个问题,通常我们先创建PV,然后创建PVC,这时候如果两者匹配那么系统会自动进行绑定,哪怕是动态PV创建,也是先调度POD到任意一个节点,然后根据PVC来进行创建PV然后进行绑定最后挂载到POD中,可是本地持久化存储有一个问题就是这种PV必须要先准备好,而且不一定集群所有节点都有这种PV,如果POD随意调度肯定不行,如何保证POD一定会被调度到有PV的节点上呢?这时候就需要在PV中声明节点亲和,且POD被调度的时候还要考虑卷的分布情况。
定义PV
如果你在node02上也有/data/vol1这个目录,上面这个PV也一定不会在node02上,因为下面的nodeAffinity设置了主机名就等于node01。
另外这种本地PV通常推荐使用的是宿主机上单独的硬盘设备,而不是和操作系统共有一块硬盘,虽然可以这样用。
定义存储类
这里的volumeBindingMode: WaitForFirstConsumer很关键,意思就是延迟绑定,当有符合PVC要求的PV不立即绑定。因为POD使用PVC,而绑定之后,POD被调度到其他节点,显然其他节点很有可能没有那个PV所以POD就挂起了,另外就算该节点有合适的PV,而POD被设置成不能运行在该节点,这时候就没法了,延迟绑定的好处是,POD的调度要参考卷的分布。当开始调度POD的时候看看它要求的LPV在哪里,然后就调度到该节点,然后进行PVC的绑定,最后在挂载到POD中,这样就保证了POD所在的节点就一定是LPV所在的节点。所以让PVC延迟绑定,就是等到使用这个PVC的POD出现在调度器上之后(真正被调度之前),然后根据综合评估再来绑定这个PVC。
定义PVC
可以看到这个PVC是pending状态,这也就是延迟绑定,因为此时还没有POD。
定义POD
这个POD被调度到node01上,因为我们的PV就在node01上,这时候你删除这个POD,然后在重建该POD,那么依然会被调度到node01上。
总结:本地卷也就是LPV不支持动态供给的方式,延迟绑定,就是为了综合考虑所有因素再进行POD调度。其根本原因是动态供给是先调度POD到节点,然后动态创建PV以及绑定PVC最后运行POD;而LPV是先创建与某一节点关联的PV,然后在调度的时候综合考虑各种因素而且要包括PV在哪个节点,然后再进行调度,到达该节点后在进行PVC的绑定。也就说动态供给不考虑节点,LPV必须考虑节点。所以这两种机制有冲突导致无法在动态供给策略下使用LPV。换句话说动态供给是PV跟着POD走,而LPV是POD跟着PV走。