持久化存储
最后更新于
这有帮助吗?
容器化一个应用比较复杂的地方是状态的管理,常见的状态就是存储状态。
PV:描述持久化存储数据卷,这个API对象主要定义的是持久化存储在宿主机上的目录,比如一个NFS挂载目录
PVC:描述的Pod所希望使用的持久化存储的属性,比如Volume存储的大小、可读写权限等
持久化存储有两种方式:静态供应或动态供应。
静态Provisioning:由集群管理员事先去规划这个集群中的用户会怎样使用存储,它会先预分配一些存储,也就是预先创建一些 PV;然后用户再提交自己的存储需求(也就是 PVC)的时候,Kubernetes 内部相关组件会帮助它把 PVC 和 PV 做绑定;之后用户再通过 Pod 去使用存储的时候,就可以通过 PVC 找到相应的 PV,它就可以使用了。
静态供应方式的不足:需要集群管理员预分配,预分配其实是很难预测用户真实需求的。
举一个最简单的例子:如果用户需要的是 20G,然而集群管理员在分配的时候可能有 80G 、100G 的,但没有 20G 的,这样就很难满足用户的真实需求,也会造成资源浪费。
通常情况,PV对象是由运维人员事先创建在Kubernetes集群中待用,可以定义如下的PV:
动态Provisioning:现在集群管理员不预分配 PV,他写了一个模板文件,这个模板文件是用来表示创建某一类型存储(块存储,文件存储等)所需的一些参数,这些参数是用户不关心的,给存储本身实现有关的参数。用户只需要提交自身的存储需求,也就是PVC文件,并在 PVC 中指定使用的存储模板(StorageClass)。
Kubernetes 集群中的管控组件,会结合 PVC 和 StorageClass 的信息动态生成用户所需要的存储(PV),将 PVC 和 PV 进行绑定后,Pod 就可以使用 PV 了。通过 StorageClass 配置生成存储所需要的存储模板,再结合用户的需求动态创建 PV 对象,做到按需分配,在没有增加用户使用难度的同时也解放了集群管理员的运维工作。
PVC对象通常由开发人员创建,或者以PVC模板的方式成为StatefulSet的一部分,然后由StatefulSet控制器负责创建带编号的PVC,如下:
用户创建的PVC要真正被容器使用起来,就必须先和某个符合条件的PV进行绑定,这需要检查两部分:
PV和PVC的spec字段,比如PV的存储(Storage
)大小,必须满足PVC的要求
PV和PVC的storageClassName
字段必须一样
绑定成功后,Pod就能够像使用hostPath等常规类型的Volume一样,在YAML文件中声明使用这个PVC,如下所示:
Pod在volumes字段中声明要使用的PV名字,等这个Pod创建后,kubelet就会把这个PVC所对应的PV挂载到Pod容器内的目录上。PV与PVC的设计,和面向对象的思想完全一样。开发人员只需要和PVC这个接口打交道,不必关心具体的实现细节。
PVC可以理解为持久化存储的“接口”,提供了对某种持久化存储的描述,但不提供具体的实现
PV负责持久化存储的具体实现
创建Pod时,集群中没有适合的PV与PVC进行绑定,即容器需要的volume不存在,那么Pod的启动就会报错。当需要的PV被创建后,PVC可以再次与之绑定,从而使得Pod能够顺利启动。
Volume Controller专门处理持久化存储的控制器,维护多个控制循环,其中之一就是帮助PV和PVC进行绑定,即PersistenVolumeController。它会不断的查看当前每一个PVC是否处于Bound状态,如果没有处于绑定状态,则会遍历全部可用的PV,并尝试将PVC与PV进行绑定。这样Kubernetes就能保证,用户提交的每一个PVC,只要有合适的PV出现,就能够很快进入绑定状态。
所谓的PV与PVC的绑定,就是将这个PV对象的名字填写在PVC对象spec.volumeName
字段,这样获取了PVC就能知道绑定的PV。
所谓容器的Volume,其实就是一个将宿主机上的目录,跟一个容器里的目录绑定挂载在一起。所谓的持久化Volume,就是这个宿主机的目录具备持久性(目录中的内容不会因为容器删除而被清理掉,也不跟当前宿主机绑定)。当容器被重启或者其他节点上重建出来,它仍然可以通过挂载这个Volume访问到这些内容。
hostPath、emptyDir类型的volume都不具备这样的特性:既会被kubelet清理掉,也不能被迁移到其他节点上。大多数持久化volume的实现依赖于远程存储服务:
远程文件存储(NFS、GlusterFS)
远程块存储(公有云提供的远程磁盘)等
Kubernetes的工作就是使用这些存储服务,来为容器准备一个持久化的宿主机目录,以供将来进行绑定挂载使用。所谓持久化是容器在这个目录里写的文件,都会保存在远程存储中,从而使得这个目录具备了持久性。
用户在提交 PVC 的时候,最重要的两个字段 Capacity 和 AccessModes。
Capacity:是存储对象的大小;
AccessModes:是用户需要关心的使用这个 PV 的方式。它有三种使用方式:
第一种是单 node 读写访问;
第二种是多个 node 只读访问,是常见的一种数据的共享方式;
第三种是多个 node 上读写访问。
在提交 PVC 后,Kubernetes集群中的相关组件首先通过为 PV 建立的 AccessModes 索引找到所有能够满足用户的 PVC 里面的 AccessModes 要求的 PV list,然后根据PVC的 Capacity,StorageClassName, Label Selector 进一步筛选 PV,如果满足条件的 PV 有多个,选择 PV 的 size 最小的,accessmodes 列表最短的 PV,也即最小适合原则。
ReclaimPolicy:用户方 PV 的 PVC 在删除之后,PV 应该做如何处理,常见的有三种方式:
第一种方式 Recycle,现在 Kubernetes 中已经不推荐使用了;
第二种方式 delete,PVC 被删除之后,PV 也会被删除;
第三种方式 Retain,保留,后面这个 PV 需要管理员来手动处理。
StorageClassName:动态 Provisioning 时必须指定的一个字段,要指定到底用哪一个模板文件来生成 PV ;
NodeAffinity:创建出来的 PV,它能被哪些 node 去挂载使用,其实是有限制的。通过 NodeAffinity 来声明对node的限制,这样其实对使用该PV的Pod调度也有限制,就是说 Pod 必须要调度到这些能访问 PV 的 node 上,才能使用这块 PV。
PV 的状态流转:
首先在创建 PV 对象后,它会处在短暂的pending 状态,
等真正的 PV 创建好之后,它就处在 available 状态,
available 状态意思就是可以使用的状态,用户在提交 PVC 之后,被 Kubernetes 相关组件做完 bound(即:找到相应的 PV),这个时候 PV 和 PVC 就结合到一起了,此时两者都处在 bound 状态,
当用户在使用完 PVC,将其删除后,这个 PV 就处在 released 状态,之后它应该被删除还是被保留,会依赖 ReclaimPolicy。
当 PV 已经处在 released 状态下,它是没有办法直接回到 available 状态,也就是说无法被一个新的 PVC 去做绑定。如果想把已经 released 的 PV 复用:
第一种方式:新建一个 PV 对象,然后把之前的 released 的 PV 的相关字段的信息填到新的 PV 对象里面,这个 PV 就可以结合新的 PVC 了;
第二种方式:在删除 Pod 之后,不删除 PVC 对象,这样给 PV 绑定的 PVC 还是存在的,下次 Pod 使用的时候,就可以直接通过 PVC 去复用。Kubernetes中的 StatefulSet 管理的 Pod 带存储的迁移就是通过这种方式。
CSI(container storage interface)是Kubernetes社区对存储插件实现(out of tree,不在官方仓库中)的官方推荐方式。
CSI的实现大体可以分为两部分:
第一部分是由Kubernetes社区驱动实现的通用的部分,对应上图中的的csi-provisioner和 csi-attacher controller;
第二部分是由云存储厂商实现,对接云存储厂商的 OpenApi,主要是实现真正的 create/delete/mount/unmount 存储的相关操作,对应到上图中的csi-controller-server和csi-node-server。
当用户提交 YAML 之后,Kubernetes内部的处理流程:
用户在提交 PVC YAML 文件时,首先会在集群中生成一个 PVC 对象,
然后 PVC 对象会被 csi-provisioner controller watch到,csi-provisioner 会结合 PVC 对象以及 PVC 对象中声明的 storageClass,通过 GRPC 调用 csi-controller-server,
然后到云存储服务这边去创建真正的存储,并最终创建出来 PV 对象,
最后由集群中的 PV controller 将 PVC 和 PV 对象做 bound 之后,这个 PV 就可以被使用了。
用户在提交 Pod 之后,首先会被调度器调度选中某一个合适的node,
然后该 node 上面的 kubelet 在创建 Pod 流程中会通过首先 csi-node-server 将之前创建的 PV 挂载到 Pod 可以使用的路径,
然后 kubelet 开始 create && start Pod 中的所有 container。
主要分为三个阶段:
第一个阶段(Create阶段):主要是创建存储
用户提交完 PVC,由 csi-provisioner 创建存储,并生成 PV 对象,
然后 PV controller 将 PVC 及生成的 PV 对象做 bound。
第二个阶段(attach阶段):将那块存储挂载到 node 上面(通常为将存储load到node的/dev
下面)
然后用户提交 Pod YAML 的时候,首先会被调度选中某一个合适的node,
等 Pod 的运行 node 被选出来之后,会被 AD Controller watch 到 Pod 选中的 node,它会去查找 Pod 中使用了哪些 PV。
然后它会生成一个内部的对象叫 VolumeAttachment 对象,从而去触发 csi-attacher去调用csi-controller-server 去做真正的 attache 操作,attach操作调到云存储厂商OpenAPI。
这个 attach 操作就是将存储 attach到 Pod 将会运行的 node 上面。
第三个阶段(mount阶段):将对应的存储挂载到 Pod 可以使用的路径
kubelet 创建 Pod的过程中,在创建 Pod 的过程中,首先要去做一个 mount,这里的 mount 操作是为了将已经attach到这个 node 上面那块盘,进一步 mount 到 Pod 可以使用的一个具体路径,
然后 kubelet 才开始创建并启动容器。
当Pod调度到某个节点,kubelet就会负责为这个Pod创建它的Volume目录,默认情况下,kubelet为volume创建的在宿主机上的目录是/var/lib/kubelet/pods/<Pod的ID>/volumes/Kubernetes.io~<Volume类型>/<Volume名字>
。
kubelet根据volume的类型进行具体的操作,如果volume是远程块存储,如GCE的Persistent Disk,kubelet需要先调用GCP的API,将它所提供的Persistent Disk挂载到Pod所在的宿主机上。相当于执行如下命令:
以上操作是第一个阶段,在Kubernetes中称为Attach。
为了能够使用这个远程磁盘,kubelet进行第二个操作,格式化这个磁盘设备,然后挂载到宿主机指定的挂载点(宿主机的volume目录)上。相当于执行如下操作:
以上操作是第二个阶段,称为Mount。
Mount阶段完成后,这个Volume的宿主机目录就是一个“持久化”目录,容器在它里面写入的内容就会被保存在GCP的远程磁盘中。
如果volume的类型是远程文件存储NFS,kubelet的处理过程比较简单,直接进行Mount阶段即可。因为远程文件存储一般没有存储设备需要挂载到宿主机。
在Mount的过程中kubelet需要作为client,将远端NFS服务器的目录(如/
目录)挂载到Volume的宿主机目录上,即相当于执行如下命令:
通过这个操作volume的宿主机目录就成为了一个远程NFS目录的挂载点,在这个目录下写入的所有文件就会被保存在远程NFS服务上。在具体的Volume插件的实现接口上,Kubernetes分别为这两个阶段提供了两种不同的参数列表:
Attach:Kubernetes提供的可用参数是nodeName,即宿主机的名字
Mount:Kubernetes提供的可用参数的dir,即 Volume的宿主机目录
得到持久化的Volume宿主机目录后,kubelet只要把这个Volume目录通过CRI里的Mounts参数,传递给Docker,然后就可以为Pod里的容器挂载这个持久化的Volume,相当于执行如下操作:
相应的,在删除PV的时候,Kubernetes也需要umount和Dettach两个阶段。
在这个PV的处理过程中,与Pod和容器的启动流程没有太多的耦合,只要kubelet在向Docker发起CRI请求之前,确保持久化的宿主机目录已经处理完毕即可。在Kubernetes中,关于PV的处理过程,是独立与kubelet主控制循环(kubelet sync loop)之外的两个控制循环实现的。
Attach和Dettach操作,由Volume Controller负责维护,这个控制循环的名字叫AttachDettchController。它的作用就是不断地检查每一个Pod对应的PV,和这个Pod所在宿主机之间的挂载情况,从而决定是否需要对这个PV进行Attach或者Dettach操作。
Kubernetes内置的控制器,Volume Controller是kube-controller-manager的一部分,所有AttachDettach也是运行在Master节点,Attach操作只需要调用公有云或者具体存储项目的API,并不需要在具体宿主机上执行操作。
Mount和Umount操作,必须发生在Pod对应的宿主机上,所以必须是kubelet组件的一部分,这个控制循环的名字,叫作VolumeManagerReconciler,运行起来之后,是一个独立于kubelet主循环goroutine。
通过将Volume的处理同kubelet的主循环解耦,避免了耗时的远程挂载操作拖慢kubelet的主控制循环,进而导致Pod的创建效率大幅下降的问题。
kubelet的一个主要设计原则就是它的主控制循环绝对不能被block。
PV的创建需要运维人员完成,在大规模的生产环境中,这个工作太麻烦,需要预先创建很多PV,随着需求的变化需要继续添加新的PV。Kubernetes提供了一套可以自动创建PV的机制,Dynamic Provisioning,它的核心在于StorageClass API对象。StorageClass对象的作用,就是创建PV的模板。它会定义如下两个部分:
PV的属性,如存储类型,Volume大小等
创建这个PV所需要用到的存储插件,如Ceph等
有了这两个信息之后,Kubernetes就能够根据用户提交的PVC找到一个对应的StorageClass,然后Kubernetes就会调用该StorageClass声明的存储插件,创建出需要的PV。如下所示,volume的类型是GCE的Persistent Disk:
如下所示,在本地集群使用rook存储服务:
在StorageClass的YAML文件之后,运维人员就可以创建StorageClass了,开发人员只需要在PVC中指定storage即可:
Kubernetes只会将storageclass相同的PVC和PV绑定起来。
注意 StorageClass并不是专门为了Dynamic Provisioning而设计的。
上面的例子中在 PV 和 PVC 里都声明了 storageClassName=manual。而集群里,实际上并没有一个名叫 manual 的StorageClass 对象。这完全没有问题,这个时候 Kubernetes 进行的是 Static Provisioning,但在做绑定决策的时候,它依然会考虑 PV 和PVC 的 StorageClass 定义。这么做的好处也很明显:这个 PVC 和 PV 的绑定关系,就完全在自己的掌控之中。
如果集群已经开启了名叫 DefaultStorageClass 的 Admission Plugin,它就会为PVC 和 PV 自动添加一个默认的StorageClass;否则,PVC 的 storageClassName 的值就是“”,这也意味着它只能够跟 storageClassName 也是“”的 PV 进行绑定。
PVC 描述的是Pod想要使用的持久化存储的属性,比如存储的大小、读写权限等
PV 描述的是具体的Volume的属性,比如volume的类型,挂载的目录,远程存储服务器地址等
StorageClass的作用是充当PV的模板,并且只有属于同一个StorageClass的PV和PVC才可以绑定
StorageClass的另一个重要的作用是指定Provisioner(存储插件),这时候,如果存储插件支持Dynamic Provisioning的话,Kubernetes就可以自动创建PV。
如何解决Kubernetes内置的20种持久化数据集不能满足需求的问题?在开源项目中,“不能用、不好用、需定制开发”是开源项目落地的三大常态。在持久化存储中,本地持久化存储的需求最高,这样的好处很明显。Volume直接使用本地磁盘(SSD),读写性能相比于大多数远程存储来说,要好很多。
Kubernetes v1.10之后,依靠PV、PVC实现了本地持久化存储,Local Persistent Volume。
Local Persistent Volume并不适用于所有应用。它的使用范围非常固定,如高优先级的系统应用,需要在多个不同节点上存储数据,并且对I/0较为敏感。典型的应用包括:
分布式数据存储:MongoDB、Cassandra等
分布式文件系统:GlusterFS、Ceph等
需要在本地磁盘上进行大量数据缓存的分布式应用
相比于正常的PV,一旦这些节点宕机且不能恢复,Local Persistent Volume的数据就可能丢失,这就要求使用Local Persistent Volume的应用必须具备数据备份和恢复能力,允许把这些数据定时备份到其他位置。
设计的难点:
如何把本地磁盘抽象成PV?
调度如何保证Pod始终能被正确地调度到它所请求的Local Persistent Volume所在节点?
假设将一个Pod声明使用类型为Local的PV,而这个PV其实是一个hostPath类型的Volume,如果这个hostPath对应的目录在A节点上被事先创建好,那么只需要给这个Pod加上nodeAffinity=nodeA。事实上,绝不能把一个宿主机上的目录当做PV使用。因为:
本地目录的存储行为完全不可控:它所在磁盘随时可能被应用写满,甚至造成整个宿主机宕机
不同的本地目录之间也缺乏哪怕最基础的I/O隔离机制
所以,一个Local Persistent Volume对应的存储介质,一定是一块额外(非宿主机根目录使用的主硬盘)挂载在宿主机的错或者块设备,即一个PV一块盘。
调度器如何保证Pod始终能被正确地调度到它所请求的Local Persistent Volume。
对于常规的PV,Kubernetes都是先调度Pod到某个节点,然后再通过Attach和Mount两个阶段来持久化这台机器上的Volume目录,进而完成Volume目录与容器的绑定挂载
对于Local PV来说,节点上可供使用的磁盘(块设备),必须是运维人员提前准备好(在不同节点上挂载情况可能完全不同,甚至有的节点上没有这种磁盘)
所以调度器必须知道所有节点与Local PV对应的磁盘的关联关系,然后根据这个信息来调度Pod,即调度的时候考虑Volume分布。在Kubernetes的调度器中,VolumeBindingChecker过滤条件负责在调度时考虑Volume的分布情况,在v1.11中,这个过滤条件默认开启。因此在使用Local PV前,需要在集群里配置好磁盘或块设备:
在公有云上等同于给虚拟机额外挂载一个磁盘(如GCE的Local SSD类型的磁盘)
在私有集群中,有两个方法来解决:
给宿主机挂载并格式化一个可用的本地磁盘
对于实验环境,在宿主机上挂载几个RAM Disk来模拟本地磁盘
以内存盘为例子进行挂载。
在节点上创建挂载点/mnt/disks
用RAM Disk模拟本地磁盘
为本地磁盘定义PV:
创建PV:
使用PV和PVC的最佳实践,创建一个StorageClass来描述这个PV:
通常,在提交了PV和PVC的YAML后,Kubernetes会根据它们的属性以及StorageClass来进行绑定,绑定成功后Pod才能通过PVC使用这个PV,但是在Local PV中这个流程不行。
举个例子,有个Pod声明使用pvc-1,规定只能运行在node-2,此时集群中有两个属性(大小和读写权限)相同的Local PV(PV-1对应磁盘在node-1,PV-2对应磁盘在node-2)。假设Kubernetes的Volume控制循环里,首先检查到PVC-1和PV-1匹配,直接绑定在一起。然后Kubernetes创建Pod,问题来了,Pod声明的PVC-1与node-1的PV-1绑定了,根据调度器必须考虑Volume分布的原则,Pod被调度到node-1,但是我们规定是Pod要运行在node-2,所有Pod调度失败。
所以使用Local PV的时候,必须要推迟绑定PV和PVC。具体而言就是推迟到调度的时候进行绑定。等待第一个声明使用该PVC的Pod出现在调度器之后,调度器再综合考虑所有的调度规则(包括每个PV所在的节点位置),来统一决定,这个Pod声明的PVC应该跟哪个PV进行绑定。
通过延迟绑定机制,原本实时发生在PVC和PV的绑定过程,就被延迟到了Pod第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响Pod正常调度。在具体实现中,调度器实际上维护了一个与Volume Controller类似的控制循环,专门负责为那些声明了“延迟绑定”的PV和PVC进行绑定工作。
当一个Pod的PVC尚未完成绑定时,调度器也不会等待,而是直接把这个Pod重新放回调度队列,等到下一个调度周期再做处理。
然后创建普通的PVC就能让Pod来使用Local PV:
PVC会一直处于pending状态,等待绑定。
编写一个Pod来声明使用这个PVC:
一旦创建这个Pod之前处于pending的pvc就会编程Bound状态:
在创建的Pod进入调度器之后,绑定操作就开始进行。像Kubernetes这样构建出来的、基于本地存储的Volume,完全可以提供容器持久化存储的功能,所以像StatefulSet这样的有状态编排工具也完全可以通过声明Local类型的pv和pvc来管理应用的存储状态。
手动创建PV(即static的PV管理方式)在删除PV时需要按如下流程执行操作,否则PV删除失败:
删除使用这个PV的Pod
从宿主机移除本地磁盘(如umount)
删除pvc
删除pv
上述操作比较繁琐,Kubernetes提供了Static Provision来帮助管理这些PV。
比如,所有磁盘都挂载在宿主机的/mnt/disks
目录下,当Static Provision启动后,通过DaemonSet自动检查每个宿主机的/mnt/disks
目录,然后调用Kubernetes API,为这些目录下的每一个挂载,创建一个对应的PV对象出来,如下所示:
这个PV里面的各种定义,StorageClass、本地片挂载点位置,都是通过Provision的配置文件指定的。这个Provision本身也是一个External Provision。
自定义存储插件:PV和PVC的实现原理,是处于整个存储体系的可扩展性的考虑,在Kubernetes中,存储插件的开发有两种方式:FlexVolume和CSI。