怎么进行Kubernetes集群调度器原理剖析及思考

13次阅读
没有评论

这篇文章将为大家详细讲解有关怎么进行 Kubernetes 集群调度器原理剖析及思考,文章内容质量较高,因此丸趣 TV 小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

云环境或者计算仓库级别(将整个数据中心当做单个计算池)的集群管理系统通常会定义出工作负载的规范,并使用调度器将工作负载放置到集群恰当的位置。好的调度器可以让集群的工作处理更高效,同时提高资源利用率,节省能源开销。

通用调度器,如 Kubernetes 原生调度器 Scheduler 实现了根据特定的调度算法和策略将 pod 调度到指定的计算节点(Node)上。但实际上设计大规模共享集群的调度器并不是一件容易的事情。调度器不仅要了解集群资源的使用和分布情况,还要兼顾任务分配速度和执行效率。过度设计的调度器屏蔽了太多的技术实现,以至于无法按照预期完成调度任务,或导致异常情况的发生,不恰当的调度器的选择同样会降低工作效率,或导致调度任务无法完成。

本文主要从设计原理、代码实现两个层面介绍 Kubernetes 的调度器以及社区对其的补充加强,同时对业界常用调度器的设计实现进行比较分析。通过本文,读者可了解调度器的来龙去脉,从而为选择甚至设计实现适合实际场景的调度器打下基础。

注明:本文中代码基于 v1.11 版本 Kubernetes 进行分析,如有不当之处,欢迎指正!

调度器的基本知识

1.1 调度器的定义

通用调度的定义是指基于某种方法将某项任务分配到特定资源以完成相关工作,其中任务可以是虚拟计算元素,如线程、进程或数据流,特定资源一般是指处理器、网络、磁盘等,调度器则是完成这些调度行为的具体实现。使用调度器的目的是实现用户共享系统资源的同时,降低等待时间,提高吞吐率以及资源利用率。

本文中我们讨论的调度器是指大规模集群下调度任务的实现,比较典型的有 Mesos/Yarn(Apache)、Borg/Omega(Google)、Quincy(Microsoft) 等。构建大规模集群(如数据中心规模)的成本非常之高,因此精心设计调度器就显得尤为重要。

常见类型的调度器的对比分析如下表 1 所示:

怎么进行 Kubernetes 集群调度器原理剖析及思考

1.2 调度器的考量标准

我们首先思考一下调度器是根据哪些信息来进行调度工作的,以及哪些指标可以用来衡量调度工作质量。

调度器的主要工作是将资源需求与资源提供方做全局最优的匹配。所以一方面调度器的设计需要了解不同类型的资源拓扑,另一方面还需要对工作负载有充分的认识。

了解不同类型的资源拓扑,充分掌握环境拓扑信息能够使调度工作更充分的利用资源(如经常访问数据的任务如果距数据近可以显著减少执行时间),并且可以基于资源拓扑信息定义更加复杂的策略。但全局资源信息的维护消耗会限制集群的整体规模和调度执行时间,这也让调度器难以扩展,从而限制集群规模。

另一方面,由于不同类型的工作负载会有不同的甚至截然相反的特性,调度器还需要对工作负载有充分的认识,例如服务类任务,资源需求少,运行时间长,对调度时间并不敏感;而批处理类任务,资源需求大,运行时间短,任务可能相关,对调度时间要求较高。同时,调度器也要满足使用方的特殊要求。如任务尽量集中或者分散,保证多个任务同时进行等。

总的来说,好的调度器需要平衡好单次调度(调度时间,质量),同时要考虑到环境变化对调度结果的影响,保持结果最优(必要时重新调度),保证集群规模,同时还要能够支持用户无感知的升级和扩展。调度的结果需要满足但不限于下列条件,并最大可能满足尽可能优先级较高的条件:

资源使用率最大化

满足用户指定的调度需求

满足自定义优先级要求

调度效率高,能够根据资源情况快速做出决策

能够根据负载的变化调整调度策略

充分考虑各种层级的公平性

1.3 锁对调度器设计的影响

对于资源的调度,一定会涉及到锁的应用,不同类型锁的选择将直接决定调度器的使用场景。类似 Mesos 等两层调度器,一般采用悲观锁的设计实现方式,当资源全部满足任务需要时启动任务,否则将增量继续申请更多的资源直到调度条件满足;而共享状态的调度器,会考虑使用乐观锁的实现方式,Kubernetes 默认调度器是基于乐观锁进行设计的。

我们首先通过一个简单的例子,比较下悲观锁和乐观锁处理逻辑的不同,假设有如下的一个场景:

作业 A 读取对象 O

作业 B 读取对象 O

作业 A 在内存中更新对象 O

作业 B 在内存中更新对象 O

作业 A 写入对象 O 实现持久化

作业 B 写入对象 O 实现持久化

悲观锁的设计是对对象 O 实现独占锁,直到作业 A 完成对对象 O 的更新并写入持久化数据之前,阻断其他读取请求。乐观锁的设计是对对象 O 实现共享锁,假设所有的工作都能够正常完成,直到有冲突产生,记录冲突的发生并拒绝冲突的请求。

乐观锁一般会结合资源版本实现,同样是上述中的例子,当前对象 O 的版本为 v1,作业 A 首先完成对对象 O 的写入持久化操作,并标记对象 O 的版本为 v2,作业 B 在更新时发现对象版本已经变化,则会取消更改。

Kubernetes 调度器剖析

Kubernetes 中的计算任务大多通过 pod 来承载运行。pod 是用户定义的一个或多个共享存储、网络和命名空间资源的容器的组合,是调度器可调度的最小单元。Kubernetes 的调度器是控制平面的一部分,它主要监听 APIServer 提供的 pod 任务列表,获取待调度 pod,根据预选和优选策略,为这些 pod 分配运行的节点。概括来说,调度器主要依据资源消耗的描述得到一个调度结果。

2.1 Kubernetes 调度器的设计

Kubernetes 的调度设计参考了 Omega 的实现,主要采用两层调度架构,基于全局状态进行调度,通过乐观锁控制资源归属,同时支持多调度器的设计。

两层架构帮助调度器屏蔽了很多底层实现细节,将策略和限制分别实现,同时过滤可用资源,让调度器能够更灵活适应资源变化,满足用户个性化的调度需求。相比单体架构而言,不仅更容易添加自定义规则、支持集群动态伸缩,同时对大规模集群有更好的支持(支持多调度器)。

相比于使用悲观锁和部分环境视图的架构(如 Mesos),基于全局状态和乐观锁实现的好处是调度器可以看到集群所有可以支配的资源,然后抢占低优先级任务的资源,以达到策略要求的状态。它的资源分配更符合策略要求,避免了作业囤积资源导致集群死锁的问题。当然这会有抢占任务的开销以及冲突导致的重试,但总体来看资源的使用率更高了。

Kubernetes 中默认只有一个调度器,而 Omega 的设计本身支持资源分配管理器共享资源环境信息给多个调度器。所以从设计上来说,Kubernetes 可以支持多个调度器。

2.2 Kubernetes 调度器的实现

Kubernetes 调度器的工作流程如下图所示。调度器的工作本质是通过监听 pod 的创建、更新、删除等事件,循环遍历地完成每个 pod 的调度流程。如调度过程顺利,则基于预选和优选策略,完成 pod 和主机节点的绑定,最终通知 kubelet 完成 pod 启动的过程。如遇到错误的调度过程,通过优先级抢占的方式,获取优先调度的能力,进而重新进入调度循环的过程,等待成功调度。

2.2.1 调度循环的完整逻辑

Kubernetes 调度器完成调度的整体流程如下图 1 所示。下面就每个步骤的实现逻辑进行说明。

怎么进行 Kubernetes 集群调度器原理剖析及思考(1)基于事件驱动启动循环过程

Kubernetes 调度器维护 sharedIndexInformer,来完成 informer 对象的初始化工作。也就是调度器会监听 pod 创建、更新、删除的操作事件,主动更新事件缓存,并持久化到内存队列,发起调度循环。

该过程的函数入口在

https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/factory/factory.go#L631

怎么进行 Kubernetes 集群调度器原理剖析及思考(2)将没有调度的 pod 加到调度器缓存并更新调度器队列

Informer 对象负责监听 pod 的事件,主要的事件类型有:针对已调度 pod 的 addPodToCache、updatePodInCache、deletePodFromCache 和针对未被调度 pod 的 addPodToSchedulingQueue、updatePodInSchedulingQueue、deletePodFromSchedulingQueue 六种事件。该过程的函数入口在:

https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/eventhandlers.go

各类事件的含义如下表 2 所示:

怎么进行 Kubernetes 集群调度器原理剖析及思考

(3)对调度器队列中的每个 pod 执行调度

这里需要指出的是,在单个 pod 调度的过程中,对于主机节点的调度算法是顺序执行的。也就是说,pod 在调度的过程中会严格的顺序执行 Kubernetes 内置的策略和优先级,然后选择最合适的节点。

单个 pod 的调度过程分为预选和优选两个阶段。预选阶段调度器根据一组规则过滤掉不符合要求的主机,选择出合适的节点;优选阶段通过节点优先级打分的方式(依据整体优化策略等),选择出分值最高的节点进行调度。

单个 pod 的调度过程由以下函数作为入口:
https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/scheduler.go#L457
怎么进行 Kubernetes 集群调度器原理剖析及思考

当然,调度的过程可能由于没有满足 pod 运行条件的节点而调度失败,此时当 pod 有优先级指定的时候,将触发竞争机制。具有高优先级的 pod 将尝试抢占低优先级的 pod 资源。相关部分代码实现如下:

怎么进行 Kubernetes 集群调度器原理剖析及思考 如果资源抢占成功,将在下一次调度循环时标记可调度过程。如果抢占失败,调度程序退出。调度结果不保存意味着 pod 仍然会出现在未分配列表中。

(4)接下来检查用户提供的插件的条件是否满足

Reserve 插件是 Kubernets 留给用户进行扩展的接口,基于 reserver 插件用户在这个阶段可以设定自定义条件,从而满足期望的调度过程。插件的入口函数在:
https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/plugins/registrar.go

可以在 https://github.com/kubernetes/kubernetes/tree/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/plugins/examples 查看插件扩展 reserver 接口进行自定义调度的示例。

(5)找到满足的节点后,更新 Pod 对象的标签,保存被调度节点的结果

该过程的函数入口在 https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/scheduler.go#L517。
怎么进行 Kubernetes 集群调度器原理剖析及思考

(6)完成 pod 到节点的绑定

pod 到节点的绑定需要首先完成存储卷的挂载,最后通过 pod 对象的更新,完成最后的绑定。具体代码的逻辑可以参考:
https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/scheduler.go#L524

(7)调度完成后,主协程返回,执行下一个调度

至此调度的完整流程就完成了,下面重点介绍下,在单个 pod 调度过程中 Kubernetes 主要是如何对节点进行选择的,主要包括预选和优选两种策略。

2.2.2 单个 pod 的调度流程

单个 pod 的调度过程如下图 2 所示。主要包括由 pre-filter、filter、post-filter 的预选过程和 scoring 的优选过程。

怎么进行 Kubernetes 集群调度器原理剖析及思考 图 2:单个 Pod 的调度过程

(1)pod 进入调度阶段,首先进入预选环节

通过规则过滤找到满足 pod 调度条件的节点。
怎么进行 Kubernetes 集群调度器原理剖析及思考

k8s 内置了许多过滤规则,调度器会按照事先定义好的顺序进行过滤。内置的过滤规则主要包括检查节点是否有足够资源(例如 CPU、内存与 GPU 等)满足 pod 的运行需求,检查 pod 容器所需的 HostPort 是否已被节点上其它容器或服务占用,检查节点标签(label)是否匹配 pod 的 nodeSelector 属性要求,根据 taints 和 toleration 的关系判断 pod 是否可以调度到节点上 pod 是否满足节点容忍的一些条件,还有检查是否满足 csi 最大可挂载卷限制等。
怎么进行 Kubernetes 集群调度器原理剖析及思考

(2) 经过预选策略对节点过滤后,进入优选阶段

调度器根据预置的默认规则进行打分(优先级函数得分 * 权重的和),然后选择分数最高的节点实现 pod 到节点的绑定。

怎么进行 Kubernetes 集群调度器原理剖析及思考Kubernetes 内置的优先级函数如下,主要包括平均分布优先级(SelectorSpreadPriority)、最少访问优先级(LeastRequestedPriority)、平衡资源分布优先级(BalancedResourceAllocation)等。

怎么进行 Kubernetes 集群调度器原理剖析及思考SelectorSpreadPriority:为了更好的高可用,对同属于一个 service、replication controller 或者 replica 的多个 Pod 副本,尽量调度到多个不同的节点上。

InterPodAffinityPriority:通过迭代 weightedPodAffinityTerm 的元素计算和,如果对该节点满足相应的 PodAffinityTerm,则将“weight”加到和中,具有最高和的节点是最优选的。

LeastRequestedPriority:由节点空闲资源与节点总容量的比值,即由(总容量 - 节点上 Pod 的容量总和 - 新 Pod 的容量)/ 总容量)来决定节点的优先级。CPU 和 memory 具有相同权重,比值越大的节点得分越高。

BalancedResourceAllocation:CPU 和内存使用率越接近的节点优先级越高,该策略不能单独使用,必须和 LeastRequestedPriority 同时使用,也就是说尽量选择在部署 Pod 后各项资源更均衡的机器。

NodePreferAvoidPodsPriority(权重 1w):如果节点的 Anotation 没有设置 key-value:scheduler. alpha.kubernetes.io/ preferAvoidPods =“…”,则该 节点对该 policy 的得分就是 10 分,加上权重 10000,那么该节点对该 policy 的得分至少 10W 分。如果节点的 Anotation 设置了 scheduler.alpha.kubernetes.io/preferAvoidPods =“…”,如果该 pod 对应的 Controller 是 ReplicationController 或 ReplicaSet,则该节点对该 policy 的得分就是 0 分。

NodeAffinityPriority:实现 Kubernetes 调度中的亲和性机制。

TaintTolerationPriority : 使用 Pod 中 tolerationList 与 节点 Taint 进行匹配,配对成功的项越多,则得分越低。

Kubernetes 调度器的不足和解决思路

3.1 典型的几个问题和解决思路

(1)调度器只根据当前资源环境情况进行一次调度,一旦完成调度就没有机制实现调整

虽然 pod 只有在自己退出、用户删除以及集群资源不足等情况下才会有变化。但资源拓扑的变化是随时都有可能发生的,如批处理任务会结束,节点会新增或崩溃。这些情况导致调度的结果可能在调度时是最优的,但在拓扑变化后调度质量由于以上情况的发生而下降。

经过社区讨论,认为需要重新找出不满足调度策略的 pod,删除并创建替代者来重新调度,据此设计启动了项目 descheduler。

(2)调度以单个 pod 进行的,因而调度互相关联的工作负载会难以实现

如大数据分析、机器学习等计算多依赖于批处理任务,这类工作负载相关性大,互相之间有依赖关系。为了解决这个问题,社区经过讨论,提出了 coscheduling 一次调度一组 pod 的项目,以此来优化这类调度任务的执行。

(3)目前调度器的实现只关心是否能将 pod 与节点绑定,资源使用情况的数据未被充分利用

目前,集群的使用量只能通过监控数据间接推导。如果 k8s 集群剩余资源不足时,并没有直观数据可以用来触发扩容或者告警。

根据上述情况,社区启动了 cluster-capacity framework 项目,提供集群的容量数据,方便集群的维护程序或者管理员基于这些数据做集群扩容等。也有项目抓取监控数据自己计算集群的整体负载情况给调度算法参考,如 poseidon。

3.2 Kubernetes 调度器的定制扩展

如上节所述,通用调度器在某些场景下并不能满足用户个性化需求,实际环境下运行的集群的调度器,往往需要根据实际的需求做定制与二次开发。

kubernetes 的调度器以插件化的形式实现的,方便用户对调度的定制与二次开发。定制调度器有如下几种方式的选择:

更改 Kubernetes 内置策略,通过更改默认的策略文件或者重新编译调度器来实现。

扩展调度器在 pre-filter、filter、post-filter、reserve、prebind、bind 和 post-bind 各个阶段的接口,更改调度器过滤、打分、抢占、预留的具体实现逻辑。

更改调度器调度算法,从头实现调度器逻辑。
怎么进行 Kubernetes 集群调度器原理剖析及思考

企业场景应用的案例

4.1 通用计算场景

Kubernetes default-scheduler 满足通用计算的需求,主要服务于以快速开发测试为目标的持续集成和持续部署平台(DevOps 平台)、以标准三层架构应用为特点的容器应用运行与运维平台(容器平台)、PaaS 平台和云原生应用的核心基础架构平台(aPaaS 平台)几种场景。

通常情况下,标准 Kubernetes 调度器能够满足大多数通过计算场景的诉求,主要解决应用上云过程中不同异构云资源之间的调度问题,应用上云后弹性伸缩、故障自愈等的动态调度响应,标准中间件服务和数据库服务基于日常运维规范的调度问题以及云原生应用在服务治理、配置管理、状态反馈、事件链路跟踪上的综合调度过程。

4.2 批处理场景

大数据分析和机器学习类任务执行时需要大量资源,多个任务同时进行时,资源很快会用尽,部分任务会需要等待资源释放。这类型任务的步骤往往互相关联,单独运行步骤可能会影响最终结果。使用默认的调度器在集群资源紧张时,甚至会出现占用资源的 pod 都在等待依赖的 pod 运行完毕,而集群没有空闲资源去运行依赖任务,导致死锁。所以在调度这类任务时,支持群组调度(在调度作业所需的资源都收集完成后才进行调度),减少了 pod 数量,因而降低调度器的负载,同时避免了很多资源紧张带来的问题。

与默认调度器一次调度一个 pod 不同,kube-batch 定义了 PodGroup 定义一组相关的 pod 资源,并实现了一个全新的调度器。调度器的流程基本与默认调度器相同。Podgroup 保证一组 pod 可以同时被调度。是 Kubernetes 社区在大数据分析场景中的一种实现。

4.3 特定领域业务场景

特定的业务场景需要调度器能够快速生成调度的策略,并尽可能避免调度超时。Poseidon 是大规模集群中基于图应用数据局部性减少任务执行时间同时混合多种调度算法提升调度速度的一种调度器。

Poseidon 是基于 Firmament 算法的调度器,它通过接收 heapster 数据来构建资源使用信息。调用 Firmament 实现进行调度。Firmament 算法受 Quincy[11] 启发,构建一个从任务到节点的图,但作者为减少调度时间,将两种计算最短路径的算法合并,将全量环境信息同步改为增量同步。让 Firmament 处理短时间批量任务时快于 Quincy,在资源短缺时没有 Kubernetes 默认调度器超时的问题。

主要从设计原理、代码实现等层面介绍 Kubernetes 的调度器以及社区对其的补充加强,总结了 Kubernetes 调度器的设计原理以及在何种场景如何增强 Kubernetes 来满足业务需求,提供技术选型的依据和评价标准。

关于怎么进行 Kubernetes 集群调度器原理剖析及思考就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。