针对不同的应用以及不同的场景,我们往往需要不同的存储方案以满足我们的需求,而各类存储方案的实现以及配置方法各异,直接使用通常会造成较大的心智负担。因此,Kubernetes作为容器编排领域的事实标准,设计一套通用的存储框架用以无缝接入尽量多的存储方案并能够供用户方便地使用,就显得尤为重要了。当然,这样一套框架也不是一蹴而就的,对于存储方案的集成方式大致上经历了InTree(与存储方案对接的实现直接硬编码至Kubernetes中),FlexVolume以及CSI三个阶段。本文将对最新的基于CSI的Kubernetes存储框架进行分析,包含的主要内容如下:
对于存储来说,普通用户最关心的或者只想关心的是我要给应用挂载一个多大的盘,至于如何配置底层存储以满足需求,这对存储的使用者来说往往是一种额外的负担,例如,要学会如何配置Ceph以获取一块存储对任何初学者来说都不是一件容易的事情。因此,这也是Kubernetes设计PV/PVC/SC这一套资源对象的根本原因:
在用户声明了PV/PVC/SC之后,Kubernetes又是做了哪些工作将声明转换为真正的存储资源供应用使用呢?同样,我们可以用三个单词进行描述,Provision/Attach/Mount,这三个操作的具体含义如下:
事实上这三个动作正是对底层存储方案的抽象,任何存储方案至多只要实现这三个动作(例如基于NFS的存储就无需实现Provision/Attach)就能接入Kubernetes。至此,我们可以将Kubernetes的存储框架划分为一个三层结构,如下图所示:
我们知道,“资源对象+控制器”是Kubernetes架构体系的基础,对于存储的实现也不例外。存储相关的控制器会持续地对集群中存储相关的资源对象进行List/Watch并根据资源对象的增删改查做出相应的调整,例如,为PVC找到合适的PV进行绑定,根据策略对PV进行回收等等。如果真正需要操纵底层的存储资源,则通过标准的Volume Plugin进行实现。最后,我们对框架的主体,即控制器部分进行说明。
如上图所示,存储相关的控制器主要为:PersistentVolume Controller,AttachDetach Controller,Kubelet,三者通过PV/PVC/SC/Pod/Node这五个资源对象进行交互,最终完成将用户指定的存储资源挂载到指定应用负载的任务。虽然随着整体架构的发展,各个Controller的存在形式(集成到Kube-Controller-Manager中或者以独立的方式运行)或者功能划分(Attach/Detach从Kubelet移动到AD Controller)会有所不同,但是一种存储类型要接入Kubernetes总是需要有相应的组件来完成类似的功能。下面就以上图为例,依次说明各个Controller在框架中所起的作用,一方面能够对整个框架的执行过程有一个宏观的了解,另一方面也能为后续两块内容:存储约束下的容器调度以及CSI奠定基础。
PV Controller主要包含两个逻辑处理单元:Claim Worker以及Volume Worker,它们基于标准的Kubernetes Controller模式分别对PVC和PV的Add/Update/Delete进行处理。不过PV/PVC这两个资源对象状态以及字段繁多,更由于两者之间存在绑定关系,因此无论是PV还是PVC的Add/Update/Delete都涉及大量边界条件的处理。所以下面将只对最核心的,即Unbound的PVC如何寻找合适的PV并与之绑定的过程,进行分析,从而能够对PV Controller的整体架构有着提纲挈领式的认识而不至于过度陷入细节而无法自拔(注:暂时不考虑Delay Binding的场景)。
pv.kubernetes.io/bind-completed
的键值对,则说明PVC仍未绑定。Spec.Claim
字段与目标PVC完全匹配的PV。若不存在pre-bound且匹配的PV,则在剩余匹配的PV中选择Size最小的PV。Spec.ClaimRef
字段会用PVC对应的内容进行填充,从而保证在下一次同步过程的步骤2中,该PVC和PV能优先绑定。Bound
,表示二者已经绑定。否则,PVC处于Pending
状态,等待下一次的同步。总之,PV Controller用于对PV/PVC这两个资源对象的生命周期以及状态进行管理,简答地说,就是努力为PVC绑定最合适的PV。由于PV以及PVC之间的关联异常紧密,因此其中一个资源对象发生变化往往另一个相对的资源也需要做变更从而保持整体的一致性,这也在一定程度上增加了处理逻辑的复杂性,针对某个具体场景或者异常条件下的处理,建议直接分析源码,在此不再赘述。
AD Controller,即Attach/Detach Controller,包含如下核心组成部分:
AD Controller的架构如上所示,执行流程如下:
Status.VolumesAttached
字段表明该节点当前附着的Volumes,因此当AD Controller初始化时会遍历集群中所有的Node对象并利用该字段填充ASW。Status.VolumesAttached
字段, 2)遍历DSW中所有应该被附着的Volume,如果它不存在于ASW中则调用底层对应的Volume Plugin将该Volume Attach并更新相应节点的Status.VolumesAttached
字段关于AD Controller架构的简要描述如上,简单地说就是分别从Pod中获取期望的Volume的附着状态,从Node中获取初始的、实际的Volume附着状态并不断在两者之间进行调谐。需要注意的是并不是所有的Volume类型都需要Attach/Detach,而且Attach/Detach操作事实上最初是封装在Kubelet中的,但是考虑到需要将Attach/Detach独立于节点的可用性并且在公有云场景下,对云盘的Attach/Detach往往意味着更大的操作权限,将它们下放到每个节点显然是不太合适的,因此最终独立出了AD Controller用于处理此类操作,具体可参加其设计文档。
Kubelet是Kubernetes中位于每个节点的Agent,负责对调度到该节点的Pod的生命周期进行管理,Kubelet用众多的Manager共同协作来完成这一任务,其中对存储进行管理的是Volume Manager。如果阅读过相关的代码就会发现Volume Manager的处理逻辑与AD Controller非常相似,它同样包含如下几个核心组成部分:
/var/lib/kubelet/pods/{podID}/volumes/{volume plugin}/{volume name}
)VolumeManager与AD Controller的执行过程也非常相似:
Node.Status.VolumesAttached
字段包含这个Volume,最后调用底层的Volume Plugin,将Volume挂载到对应Pod的挂载目录(对于有的存储类型,可能需要先挂载到一个全局目录再Bind Mount
到Pod的挂载目录),3)遍历ASW中所有Unmounted Volume(所谓Unmounted Volume是指附着到本节点但是已经没有Pod引用的Volume),如果它是需要先挂载到全局的Volume类型,则先将其从全局目录中卸载,最后将它从ASW中移除需要注意的是,如上文所述,最初其实是由Kubelet来执行Attach/Detach操作的,而最终是由谁来执行该操作会根据每个节点的Kubelet的配置不同而不同。如果一个节点的Kubelet的启动参数中,enable-controller-attach-detach
为true,则该节点的Attach/Detach操作由AD Controller执行,否则该节点由Kubelet进行该操作。如果是后者,则Volume Manager的Reconciler在上文的步骤2中,则应该主动执行Attach操作,包括在第三步中进行Detach,而不应该等待本节点的Node.Status.VolumesAttached
发送变更。
综上即是Kubernetes存储框架的概要描述,简单地说就是PV Controller根据PVC找到或者创建合适的PV进行绑定,AD Controller(或者Kubelet)有必要的话,将相应的Volume附着到引用该Volume的Pod被调度到的节点上,最后该节点的Kubelet将Volume挂载到引用该Pod的相应的目录。不过,接下来我们也将看到根据底层存储的不同,整个框架的执行过程乃至各个组件的作用都会发生一定的变化,尤其是在引入CSI之后,但这也仅仅是对上述框架进行的扩展。有了上面这个“原始”框架的铺垫,后面的内容理解起来应该不至于太过困难。
从上文的分析来看,Pod的调度和存储资源(PV)的创建过程是平行的,两者可以异步执行。这个结论的前提是,存储对于所有节点都是平等的,无论Pod调度到哪个节点,PV对应的存储资源都能被该节点的Pod使用。但是现实不会如此美好,存储往往是存在拓扑限制的,即存储资源只能被集群中的某些节点使用。因此存储拓扑与Pod的调度往往是会相互影响的,一般会反映为如下两个问题:
这个问题显然需要通过扩展Kubernetes的调度器实现,将系统当前的存储拓扑结构纳入调度的约束条件。已知Kubernetes的调度器主要可以分为Predicate和Priority,前者用于筛选出符合调度条件的节点,后者用于在符合条件的节点中挑选出最优解。这里我们只需要扩展Predicate阶段,增加存储相关的筛选过程即可。当前内置的存储相关的筛选过程包括:VolumeZonePredicate、CSIMaxVolumeLimitPredicate、VolumeBindingPredicate等等。这里着重对VolumeBindingPredicate进行说明,它的主要作用在于判断当前节点是否能满足该Pod所有PVC/PV对Node Affinity的要求。其具体执行过程如下:
WaitForFirstConsumer
,即延迟绑定,通过下文可以了解到,利用这种绑定类型即可解决本节中的第二个问题上述是Kubernetes内置的基于存储拓扑对可调度节点的筛选,事实上对于某些有着特殊要求的存储资源类型,原生的Kubernetes调度器是不够的,我们往往需要利用Scheduler Extender
等机制来进一步扩展存储拓扑对调度过程的影响,基于LVM的动态本地存储就涉及到了类似的实现。
这个问题最典型的场景莫过于在公有云上一个集群跨多个可用区,但是存储不能跨可用区共享。因此必须在已知Pod调用结果的情况下才能在对应可用区创建相应的存储资源。而这个问题的解决方法已经在上面提到了,即将StorageClass的VolumeBindingMode设置为WaitForFirstConsumer
。PV Controller也需要对此进行一定的扩展:
WaitForFirstConsumer
且PVC的annotations中不包含Key为"volume.kubernetes.io/selected-node"的键值对,则跳过该PVC,暂不做处理原文:https://www.cnblogs.com/YaoDD/p/13057501.html