P3-Node筛选算法
前言
在上一篇文档中,我们找到调度器筛选node的算法入口pkg/scheduler/core/generic_scheduler.go:162
Schedule()
方法
那么在本篇,由此Schedule()
函数展开,看一看调度器的node筛选算法,优先级排序算法留作下一篇.
正文
Schedule()的筛选算法核心是findNodesThatFit()
方法 ,直接跳转过去:
pkg/scheduler/core/generic_scheduler.go:184
–> pkg/scheduler/core/generic_scheduler.go:435
下面注释划出重点,篇幅有限省略部分代码:
1 | func (g *genericScheduler) findNodesThatFit(pod *v1.Pod, nodes []*v1.Node) ([]*v1.Node, FailedPredicateMap, error) { |
这里一眼就可以看出核心匿名函数内的主体是podFitsOnNode()
,但是并不是直接执行podFitsOnNode()
函数,而是又封装了一层函数,这个函数的作用是在外层使用nodeName := g.cache.NodeTree().Next()
来获取要判断的node主体,传递给podFitsOnNode()
函数,而后对podFitsOnNode
函数执行返回的结果进行处理。着眼于其下的并发处理实现:workqueue.ParallelizeUntil(ctx, 16, int(allNodes), checkNode)
,就可以理解这样封装的好处了,来看看并发实现的内部吧:
vendor/k8s.io/client-go/util/workqueue/parallelizer.go:38
1 | func ParallelizeUntil(ctx context.Context, workers, pieces int, doWorkPiece DoWorkPieceFunc) { |
敲黑板记笔记:
1 | 1.chan struct{}是什么鬼? struct{}类型的chan,不占用内存,通常用作go协程之间传递信号,详情可参 |
来看看checkNode()
内部是怎样获取每个子协程对应的node主体的:
pkg/scheduler/core/generic_scheduler.go:460 --> pkg/scheduler/internal/cache/node_tree.go:161
可以看到,这里有一个zone的逻辑层级,这个层级仿佛没有见过,google了一番才了解了这个颇为冷门的功能:这是一个轻量级的支持集群联邦特性的实现,单个cluster可以属于多个zone,但这个功能目前只有GCE和AWS支持,且绝大多数的使用场景也用不到,可以说是颇为冷门。默认情况下,cluster只属于一个zone,可以理解为cluster和zone是同层级,因此后面见到有关zone相关的层级,我们直接越过它。有兴趣的朋友可以了解一下zone的概念:
https://kubernetes.io/docs/setup/best-practices/multiple-zones/
继续往下, pkg/scheduler/internal/cache/node_tree.go:176
–> pkg/scheduler/internal/cache/node_tree.go:47
1 | // nodeArray is a struct that has nodes that are in a zone. |
果然可以看到, nodeArray结构体内部维护了一个lastIndex指针来获取node,印证了上面的推测。
回到pkg/scheduler/core/generic_scheduler.go:461
,正式进入podFitsOnNode
内部:
1 | func podFitsOnNode( |
注释和部分代码已省略,基于podFitsOnNode
函数内的注释,来做一下说明:
1.通过指定pod.spec.priority
,来为pod指定调度优先级的功能,在1.14版本已经正式GA,这里所有的调度相关功能都会考虑到pod优先级,因为优先级的原因,因此除了正常的Schedule调度动作外,还会有Preempt
抢占调度的行为,这个podFitsOnNode()
方法会被在这两个地方调用。
2.Schedule调度时,会取出当前node上所有已存在的pod,与被提名调度的pod进行优先级对比,取出所有优先级大于等于提名pod,将它们需求的资源加上提名pod所需求的资源,进行汇总,predicate筛选算法计算的时候,是基于这个汇总的结果来进行计算的。举个例子,node A memory cap = 128Gi
,其上现承载有20个pod,其中10个pod的优先级大于等于提名pod,它们sum(request.memory) = 100Gi
,若提名pod的request.memory = 32Gi
, (100+32) > 128
,因此筛选时会在内存选项失败返回false;若提名pod的request.memory = 16Gi
,(100+16) < 128
,则内存项筛选通过。那么剩下的优先级较低的10个pod就不考虑它们了吗,它们也要占用内存呀?处理方式是:如果它们占用内存造成node资源不足无法调度提名pod,则调度器会将它们剔出当前node,这即是Preempt
抢占。Preempt抢占的说明会在后面的文章中补充.
3.对于每个提名pod,其调度过程会被重复执行1次,为什么需要重复执行呢?考虑到有一些场景下,会判断到pod之间的亲和力筛选策略,例如pod A对pod B有亲和性,这时它们一起调度到node上,但pod B此时实际并未完成调度启动,那么pod A的inter-pod affinity predicates
一定会失败,因此,重复执行1次筛选过程是有必要的.
有了以上理解,我们接着看代码,图中已注释:
图中pkg/scheduler/core/generic_scheduler.go:608
位置正式开始了逐个计算筛选算法,那么筛选方法、筛选方法顺序在哪里呢?在上一篇P2-框架篇)中已经有讲过,默认调度算法都在pkg/scheduler/algorithm/
路径下,我们接着往下看.
Predicates Ordering / Predicates Function
筛选算法相关的key/func/ordering
,全部集中在pkg/scheduler/algorithm/predicates/predicates.go
这个文件中
筛选顺序:
pkg/scheduler/algorithm/predicates/predicates.go:142
1 | // 默认predicate顺序 |
官方的备注:
筛选key
1 | const ( |
筛选Function
每个predicate key
对应的function name
一般为${KEY}Predicate
,function的内容其实都比较简单,不一一介绍了,自行查看,这里仅列举一个:
pkg/scheduler/algorithm/predicates/predicates.go:1567
1 | // CheckNodeMemoryPressurePredicate checks if a pod can be scheduled on a node |
筛选算法过程到这里就已然清晰明了!
重点回顾
筛选算法代码中的几个不易理解的点(亮点?)圈出:
- node粒度的并发控制
- 基于优先级的pod资源总和归纳计算
- 筛选过程重复1次
本篇调度器筛选算法篇到此结束,下一篇将学习调度器优先级排序的算法详情内容