目录
本文主要介绍 Kubernetes调度器的重要机制:优先级(Priority )和抢占(Preemption)机制
一、优先级和抢占机制解决的问题
1、解决问题
首先需要明确的是:优先级和抢占机制,解决的是pod调度失败时该怎么办的问题。
正常情况下,当一个pod调度失败后,就会被暂时 “搁置”pending状态,直到pod被更新,或者集群状态发生变化,调度器才会对这个pod进行重新调度。
场景分析:当一个高优先级的pod调度失败后,该pod并不会被”搁置”,而是会”挤走”某个node上的一些低优先级的Pod。这样就可以保证高优先级pod的调度成功。
2、如何使用
1) PriotityClass
而在kubernetes中,优先级和抢占机制是在1.10版本后才逐步可用的。要使用这个机制,首先需要在kubernetes里提交一个PriotityClass的定义,如下所示:
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."
kubernetes规定,优先级是一个32bit的整数,最大值不超过1000000000(10亿),并且值越大优先级越高。超过10亿的,是被kubernetes保留下来分配给系统pod使用的。目的是保证系统pod不会被用户抢占掉。
上述yaml文件中的globalDefault被设置成true的话,那就意味着这个PriorityClass的值会成为系统的默认值。false表示的是只希望声明使用该PriorityClass的Pod拥有值为1000000的优先级,而对于没有声明Priority的Pod来说,优先级就是0。
2) pod使用
创建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提交给kubernetes之后,kubernetes的PriorityAdmissionController就会将这个pod的spec.priority字段设置为100000。
二、追踪代码分析优先级和抢占策略
1、优先级和抢占体现
优先级:
podQueue: core.NewSchedulingQueue(),
// 没有开启优先级调度时 FIFO 先进先出的队列
// 开启优先级调度时, 优先级队列
// NewSchedulingQueue initializes a new scheduling queue. If pod priority is
// enabled a priority queue is returned. If it is disabled, a FIFO is returned.
func NewSchedulingQueue() SchedulingQueue {
if util.PodPriorityEnabled() {
return NewPriorityQueue()
}
return NewFIFO()
}
// NewFIFO returns a Store which can be used to queue up items to
// process.
func NewFIFO(keyFunc KeyFunc) *FIFO {
f := &FIFO{
items: map[string]interface{}{},
queue: []string{},
keyFunc: keyFunc,
}
f.cond.L = &f.lock
return f
}
// NewPriorityQueue creates a PriorityQueue object.
func NewPriorityQueue() *PriorityQueue {
pq := &PriorityQueue{
activeQ: newHeap(cache.MetaNamespaceKeyFunc, util.HigherPriorityPod),
unschedulableQ: newUnschedulablePodsMap(),
nominatedPods: map[string][]*v1.Pod{},
}
pq.cond.L = &pq.lock
return pq
}
述代码可以看到,调度器中维护着一个调度队列。当pod拥有了优先级之后,高优先级的pod就可能会比低优先级的pod提前出队,从而尽早完成调度过程。”优先级”概念在kubernetes里的主要体现。
抢占:待调度的高优先级pod称为“抢占者”。
而当一个高优先级的pod调度失败的时候,调度器的抢占能力就会被触发。这时,调度器就会试图从当前集群里寻找一个节点,使得当这个节点上的一个或者多个低优先级pod被删除后,待调度的高优先级pod就可以被调度到这个节点上。这个过程,就是“抢占”在kubernetes中的主要体现。
当抢占过程发生时,抢占者并不会立刻被调度到被抢占者的Node上。事实上,调度器只会把抢占者的spec.nominatedNodeName字段设置为被抢占的Node的名字。然后,抢占者会重新进入下一个调度周期。意味着即使在下一个调度周期,调度器也不会保证抢占者一定会运行在被抢占的节点上。
为什么这样设计?调度器只会通过标准的DELETE API来删除被抢占的Pod,所以pod必然是有一定的“优雅退出”时间(默认30S)的。而在这段时间里,其他的节点有可能变为可调度的,或者有其他新节点添加到集群中来。所以,鉴于优雅退出期间,集群的可调度性可能会发生变化,把抢占者交给下一个调度周期再处理,是合理的选择。
而抢占者等待被调度的过程,如果有其他更高优先级的Pod也要抢占同一个节点,那么调度器就会清空原抢占者的spec.nominatedNodeName字段,从而允许更高优先级的抢占者执行抢占,并且原抢占者本身在下一个调度周期会重新抢占其他节点。都是设置nominatedNodeName字段的主要目的。
2、调度器的抢占机制
抢占发生的时机一定是一个高优先级的Pod调度失败。
而kubernetes调度器实现抢占算法的一个重要的设计,就是在调度队列中使用两个不同的队列。
第一个队列,activeQ。凡是在activeQ里的pod,都是下一个调度周期需要调度的对象。创建pod的时候,调度器会将pod入队到activeQ里面。出队pop pod进行调度。
第二个队列:unschedulableQ 专门用来存在调度失败的Pod。关键点在于当一个unschedulabeQ里的Pod被更新之后,调度器会自动把pod移动到activeQ里,从而给这些调度失败的pod“重新调度”的机会。
1)抢占者调度失败时间点怎么处理?
调度失败后,抢占者会被放进unschedulableQ里面。失败时间就会触发 调度器为抢占者寻找牺牲者的流程。
a. 调度器会检查这次失败事件的原因,来确认抢占是不是可以帮助抢占者找到一个新节点。很多predicate的失败是不能通过抢占解决的。 比如 PodFitHost算法(负责的是检查pod的nodeSelector与node的名字是否匹配),这种情况除非node名字发生变化。否则删除再多的pod抢占者也不可能调度成功。
b. 如果确认抢占可以发生,那么调度器就会把自己缓存的所有节点信息复制一份,然后使用这个副本来模拟抢占过程。
// 调度失败代码 fitError 会携带 pod信息以及预选策略失败的算法
sched.preempt(pod, fitError)
// preempt 通过删除抢占低优先级pods 尝试去为调度失败的高优pod创建空间
1. 是否启用抢占调度策略
2. 获取preemptor pod信息
3. 核心抢占算法:返回nominated node, node上牺牲者pod信息、清除已经nominated的pod
4. 设置 .status.nominatedNodename 抢占者删除node上的牺牲者pods
清除低优先级pod的nominatedNodename重新进入下一个调度周期。
func (sched *Scheduler) preempt(preemptor *v1.Pod, scheduleErr error) (string, error) {
if !util.PodPriorityEnabled() || sched.config.DisablePreemption {
}
preemptor, err := sched.config.PodPreemptor.GetUpdatedPod(preemptor)
node, victims, nominatedPodsToClear, err := sched.config.Algorithm.Preempt(preemptor, sched.config.NodeLister, scheduleErr)
var nodeName = ""
if node != nil {
nodeName = node.Name
err = sched.config.PodPreemptor.SetNominatedNodeName(preemptor, nodeName)
for _, victim := range victims {
if err := sched.config.PodPreemptor.DeletePod(victim); err != nil {
}
}
}
for _, p := range nominatedPodsToClear {
rErr := sched.config.PodPreemptor.RemoveNominatedNodeName(p)
}
}
return nodeName, err
}
2) 抢占过程
调度器检查缓存副本里的每一个节点,然后从该节点上最低优先级的pod开始,逐一“删除”这些pod。而删除每一个低优先级pod,调度器都会检查一下抢占者是否能够运行在该node上。一旦可以运行,调度器就记录下这个Node的名字和被删除Pod的列表,这就是一次抢占过程的结果。
1. 判断pod是否有抢占资格,即判断能否发生抢占
2. 获取集群node,过滤掉预选策略失败的node节点,得到 potentialNodes
3. PodDisruptionBudget 特性 == > 后续博文介绍(一句话)
PodDisruptionBudget控制器可以设置应用POD集群处于运行状态最低个数,也可以设置应用POD集群处于运行状态的最低百分比,这样可以保证在主动销毁应用POD的时候,不会一次性销毁太多的应用POD,从而保证业务不中断或业务SLA不降级。
4. selectNodesForPreemption 获取所有node中可能的牺牲者 node ==> victims
5. pickOneNodeForPreemption 选择候选node
6. candidateNode 上移除 Removing their nomination updates these pods and moves them to the active queue. 并进行重新调度。
func (g *genericScheduler) Preempt(pod *v1.Pod, nodeLister algorithm.NodeLister, scheduleErr error) (*v1.Node, []*v1.Pod, []*v1.Pod, error) {
// Scheduler may return various types of errors. Consider preemption only if
// the error is of type FitError.
fitError, ok := scheduleErr.(*FitError)
if !ok || fitError == nil {
return nil, nil, nil, nil
}
err := g.cache.UpdateNodeNameToInfoMap(g.cachedNodeInfoMap)
if err != nil {
return nil, nil, nil, err
}
if !podEligibleToPreemptOthers(pod, g.cachedNodeInfoMap) {
glog.V(5).Infof("Pod %v/%v is not eligible for more preemption.", pod.Namespace, pod.Name)
return nil, nil, nil, nil
}
allNodes, err := nodeLister.List()
if err != nil {
return nil, nil, nil, err
}
if len(allNodes) == 0 {
return nil, nil, nil, ErrNoNodesAvailable
}
potentialNodes := nodesWherePreemptionMightHelp(allNodes, fitError.FailedPredicates)
if len(potentialNodes) == 0 {
glog.V(3).Infof("Preemption will not help schedule pod %v/%v on any node.", pod.Namespace, pod.Name)
// In this case, we should clean-up any existing nominated node name of the pod.
return nil, nil, []*v1.Pod{pod}, nil
}
pdbs, err := g.cache.ListPDBs(labels.Everything())
if err != nil {
return nil, nil, nil, err
}
nodeToVictims, err := selectNodesForPreemption(pod, g.cachedNodeInfoMap, potentialNodes, g.predicates,
g.predicateMetaProducer, g.schedulingQueue, pdbs)
if err != nil {
return nil, nil, nil, err
}
// We will only check nodeToVictims with extenders that support preemption.
// Extenders which do not support preemption may later prevent preemptor from being scheduled on the nominated
// node. In that case, scheduler will find a different host for the preemptor in subsequent scheduling cycles.
nodeToVictims, err = g.processPreemptionWithExtenders(pod, nodeToVictims)
if err != nil {
return nil, nil, nil, err
}
candidateNode := pickOneNodeForPreemption(nodeToVictims)
if candidateNode == nil {
return nil, nil, nil, err
}
// Lower priority pods nominated to run on this node, may no longer fit on
// this node. So, we should remove their nomination. Removing their
// nomination updates these pods and moves them to the active queue. It
// lets scheduler find another place for them.
nominatedPods := g.getLowerPriorityNominatedPods(pod, candidateNode.Name)
if nodeInfo, ok := g.cachedNodeInfoMap[candidateNode.Name]; ok {
return nodeInfo.Node(), nodeToVictims[candidateNode].Pods, nominatedPods, err
}
return nil, nil, nil, fmt.Errorf(
"preemption failed: the target node %s has been deleted from scheduler cache",
candidateNode.Name)
}
当遍历完所有的节点之后,调度器会在上述模拟产生的所有抢占结果里做一个选择,找出最佳结果。而这一步的判断原则,就是尽量减少抢占对整个系统的影响。比如,需要抢占的pod越少越好,需要抢占的pod的优先级越低越好。
3)抢占后
抢占算法执行完之后,得到了即将被抢占的Node;被删除的Pod列表,即牺牲者。接下来调度器就可以真正开始抢占的操作了。
- 调度器检查牺牲者列表,清理这些Pod携带的nominatedNodeName字段
- 调度器设置抢占者的nominatedNodeName设置为被抢占的Node的名字
- 调度器会开启一个goroutinue,同步地删除牺牲者。
第二步中对抢占者pod的更新操作,就会触发“重新调度”的流程,从而让抢占者在下一个调度周期重新进入调度流程。
接下来,调度器就会通过正常的调度流程把抢占者调度成功。调度器并不保证抢占的结果:正常的调度流程,一切皆有可能。
以上就是kubernetes默认调度器里实现的优先级和抢占机制的实现原理。V1.11之后已经是Beta了,意味着比较稳定,开启这两个特性以便实现更高的资源使用率。
三、两种情况
- 当整个集群发生可能会影响调度结果的变化(添加或者更新Node,添加和更新PV、Service等)时,调度器会执行一个c.podQueue.MoveAllToActiveQueue()的操作,把所调度失败的pod从unschedulableQ移动到activeQ中
- 当一个已经调度成功的Pod被更新时,调度器则会将unschedulableQ里面跟这个pod有亲和性/互斥性关系的pod,移动到activeQ里面
只要是对调度结果会产生影响,调度器都会把unschedulableQ移动到activeQ中重新进行调度流程。
参考
https://kubernetes.cn/docs/concepts/configuration/pod-priority-preemption/
张磊kubernetes讲解结合源码