介绍
在 Kubernetes 项目中,默认调度器kube-scheduler
的主要职责,就是为一个新创建出来的 Pod,寻找一个最合适的节点(Node)。
而这里“最合适”的含义,包括两层:
- 从集群所有的节点中,根据调度算法挑选出所有可以运行该 Pod 的节点;
- 从第一步的结果中,再根据调度算法挑选一个最符合条件的节点作为最终结果。
调度器对一个 Pod 调度成功,实际上就是将它的spec.nodeName
字段填上调度结果的节点名字。
这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:
- 如何保证全部节点调度的公平性?并不是所有节点资源配置是一样的
- 如何保证每个节点都能被分配资源?
- 集群资源如何才能被高效?被最大化使用?
- 如何保证Pod调度的性能和效率?
- 用户是否可以根据自己的实际需求定制自己调度策略?
- ......
考虑到实际环境中各种复杂情况,kubernetes的调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发。
kubernetes 调度器的源码位于 kubernetes/pkg/scheduler
中,其中 Scheduler 创建和运行的核心程序,对应的代码在 pkg/scheduler/scheduler.go
,如果要查看 kube-scheduler
的入口程序,对应的代码在 cmd/kube-scheduler/scheduler.go
。
调度主要分为以下几个部分:
- 首先是预选过程,过滤掉不满足条件的节点,这个过程称为
Predicates
(过滤) - 然后是优选过程,对通过的节点按照优先级排序,称之为
Priorities
(打分) - 最后从中选择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误
Predicates
阶段首先会遍历全部节点,过滤掉不满足条件的节点,属于强制性
规则,这一阶段输出的所有满足要求的节点将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么Pod将会一直处于Pending状态,直到节点满足条件,在这期间调度器会不断重试。
Priorities
阶段即在此对节点进行筛选,如果有多个节点都满足条件的话,那么系统会按照节点的优先级(priorites)大小对节点进行排序,最后选择优先级最高的节点来部署Pod应用。
1.首先客户端通过API Server 的REST API或者kubectl 工具创建Pod资源;
2.API Server收到用户请求后,存储相关数据到etcd中;
3.调度器监听到API Server查看到还未被调度(bind)的Pod列表,循环遍历地为每个Pod尝试分配节点,这个分配过程将就是上面提到的两个阶段:
- 预选阶段(Predicates),过滤节点,调度器用一组规则过滤掉不符合要求的Node节点,比如Pod设置了资源的request,那么可用资源比Pod需要的资源少的主机显然会被过滤掉
- 优选阶段,为节点的优先级打分,将上一阶段过滤出来的Node列表进行打分,调度器会考虑一些整体的优化策略,比如把Deployment控制的多个Pod副本尽量分布到不同的主机上,使用最低负载的主机等策略
4.经过上面的阶段过滤后,选择打分最高的Node节点和Pod进行binding
操作,然后将结果存储到etcd中,最后被选择出来的Node节点对应的kubelet去执行创建Pod的相关操作。
调度框架
调度框架定义了一组扩展点,用户可以实现扩展点定义的接口来定义自己的调度逻辑(我们称之为扩展),并将扩展注册到扩展点上,调度框架在执行调度工作流时,遇到对应的扩展点时,将调用用户注册的扩展。调度框架在预留扩展点时,都是有特定的目的,有些扩展点上的扩展可以改变调度程序的决策方法,有些扩展点上的扩展只是发送一个通知。
我们知道每当调度一个 Pod 时,都会按照两个过程来执行:调度过程和绑定过程。
调度过程为 Pod 选择一个合适的节点,绑定过程则将调度过程的决策应用到集群中(也就是在被选定的节点上运行 Pod),将调度过程和绑定过程合在一起,称之为调度上下文(scheduling context)。需要注意的是调度过程是同步
运行的(同一时间点只为一个 Pod 进行调度),绑定过程可异步运行(同一时间点可并发为多个 Pod 执行绑定)。
调度过程和绑定过程遇到如下情况时会中途退出:
- 调度程序认为当前没有该 Pod 的可选节点
- 内部错误
这个时候,该 Pod 将被放回到 待调度队列,并等待下次重试。
扩展点(Extension Points)
下图展示了调度框架中的调度上下文及其中的扩展点,一个扩展可以注册多个扩展点,以便可以执行更复杂的有状态的任务。
1.Sort
扩展用于对Pod的待调度队列进行排序,以决定先调度哪个Pod,Sort
扩展本质上只需要实现一个方法Less(Pod1,Pod2)
,用于比较两个Pod谁更优先获得调度,同一时间点只能有一个Sort
插件生效;
2.Pre-filter
扩展用于对Pod的信息进行预处理,或者检查一些集群或Pod必须满足的条件,然后将其存入缓存中待Filter
扩展点执行的时候使用,如果pre-filter
返回了error,则调度过程终止;
3.Filter
扩展用于排除那些不能运行该Pod的节点,对于每一个节点,调度器将按照顺序执行filter
扩展;如果任何一个filter
将节点标记为不可选,则余下的filter
扩展将不会被执行,调度器可以同时对多个节点进行filter
扩展;
4.Post-filter
如果在Filter
扩展点全部节点都被过滤掉了,没有合适的节点进行调度,才会执行PostFilter
扩展点,如果启用了Pod抢占特性,那么会在这个扩展点进行抢占操作,可以用于logs/metrics;
5.PreScore
扩展会对Score
扩展点的数据做一些预处理的操作,然后将其存入缓存中待Score
扩展点执行的时候使用;
6.Score
扩展用于为所有可选节点进行打分,调度器将针对每一个节点调用每个Sore
扩展,评分结果是一个范围内的整数,代表最小和最大分数。在Normalize score
阶段,调度器将会把每个score
扩展对具体某个节点的评分结果和该扩展的权重合并,最为最终评分结果;
7.Normalize score
扩展在调度器对节点进行最终排序之前修改每个节点的评分结果,注册到该扩展点的扩展在被调用时,将获得同一个插件中的score
扩展的评分结果作为参数,调度框架每执行一次调度,都将调用所有插件中的normalize score
扩展一次。
8.Reserve
是一个通知性质的扩展点,有状态的插件可以使用该扩展点来获得节点上为 Pod 预留的资源,该事件发生在调度器将 Pod 绑定到节点之前,目的是避免调度器在等待 Pod 与节点绑定的过程中调度新的 Pod 到节点上时,发生实际使用资源超出可用资源的情况(因为绑定 Pod 到节点上是异步发生的)。这是调度过程的最后一个步骤,Pod 进入 reserved
状态以后,要么在绑定失败时触发 Unreserve
扩展,要么在绑定成功时,由 Post-bind
扩展结束绑定过程;
9.Permit
扩展在每个 Pod 调度周期的最后调用,用于阻止或者延迟 Pod 与节点的绑定。Permit 扩展可以做下面三件事中的一项:
- approve(批准):当所有的 permit 扩展都 approve 了 Pod 与节点的绑定,调度器将继续执行绑定过程
- deny(拒绝):如果任何一个 permit 扩展 deny 了 Pod 与节点的绑定,Pod 将被放回到待调度队列,此时将触发
Unreserve
扩展 - wait(等待):如果一个 permit 扩展返回了 wait,则 Pod 将保持在 permit 阶段,直到被其他扩展 approve,如果超时事件发生,wait 状态变成 deny,Pod 将被放回到待调度队列,此时将触发
Unreserve
扩展
10.WaitOnPermit
扩展与Permit
扩展点配置使用实现延时调度功能(内部默认实现的);
11.Pre-bind
扩展用于在Pod绑定之前执行某些逻辑。例如,pre-bind
扩展可以将一个基于网络的数据卷挂载到节点上,以便 Pod 可以使用。如果任何一个 pre-bind
扩展返回错误,Pod 将被放回到待调度队列,此时将触发 Unreserve
扩展;
12.Bind
扩展用于将Pod绑定到节点上:
- 只有所有的
pre-bind
扩展都成功执行了,bind扩展才会执行 - 调度框架按照
bind
扩展注册的顺序逐个调用bind
扩展 - 具体某个
bind
扩展可以选择处理或者不处理该Pod - 如果某个
bind
扩展处理了该Pod与节点的绑定,余下的bind
扩展将被忽略
13.Post-bind
是一个通知性质的扩展:
Post-bind
扩展在Pod成功绑定到节点上之后被动调用Post-bind
扩展是绑定过程的最后一个步骤,可以用来执行资源清理的动作
14.Unreserve
是一个通知性质的扩展,如果为 Pod 预留了资源,Pod 又在被绑定过程中被拒绝绑定,则 unreserve
扩展将被调用。Unreserve
扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中,reserve
扩展和 unreserve
扩展应该成对出现。