Kubernetes Scheduler简介

转载请注明出处即可。
所使用源码k8s源码为release-1.18

以下内容所提到的概念尽量至少找到一处出处(书籍或维基百科)

一、调度与调度器

日出而作,日入而息----《庄子·让王

概念定义

首先我们先看下百度百科中对调度的定义。
调度,常用作动词,意为调动;安排人力、车辆。用作名词时,可以指担负指挥调派人力、工作、车辆等工作的人、调度员,也可以当人讲的一类称呼。

根据这个描述我们大概能知晓调度的具体作用了。但是在实际的生活中,调度涵盖的范围远远超过了人力和车辆的安排。比如,在码头装卸集装箱,通过什么样的方式才能让现场的工作人员和机器配合在符合安全红线的基准上达到装卸速度最快。又比如在技术领域,操作系统如何分配CPU时间,达到进程间尽可能公平共享CPU时间,还要考虑不同任务的优先级。
根据现实中的生产生活以及技术领域的相关调度概念的抽象。我们可以总结出调度的一个模型和概念。

调度模型

调度

调度: 某个被调度"对象",根据对象的状态或目标的状态,通过规则(动态规则或静态规则)选择某一个目标的过程。
调度器: 或者说调度员,如图中的橙色框,承担的就是负责调度的工作。

根据上面的两个示例来理解下这个模型。
在集装箱的这个例子中,对象指的就是集装箱,规则有安全规则。也有根据目前的状态,比如轮船的大小,天气状况,现成工作人员数量等等,计算出的某个岗位需要在什么时间,或者在什么条件下做什么样的事情。最后达成装卸的目标。 在这个例子中我们也可以看到,调度器实际并不需要自己完成或做什么具体的任务(策略与执行分离),依赖港口的机械以及工作人员可以达成相关的目标。
在进程调度中,进程是被调度的对象,规则是指硬编码到kernel/sched包下(或者早期的kernel/sched.c)下的代码,状态是指当前进程的状态比如进程的优先级等。目标在这里可以描述为要获取的资源,既cpu时间。

调度的目标

在完全回到技术领域之前,我们再看下为什么要有调度,以及调度的目标是什么。

首先,在集装箱和进程调度的例子中,如果没有调度,所有的对象安装不违反部分规则的情况下,也是可以运行一会的。但最终会从有序逐渐向无序扩散,直到无法继续进行。在这里可以想下红绿灯,如果某个十字路口的红绿灯坏了,随着车流量的增多,在没有干预的情况下会怎么样。

调度的目标中除了协调对象能达成某种目标外,还需要进行提升质量(安全)降低成本提高效率。提高效率又分为两个方面,一个是调度器自身的效率提升,提高决策效率,不应该由于调度器本身的效率低下,而影响整个事件的推进。另一个是调度规则的调整和优化,提升被协调对象达成目标的效率。
如果是一个技术leader的话,可能会发现调度器的工作和自身的工作有些类似。但调度器考量的因素要比和人打交道少的多^_^

二、技术领域的资源调度

上个小节描述了调度的基本模型,那么我们回到技术领域。我们在这里专注于资源调度。定时任务调度等,不在这里来描述。
资源调度的模型和上面调度模型是一样的。只是被调度对象和目标有所约束。被调度的对象只有任务(task or work),而目标只是资源。
资源调度

在技术领域中的资源调度,其实涵盖的范围很广,在操作系统(Linux调度器)、编程语言(goroutine调度)和一些应用(k8s scheduler)中都存在着调度器进行资源调度。除了这些在部分业务系统和基础架构中的系统也多多少少会有些资源调度影子在。打个比方,如果比较熟悉运营平台的读者,会发现这里和运营系统中常用的事件驱动架构比较相似。但是它们分别有自己的考量点,资源调度需要考虑合理的利用资源,甚至是在总体资源不足的情况保障头部应用的稳定运行。而运营系统则需要考虑最后的留存等。

资源调度

在技术中,更常见的资源调度其实是负载均衡,如果一个请求(或长连接)看做是一个任务,那么负载均衡器之后的节点就是请求可以访问的资源。那么负载均衡所采用的算法(或者干脆随机)就是调度的规则。负载均衡的算法有轮询,加权轮询,哈希,最小链接,加权最小链接,负载最轻,综合利用率等。其中的综合利用率就已经相对接近k8s的调度逻辑了(部分相似,由于考虑的问题不同还是有很大的差异)。

在云计算中的资源调度可能是最复杂的,如果完全按照申请的资源供给用户的话,是无法做到利益最大化的。但是也确实存在用户无法使用全部所申请的资源造成资源浪费。那么如果能预测到用户的资源使用情况(比如大部分个人用户),就可以在相对将其QoS和SLA的情况下,进行资源超配。但是为了降低某个虚机对整体的影响,可以绑定到具体的某个cpu上,降低整体风险(仓壁模式)。这里的调度器所使用的目标函数,除了解决用户需求的服务资源调度问题,需要根据用户的特定偏好程度选择服务资源,同时要满足用户对服务时间、成本、服务质量以及满意度的要求这些意外,还需要考虑SLA的违约率,整体的违约率需要控制在一定的范围内。至于某个用户被违约的概率,就好像保险一样,不出问题有概率,出了问题就只能100%的抗下来了。所以云计算厂商也会影响(说服)用户去进行多节点的部署,可能用户觉得这是基于可用性的考虑,当然在用户角度这个是绝对正确的,在系统重要到一定程度,多地多活,甚至国际化后的全球部署都是需要考虑的。但这也其实帮助云计算厂商降低了整体的SLA违约率。

三、资源调度中的问题

我们都知道kubernetes的本质是一个容器编排系统,那么对于这种编排系统的调度器。需要考量以下问题
(1) "充分"利用集群资源
充分是加了引号的,这里的充分并不是要把某个节点,或某群节点的资源完全榨干。而是要确保集群中大部分节点的资源(每个节点的资源不一定都是一样的)消耗维持在一个相对稳定的水平线,并且要在水位下以下。

(2) 服务分级
由于服务的重要性不同,一级服务如果宕机,那么每秒都在对企业的实际收入造成损失,并且还影响了公司的声誉。所以根据服务重要性的不同,隔离到不同的节点部署,并为一级服务预留更多的资源。当然这里只是描述了不同级别服务的分离,实际的情况下,还需要根据不同的事业群,业务线,以及流量大小等一层一层的进行隔离。不重要的小流量服务,其实可以进行一定的超配。

(3) 弹性扩展
这里不用多说就是要预留部分资源作为动态扩展。防止流量突增无法快速扩充资源。

(4) 混合部署
比如CPU密集型应用和IO密集型应用混合部署,重复利用节点资源。

(5) 独特资源分配
部分机器学习深度学习任务需要使用GPU来进行模型计算。所以在分配节点时,需要优先往带有GPU节点进行部署,当然如果在GPU资源不够用的情况下,也应该能处理无GPU的只能通过CPU来运算的情况。

(6) 调度器规则扩展
因为每个公司的实际情况不同,部分调度需求无法通过默认的规则来实现,那么支持扩展,或者支持多个调度器也是一个重要的功能。

(7) 调度任务的可靠性
调度任务必须要执行,不能在调度器决策之前就丢失。所以这里需要使用调度队列来暂存待调度的任务。

当然这里并没有把所有的问题都列完全,不同公司的不同环境,以及不同的调度需求,会遇到不同的问题。我们目前只需要了解有这些问题即可,在未来修改(或扩展)调度器逻辑时,可以在仔细研究分析。下面我们来看下k8s的调度实现。

四、Kubernetes的资源调度

(1) Kubernetes的架构

k8s集群的组件
在这里简单回顾下k8s的架构
API服务器: 其他控制平面的组件都会和它交互,提供了各类资源对象(Pod, Service等)CRUD的功能。
Scheculer: 是这篇文章的主角,主要用于为pod选择最合适的node。
Controller Manager: 执行集群级别的功能,如复制组件,持续追踪工作节点,处理节点失败等。
Etcd: 分布式存储,持久化存储集群的配置。API服务器是唯一和etcd通信的服务。
etcd存储条目
kubelet: 与API服务通信,管理所在节点的容器。
kube-proxy: 负责组件之间的负载均衡

pod, service, node等基础概念不在赘述。

(2) Kubernetes Scheduler简介

Scheduler的核心作用是将pod根据部分"规则"来部署到合适的node上。并且具体的部署Scheduler并没有实现,而是调用了API服务器(api server)。
调度流程

pkg/scheduler/scheduler.go
Scheduler分为两个过程 调度绑定,调度又分为两个核心过程过滤打分

调度过程的过滤是指,在众多node中,选择符合pod调度需求的node。如果没有可以调度的node,会返回空。调度器会一直等待之到有符合的node为止。而打分是指在符合条件的node列表中根据部分规则对node进行打分,然后选择分数最高的node。如果最高分数的node存在多个,会随机选择一个。早期版本具体的调度策略由过滤的 谓词(Predicates) 和打分的 优先级(Priorities) 来实现。并且Scheduler在k8s 1.15 alpha版本增加了framework提供了在调度绑定过程中的扩展点。

(3) Scheduler Frameworks简介

框架源于Scheduler Frameworks提案并且为自定义调度器提供了很多的扩展点。下图就是在调度过程和绑定过程中的扩展点。

scheduling-framework-extensions

我们可以看到在过滤,打分,以及绑定阶段都可以编写对应的插件并进行注册,然后"干预"默认调度策略(规则)。具体每个阶段的意义,请查看这篇官网文档scheduling-framework

(4) Scheduler源码简析

切入点可以从两个方向入手, 一是cmd/kube-scheduler/scheduler.go的main方法。在这里可以发现Scheduler其实是一个cobra的应用。另一个切入点是pkg/scheduler/scheduler.go的79行Scheduler struct

首先看下scheduler的创建 pkg/scheduler/scheduler.go 235行
scheduler.go

scheduler.go

types.go

scheduler.go
在New scheduler的方法中,首先先去构造了默认的选项,并提供了默认的算法提供者名称
DefaultProvider,然后调用了pkg/scheduler/factory.go 219行的createFromProvider方法,并通过algorithmprovider.NewRegistry()来注册默认插件。
factory.go

然后在pkg/scheduler/algorithmprovider/registry.go 53行这里,获取了默认的插件配置。
registry.go

具体在各个阶段注册的插件如下所示(注释里写了插件的作用),可以看到在打分阶段,除了插件本身的名称以外,还增加了权重,评分可以简单理解为combinedScores[host] += score * weight是分数乘以权重然后求和所得(最后还需要控制到一定的范围1-100)。

func getDefaultConfig() *schedulerapi.Plugins {
	return &schedulerapi.Plugins{
		QueueSort: &schedulerapi.PluginSet{
			Enabled: []schedulerapi.Plugin{
				/*
				`QueueSort`是指在`SchedulerQueue`中的pod排序,
				优先级越高的pod就有越优先来调度和绑定。
				默认实现的是`PrioritySort`插件。
				实现逻辑是通过pod.Spec.Priority数值来判断pod的优先级,如果数值相同则通过时间戳来判断
				*/

				{Name: queuesort.Name},
			},
		},
		PreFilter: &schedulerapi.PluginSet{
			Enabled: []schedulerapi.Plugin{
				// 判断node的资源是否充足
				{Name: noderesources.FitName},
				// 判断node的端口是否被占用
				{Name: nodeports.Name},
				// pod间亲和性过滤
				{Name: interpodaffinity.Name},
			},
		},
		Filter: &schedulerapi.PluginSet{
			Enabled: []schedulerapi.Plugin{
				// pod.Spec.Priority.Unschedulable 如果设置为true,则不会进行调度
				{Name: nodeunschedulable.Name},
				// 判断node的资源是否充足
				{Name: noderesources.FitName},
				// 指定调度到具体的node, 实际生产环境不会这么做
				{Name: nodename.Name},
				// 判断node的端口是否被占用
				{Name: nodeports.Name},
				// 判断node和pod的亲和性, 里面的matchNodeselector实现了对label匹配
				{Name: nodeaffinity.Name},
				// 检查pod的volume卷配置,可能node没有可用的磁盘
				{Name: volumerestrictions.Name},
				// Taint和toleration相互配合,可以用来避免 pod 被分配到不合适的节点上,和nodeaffinity正好相反
				{Name: tainttoleration.Name},
				// 检查存储限制都和具体的云服务厂商有关
				{Name: nodevolumelimits.EBSName},
				{Name: nodevolumelimits.GCEPDName},
				{Name: nodevolumelimits.CSIName},
				{Name: nodevolumelimits.AzureDiskName},
				{Name: volumebinding.Name},
				{Name: volumezone.Name},
				// pod间亲和性过滤
				{Name: interpodaffinity.Name},
			},
		},
		PreScore: &schedulerapi.PluginSet{
			Enabled: []schedulerapi.Plugin{
				{Name: interpodaffinity.Name},
				// pod的拓扑扩展约束 https://kubernetes.io/zh/docs/concepts/workloads/pods/pod-topology-spread-constraints/
				{Name: defaultpodtopologyspread.Name},
				{Name: tainttoleration.Name},
			},
		},
		Score: &schedulerapi.PluginSet{
			Enabled: []schedulerapi.Plugin{
				// CPU和内存配比合适的node得分更高
				{Name: noderesources.BalancedAllocationName, Weight: 1},
				// 已经下载过image的得分更改,这样可以加快pod的启动速度
				{Name: imagelocality.Name, Weight: 1},
				{Name: interpodaffinity.Name, Weight: 1},
				// 资源被占用越少的得分越高
				{Name: noderesources.LeastAllocatedName, Weight: 1},
				{Name: nodeaffinity.Name, Weight: 1},
				// 会根据注解scheduler.alpha.kubernetes.io/preferAvoidPods来判断优先级,
                // 但忽略了ReplicationController和ReplicaSet,会默认返回最大值100
				{Name: nodepreferavoidpods.Name, Weight: 10000},
				// pod的拓扑扩展约束, prescore那里提到过了
				{Name: defaultpodtopologyspread.Name, Weight: 1},
				{Name: tainttoleration.Name, Weight: 1},
			},
		},
		Bind: &schedulerapi.PluginSet{
			Enabled: []schedulerapi.Plugin{
				// 默认绑定逻辑
				{Name: defaultbinder.Name},
			},
		},
	}
}

其实这里并没有把所有的核心源码都全部说完。比如SchedulerQueue和SchedulerCache等,还有scheduler自身的选举逻辑。并且如果想看下调度绑定阶段(过程)的具体实现,可以从scheduleOne方法来入手阅读。
scheduler.go

五、结束语

本文从调度的概念入手,逐步过渡到k8s中的调度器实现简介。下篇文件打算写一个Scheduler extender,也有可能还写一篇Spring源码解析的。

参考

《云计算市场交易与资源调度机制》
《云计算:资源调度管理》
《Kubernetes in Action》
《深入Linux内核架构》
k8s-release-1.18
调度-维基百科
k8s调度框架
k8s调度器性能调优
kube-scheduler介绍
scheduler调度框架
Scheduler Frameworks提案

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值