我们说一下Kubernetes调度器另一个重要机制,优先级和抢占机制问题,这个问题用于解决Pod调度失败的问题

当一个Pod调度失败的话,会被暂时搁置,直到pod被更新,或者集群发生变化,调度器才会对这个Pod重新调度,这是正常情况下

我们有时候,希望的场景是,当一个高优先级的Pod调度失败后,这个Pod并不会被搁置,而是先杀死一些低优先级的Pod,保证高优先级的Pod调度成功,这也是Borg项目中的一个基本能力

而在Kubernetes中,优先级和抢占机制也是存在的,但是这其中涉及到了优先级的概念

我们希望使用优先级机制,需要首先定义一个PriorityClass的定义

apiVersion: scheduling.k8s.io/v1beta1

kind: PriorityClass

metadata:

name: high-priority

value: 1000000

globalDefault: false

description: “This priority class should be used for high priority service pods only.”

这个YAML文件,定义了一个名为high-priority的PriorityClass,其中Vlaue的值为1000000

Kubernetes中,优先级是一个32bit的整数,最大值不超过10亿,值越大优先级越高,超过10亿的值,交由Kubernetes保留的Pod使用,这样,就可以保证系统Pod不会被用户抢占掉

上述文件的globalDefault的值说明的这个PriorityClass定义的值是否会成为系统的默认值,为true就说明这个值会成为系统的默认值.如果这个值是false,就希望声明使用该PriorityClass的Pod用优质为1000000的优先级,对于没有声明PriorityClass的Pod,优先级为0

在创建了PriorityClass对象后,Pod就可以绑定使用了,如下所示

apiVersion: v1

kind: Pod

metadata:

name: nginx

labels:

env: test

spec:

containers:

– name: nginx

image: nginx

imagePullPolicy: IfNotPresent

priorityClassName: high-priority

如果这个Pod通过priorityClassName字段后,声明使用high-priority的PriorityClass,当这个Pod被提交给Kubernets之后,Kubernetes的PriorityAdmissionController就自动将这个Pod的spec.priority字段设置为1000000的优先级,对于没有声明使用PriorityClass的Pod,优先级是0

创建了PriorityClass对象之后,Pod的绑定如下

apiVersion: v1

kind: Pod

metadata:

name: nginx

labels:

env: test

spec:

containers:

– name: nginx

image: nginx

imagePullPolicy: IfNotPresent

priorityClassName: high-priority

这就让两者绑定上了

调度器中维护着一个调度队列,当Pod拥有了优先级之后,高优先级的Pod就会比低优先级的Pod提前出队,完成调度过程

当一个高优先级的Pod调度失败的时候,调度器的抢占能力就会被触发,这时候,调度器就会试图从当前集群中寻找一个节点,这个节点上一个或者多个低优先级Pod被删除了,高优先级Pod可以调度到这个节点上,这个过程,就是抢占这个概念在Kuberentes中的主要体现

在抢占过程发生的时候,抢占者不会立刻调度到被抢占的Node上,而是先设置上spec.nominatedNodeName字段,设置为了被强占的Node的名字,然后进入等待,知道下一个调度周期,在新的周期中,才会决定是不是运行在被抢占的节点上,在下一个周期,也是可能不运行在选定的节点上

这样在下一个周期进行的原因,是被抢占者需要一定的时间进行退出,这就需要等待,而在等待的时候,可能有其他的节点是可调度的,或者有新的节点被添加进来.所以集群的可调度性是会发生变化的,所以抢占者需要等待一个调度周期

而且如果有更高级的Pod抢占同一个节点的时候,那么调度器就会清空抢占者的spec.nominatedNodeName字段,让更高优先的抢占者执行

具体的抢占流程如下

在实际的调度队列中,有两个不同的队列

一个队列,叫做activeQ,凡在activeQ中的Pod,都会在下一个调度周期中需要调度的对象,所以,Kuberentes集群中创建一个Pod,就会进入这个activeQ

另一个队列,叫做unshedulableQ,存放调度失败的pod,但是在这个队列中的pod,如果被更新了,调度器就会自动的将这个Pod移动到activeQ,给这个调度失败的pod重新做人的机会

一个pod调度失败了,那么这个pod会进入这个队列,然后队列中的控制器会检查这个失败事件的原因,确认抢占是否可以帮助失败者找到一个新的节点,很多的Predicates的失败是不能通过来解决的,比如 PodFitsHost检查中,检查pod的nodeSelector和Node的名字是否匹配,如果这种原因的失败,那么除非node名字改变,不然不可能调度成功

然后如果可以抢占,那么调度器就会把自己的缓存的节点信息复制一份,然后使用这个副本来模拟抢占

这个抢占中,调度器会检查副本中每一个节点,从这个节点上最低优先级的Pod开始,逐一开始删除这个Pod,而没删除一个低优先级的Pod,就需要检查抢占者是否可以运行在这个Node上.一旦可以了,就记录下这个Node的名字和被删除的POD的列表

遍历了所有节点之后,就会做出一个选择,选择的原则是减少对整个系统的影响.优先级,Pod少进行删除的节点.

得到了追加的抢占结果,这个结果给出的Node,就是被抢占的Node,被删除的Pod列表,就是牺牲者,调度器就可以真正的抢占操作

第一步,调度器会检查牺牲者列表,清理这些Pod携带的nominatedNodeName字段,

第二步,调度器会将抢占者的nominatedNodeName,设置为被抢占的Node

第三步,开启一个Goroutine,同步的删除牺牲者

第二步,删除被抢占者的操作,就会触发我们提到的重新做人的流程,让抢占者在下一个调度周期进入调度流程.

接下来,调度器就会按照正常的部署流程将抢占者调度成功

而一个正常部署的Pod,因为上面的抢占流程的存在,所以可能在正常调度的时候,需要考虑即将抢占的Pod,为一个Pod和Node进行匹配执行Predicates算法的时候,如果发现对应的Node是一个被抢占的节点,调度队列中有nominatedNodeName字段是这个Node名字的Pod存在,就会将Predicates算法运行两遍进行检查

第一遍,调度器会假设上述潜在的抢占者已经运行在这个节点上,执行Predicates算法.

第二遍,调度器会正常执行Predicates算法,不考虑任何的潜在抢占者

这两个Predicates算法都能通过,Pod和Node才是可以绑定的

执行第一遍Predicates算法的原因,是因为InterPodAntiAffinity规则的存在,因为InterPodAntiAffinity规则关心待考察节点上所有pod的互斥关系,我们在执行调度算法时候必须要考虑,如果抢占者已经存在了待Pod上,待调度Pod还能否存在在同一节点上,当然这一步我们只考虑优先级比自己高的Pod

第二遍则不考虑抢占者Pod,不考虑的原因是因为潜在的抢占者最后不一定会运行在待考察的Node上

这就是Kubernetes默认调度器中优先级和抢占机制的实现原理

那么思考一下,当集群发生会影响调度结果的变化时候,调度器会执行一个被称为MoveAllToActiveQuque的操作,将所有调度失败的Pod从unscheduelableQ移动到activeQ中,为何,同理,一个调度成功的Pod被更新的时候,调度器会将unschedulableQ中和这个Pod有Affinity/Anti-affininty关系的Pod,移动到activeQ中

对于第一个问题,我们可以认为是在添加或者更新了一个新的node/pv/pvc/service的时候,将会可以使得所有的等待Pod重新调度

对于第二个问题,当一个Pod被创建的时候,可能导致pod被可以重新匹配,或者一个Pod被更新的时候,他的label可能被更新导致可以重新调度

当一个Pod被删除的时候,匹配的PodAffinity Pod可以考虑调度到这个Node上

发表评论

邮箱地址不会被公开。 必填项已用*标注