P5-Pod优先级抢占调度
1. 前言
前面的两篇文章中,已经讲过了调度pod的算法(predicate/priority),在kubernetes v1.8版本之后可以指定pod优先级(v1alpha1),若资源不足导致高优先级pod匹配失败,高优先级pod会转而将部分低优先级pod驱逐,以抢占低优先级pod的资源尽力保障自身能够调度成功,那么本篇就从代码的层面展开看一看pod抢占调度的逻辑。
2. 抢占调度入口
在P1-入口篇中我们找到了调度算法计算的入口,随后展开了调度算法的两篇解读,本篇我们再次回到此入口的位置,接着往下看:
pkg/scheduler/scheduler.go:457
1 | func (sched *Scheduler) scheduleOne() { |
注释中可看出,若在筛选算法中并未找到fitNode且返回了fitError,那么就会进入基于pod优先级的资源抢占的逻辑,入口是sched.preempt(pod, fitError)
函数。在展开抢占逻辑之前,我们先来看一看pod优先级是怎么一回事吧。
2.1. Pod优先级的定义
字面意义上来理解,pod优先级可以在调度的时候为高优先级的pod提供资源空间保障,若出现资源紧张的情况,则在其他约束规则允许的情况下,高优先级pod会抢占低优先级pod的资源。此功能在1.11版本以后默认开启,默认情况下pod的优先级是0,优先级值high is better,具体说明来看看官方文档的解释吧:
下面列举一个pod优先级使用的实例:
1 | # Example PriorityClass |
了解了定义及如何使用,那我们来看看代码层面是如何实现的吧!
3. 抢占调度算法
从上面的入口跳转:
pkg/scheduler/scheduler.go:469
–> pkg/scheduler/scheduler.go:290
1 | func (sched *Scheduler) preempt(preemptor *v1.Pod, scheduleErr error) (string, error) { |
如优先级筛选算法一样,调度算法最终也是要挑选出一个供以实际运行抢占调度逻辑的node,那么一起来看看这个计算算法是怎么样的。如schedule()方法一样,preempt()的默认方法也在generic_scheduler.go
这个文件中:
pkg/scheduler/core/generic_scheduler.go:288
将函数内拆成几个重要的部分,其余部分省略,逐个说明
1 | func (g *genericScheduler) Preempt(pod *v1.Pod, nodeLister algorithm.NodeLister, scheduleErr error) (*v1.Node, []*v1.Pod, []*v1.Pod, error) { |
3.1. potentialNodes
第一步,先找出所有潜在的可能会参与抢占调度的node,何为潜在可能呢?意思是node调度此pod调度失败的原因并非”硬伤”类原因。所谓硬伤原因,指的是即使驱逐调几个pod,也无法改变此node无法运行这个pod的事实。这些硬伤包括哪些?来看看代码:
pkg/scheduler/core/generic_scheduler.go:306 -> pkg/scheduler/core/generic_scheduler.go:1082
1 | func nodesWherePreemptionMightHelp(nodes []*v1.Node, failedPredicatesMap FailedPredicateMap) []*v1.Node { |
3.2. Pod Disruption Budget(pdb)
这种资源类型本人没有实际应用过,查阅了一下官方的手册,实际上它也是kubernetes设计的一种抽象资源,主要用作面对主动中断时,保障副本可用数量的一种功能,与deployment的maxUnavailable不一样,maxUnavailable是在滚动更新(非主动中断)时用来保障,pdb通常是面对主动中断的场景,例如删除pod,drain node等主动操作,更多详细说明参考官方的手册:
Specifying a Disruption Budget for your Application
资源实例:
1 | apiVersion: policy/v1beta1 |
1 | $ kubectl get poddisruptionbudgets |
为什么这个资源相关的逻辑会出现在抢占调度里面呢?因为设计者将pod抢占造成的低优先级pod驱逐动作视为主动中断,有了这一层理解,我们接着往下。
3.3. nodeToVictims
selectNodesForPreemption()函数很重要,这个函数将会返回所有可行的node驱逐方案
pkg/scheduler/core/generic_scheduler.go:316
selectNodesForPreemption
–> pkg/scheduler/core/generic_scheduler.go:916
1 | func selectNodesForPreemption(pod *v1.Pod, |
上面已在代码中对重要部分进行注释,不难发现,重要的计算函数是selectVictimsOnNode()函数,每个node所需要驱逐的pod,以及违反PDB规则次数信息,都由此函数来计算返回,最终组成nodeToVictims这个map,返回给上层调用函数。所以,接着来看selectVictimsOnNode()函数是怎么运行的。
selectVictimsOnNode
1 | func selectVictimsOnNode( |
这个函数分5步,先是枚举出所有的低优先级pod,再贪心保障尽量多的pod能正常运行,从而计算出最终需要被驱逐的pod及相关信息,详见代码内注释。
3.4. candidateNode
上面函数返回每一个可抢占的node各自的抢占方案后,这里就需要筛选其中一个node来实际执行抢占调度操作。
pkg/scheduler/core/generic_scheduler.go:330 pickOneNodeForPreemption()
–> pkg/scheduler/core/generic_scheduler.go:809
1 | func pickOneNodeForPreemption(nodesToVictims map[*v1.Node]*schedulerapi.Victims) *v1.Node { |
上面代码结合注释,可以归纳出,这个函数中做了非常细致地检查,最高分如下4个步骤来对node进行优先级排序,筛选出一个最终合适的node来被执行抢占调度pod的操作:
1.按违反PDB约束的次数排序
2.按node上需驱逐的第一个pod(即需驱逐的优先级最高的pod)的优先级大小排序
3.按node上需驱逐的所有的pod的优先级总和计算排序
4.按node上需驱逐的所有的pod数量计算排序
5.若经过上面四个步骤的筛选,筛选出的node还是不止一个,那么就挑选其中的第一个作为最后选中node
4. 总结
抢占调度的逻辑可以说是非常细致和精彩,例如
1.从资源计算的角度:
- 基于nodeInfo快照的计算,所有计算在最终确定实施之前都是预计算
- 先枚举出所有低优先级的pod,保障待调度pod能充分获取资源
- 在待调度pod能运行后,再尽力保障最多的低优先级pod能同时运行
2.从node选取的角度:
- 分4个步骤筛选以选出驱逐造成影响最小一个node
本章完,感谢阅读!