5.3 Scheduler原理解析

前面深入分析了Controller Manager及其所包含的各个组件的运行机制,本节将继续对Kubernetes中负责Pod调度的重要功能模块—Kubernetes Scheduler的工作原理和运行机制进行深入分析。

我们知道,Kubernetes Scheduler是负责Pod调度的进程(组件),随着Kubernetes功能的不断增强和完善,Pod调度也变得越来越复杂,Kubernetes Scheduler内部的实现机制也在不断优化,从最初的两阶段调度机制(Predicates &Priorities)发展到后来的升级版的调度框架(Scheduling Framework),以满足越来越复杂的调度场景。

为什么Kubernetes里的Pod调度会如此复杂?这主要是因为Kubernetes要努力满足各种类型应用的不同需求并且努力“让大家和平共处”。Kubernetes集群里的Pod有无状态服务类、有状态集群类及批处理类三大类,不同类型的Pod对资源占用的需求不同,对节点故障引发的中断/恢复及节点迁移方面的容忍度都不同,如果再考虑到业务方面不同服务的Pod的优先级不同带来的额外约束和限制,以及从租户(用户)的角度希望占据更多的资源增加稳定性和集群拥有者希望调度更多的Pod提升资源使用率两者之间的矛盾,则当这些相互冲突的调度因素都被考虑到时,如何进行Pod调度就变成一个很棘手的问题了。

为什么Kubernetes Scheduler的设计实现从一开始就比较复杂呢?我们知道,一开始,Scheduler就被设计成两阶段调度机制,而到了1.5版本以后,新的Scheduling Framework变得更加复杂,其原因其实很简单:调度这个事情无论让机器怎么安排,都不可能完全满足每个用户(应用)的需求。因此,让用户方便地根据自己的需求去做定制和扩展,就变成一个很重要也很实用的特性了。升级后的Scheduling Framework在这方面也做得更好了。

5.3.1 Scheduler的调度流程

Kubernetes Scheduler在整个系统中承担了“承上启下”的重要功能,“承上”是指它负责接收Controller Manager创建的新Pod,为其安排一个落脚的“家”—目标Node;“启下”是指安置工作完成后,目标Node上的kubelet服务进程接管后续工作,负责Pod生命周期中的“下半生”。

具体来说,Kubernetes Scheduler的作用是将待调度的Pod(API新创建的Pod、Controller Manager为补足副本而创建的Pod等)按照特定的调度算法和调度策略绑定(Binding)到集群中某个合适的Node上,并将绑定信息写入etcd中。在整个调度过程中涉及三个对象,分别是待调度Pod列表、可用Node列表及调度算法和策略。简单地说,就是通过调度算法为待调度Pod列表中的每个Pod都从Node列表中选择一个最适合的Node。

随后,目标节点上的kubelet通过API Server监听到Kubernetes Scheduler产生的Pod绑定事件,然后获取对应的Pod清单,下载Image镜像并启动容器。完整的流程如图5.12所示。

图5.12 Scheduler流程图

Scheduler只跟API Server打交道,其输入和输入如下。

◎ 输入:待调度的Pod和全部计算节点的信息。

◎ 输出:目标Pod要“安家”的最优节点(或者暂时不存在)。

Scheduler在调度算法方面的升级主要如下。

◎ v1.2版本引入了Scheduler Extender,支持外部扩展。

◎ v1.5版本为调度器的优先级算法引入了Map/Reduce的计算模式。

◎ v1.15版本实现了基于Scheduling Framework的方式,开始支持组件化开发。

◎ v1.18版本将所有策略(Predicates与Priorities)全部组件化,将默认的调度流程切换为Scheduling Framework。

◎ v1.19版本将抢占过程组件化,同时支持Multi Scheduling Profile。

考虑到新的Scheduling Framework的代码和功能大部分来自之前旧的两阶段调度流程,所以这里有必要先介绍一下旧版本的两阶段调度流程。旧版本的Kubernetes Scheduler的调度总体上包括两个阶段:过滤(Filtering)+打分(Scoring),随后就是绑定目标节点,完成调度。

(1)过滤阶段:遍历所有目标Node,筛选出符合要求的候选节点。在此阶段,Scheduler会将不合适的所有Node节点全部过滤,只留下符合条件的候选节点。其具体方式是通过一系列特定的Filter对每个Node都进行筛选,筛选完成后通常会有多个候选节点供调度,从而进入打分阶段;如果结果集为空,则表示当前还没有符合条件的Node节点,Pod会维持在Pending状态。

(2)打分阶段:在过滤阶段的基础上,采用优选策略(xxx Priorities)计算出每个候选节点的积分,积分最高者胜出,因为积分最高者表示最佳人选。挑选出最佳节点后,Scheduler会把目标Pod安置到此节点上,调度完成。

在过滤阶段中提到的Predicates是一系列过滤器,每种过滤器都实现一种节点特征的检测,比如磁盘(NoDiskConflict)、主机(PodFitsHost)、节点上的可用端口(PodFitsPorts)、节点标签(CheckNodeLabelPresence)、CPU和内存资源(PodFitsResources)、服务亲和性(CheckServiceAffinity)等。在打分阶段提到的Priorities则用来对满足条件的Node节点进行打分,常见的Priorities包含LeastRequestedPriority(选出资源消耗最小的节点)、BalancedResourceAllocation(选出各项资源使用率最均衡的节点)及CalculateNodeLabelPriority(优先选择含有指定Label的节点)等。Predicates与Priorities合在一起被称为Kubernetes Scheduling Policies,需要特别注意。

5.3.2 Scheduler Framework

考虑到旧版本的Kubernetes Scheduler不足以支持更复杂和灵活的调度场景,因此在Kubernetes 1.5版本中出现一个新的调度机制——Scheduler Framework。从整个调度流程来看,新的Scheduler Framework是在旧流程的基础上增加了一些扩展点(基于调度Stage的扩展点),同时支持用户以插件的方式(Plugin)进行扩展。新的调度流程如图5.13所示。

图5.13 新的调度流程

下面是对新流程中这些扩展点的说明。

◎ QueueSort:对调度队列中待调度的Pod进行排序,一次只能启用一个队列排序插件。

◎ PreFilter:在过滤之前预处理或检查Pod或集群的信息,可以将Pod标记为不可调度。

◎ Filter:相当于调度策略中的Predicates,用于过滤不能运行Pod的节点。过滤器的调用顺序是可配置的,如果没有一个节点通过所有过滤器的筛选,Pod则将被标记为不可调度。

◎ PreScore:是一个信息扩展点,可用于预打分工作。

◎ Score:给完成过滤阶段的节点打分,调度器会选择得分最高的节点。

◎ Reserve:是一个信息扩展点,当资源已被预留给Pod时,会通知插件。这些插件还实现了Unreserve接口,在Reserve期间或之后出现故障时调用。

◎ Permit:可以阻止或延迟Pod绑定。

◎ PreBind:在Pod绑定节点之前执行。

◎ Bind:将Pod与节点绑定。绑定插件是按顺序调用的,只要有一个插件完成了绑定,其余插件就都会跳过。绑定插件至少需要一个。

◎ PostBind:是一个信息扩展点,在Pod绑定节点之后调用。

目前常用的插件如下。

◎ PrioritySort:提供默认的基于优先级的排序。实现的扩展点为QueueSort。

◎ ImageLocality:选择已经存在Pod运行所需容器镜像的节点。实现的扩展点为Score。

◎ TaintToleration:实现污点和容忍。实现的扩展点为Filter、Prescore、Score。

◎ NodeName:检查Pod指定的节点名称与当前节点是否匹配。实现的扩展点为Filter。

◎ NodePorts:检查Pod请求的端口在节点上是否可用。实现的扩展点为PreFilter、Filter。

◎ NodeAffinity:实现节点选择器和节点亲和性。实现的扩展点为Filter、Score。

◎ SelectorSpread:对于属于Services、ReplicaSets和StatefulSets的Pod,偏好跨多个节点部署。实现的扩展点为PreScore、Score。

◎ PodTopologySpread:实现Pod拓扑分布。实现的扩展点为PreFilter、Filter、PreScore、Score。

◎ NodeResourcesFit:检查节点是否拥有Pod请求的所有资源。实现的扩展点为PreFilter、Filter。

◎ DefaultPreemption:提供默认的抢占机制。实现的扩展点为PostFilter。

◎ NodeResourcesBalancedAllocation:在调度Pod时选择资源使用更为均衡的节点。实现的扩展点为Score。

◎ NodeResourcesLeastAllocated:选择资源分配较少的节点,实现的扩展点为Score。

◎ VolumeBinding:检查节点是否有请求的卷,或是否可以绑定请求的卷。实现的扩展点为PreFilter、Filter、Reserve、PreBind。

◎ InterPodAffinity:实现Pod间的亲和性与反亲和性。实现的扩展点为PreFilter、Filter、PreScore、Score。

◎ DefaultBinder:提供默认的绑定机制。实现的扩展点为Bind。

显而易见,这种扩展方式远远超过之前Scheduling Policies的能力,随后在Kubernetes 1.18版本中引入了全新的Scheduler配置特性——Scheduling Profiles,并在该版本中默认生效,随之而来的旧版本调度机制中的Scheduling Policies则被逐步淘汰。

为了使用Scheduling Profiles对Scheduler进行自定义配置,我们可以编写一个Profiles配置文件,并通过--config参数传递到kube-scheduler服务中。下面是一个具体的例子:

从该例子中可以看到,对在调度的什么阶段开启或关闭哪些插件,我们都可以灵活定义,插件本身也更聚焦于自己所关注的特定阶段,因此更容易实现自定义插件。

5.3.3 多调度器特性

Kubernetes自带一个默认调度器,从1.2版本开始引入自定义调度器的特性,支持使用用户实现的自定义调度器,多个自定义调度器可以与默认的调度器同时运行,由Pod选择是用默认的调度器调度还是用某个自定义调度器调度。支持多调度器的特性到1.6版本时达到Beta阶段,但该特性的实现方式不够令人满意,因为用户需要自己编译、打包一个完整的Scheduler进程,以二进制或者容器的方式启动和运行,这个过程烦琐并且实施起来相对困难。除此之外,多个调度器进程同时运行,还存在资源竞争的风险和隐患。所以Kubernetes一直在考虑另一种解决思路,即通过一个Scheduler进程加上多个配置文件的方式来实现全新的多调度器特性。而新设计的Scheduling Profiles满足了这一需求,这就是Multiple Scheduling Profiles特性,我们只要针对不同的调度规则编写不同的Profile配置文件,并给它们起一个自定义Scheduler的名称,然后把这个配置文件传递给Kubernetes Scheduler加载、生效,Kubernetes Scheduler就立即实现了多调度器支持的“多重影分身”特效。再回头看看5.3.2节的Scheduling Profiles配置文件,就能立刻明白了:

在以上KubeSchedulerConfiguration配置声明中,我们看到系统默认的Scheduler名为default-scheduler,默认的Scheduler包括之前提到的常见的插件扩展,在这个配置文件中新增了一个名为no-scoring-scheduler的自定义Scheduler,我们在自定义Scheduler中可以根据自己的需求开启或关闭指定的插件。