kube-scheduler源码分析(六)之 preempt
以下代码分析基于
kubernetes v1.12.0
版本。
本文主要分析调度中的抢占逻辑,当pod不适合任何节点的时候,可能pod会调度失败,这时候可能会发生抢占。抢占逻辑的具体实现函数为Scheduler.preempt
。
1. 调用入口
当pod不适合任何节点的时候,可能pod会调度失败。这时候可能会发生抢占。
scheduleOne
函数中关于抢占调用的逻辑如下:
此部分的代码位于/pkg/scheduler/scheduler.go
1 | // scheduleOne does the entire scheduling workflow for a single pod. It is serialized on the scheduling algorithm's host fitting. |
其中核心代码为:
1 | // 基于sched.schedule(pod)返回的err和当前待调度的pod执行抢占策略 |
2. Scheduler.preempt
当pod调度失败的时候,会抢占低优先级pod的空间来给高优先级的pod。其中入参为调度失败的pod对象和调度失败的err。
抢占的基本流程如下:
- 判断是否有关闭抢占机制,如果关闭抢占机制则直接返回。
- 获取调度失败pod的最新对象数据。
- 执行抢占算法
Algorithm.Preempt
,返回预调度节点和需要被剔除的pod列表。 - 将抢占算法返回的node添加到pod的
Status.NominatedNodeName
中,并删除需要被剔除的pod。 - 当抢占算法返回的node是nil的时候,清除pod的
Status.NominatedNodeName
信息。
整个抢占流程的最终结果实际上是更新Pod.Status.NominatedNodeName
属性的信息。如果抢占算法返回的节点不为空,则将该node更新到Pod.Status.NominatedNodeName
中,否则就将Pod.Status.NominatedNodeName
设置为空。
2.1. preempt
preempt的具体实现函数:
此部分的代码位于/pkg/scheduler/scheduler.go
1 | // preempt tries to create room for a pod that has failed to schedule, by preempting lower priority pods if possible. |
以下对preempt
的实现分段分析。
如果设置关闭抢占机制,则直接返回。
1 | if !util.PodPriorityEnabled() || sched.config.DisablePreemption { |
获取当前pod的最新状态。
1 | preemptor, err := sched.config.PodPreemptor.GetUpdatedPod(preemptor) |
GetUpdatedPod
的实现就是去拿pod的对象。
1 | func (p *podPreemptor) GetUpdatedPod(pod *v1.Pod) (*v1.Pod, error) { |
接着执行抢占的算法。抢占的算法返回预调度节点的信息和因抢占被剔除的pod的信息。具体的抢占算法逻辑下文分析。
1 | node, victims, nominatedPodsToClear, err := sched.config.Algorithm.Preempt(preemptor, sched.config.NodeLister, scheduleErr) |
将预调度节点的信息更新到pod的Status.NominatedNodeName
属性中。
1 | err = sched.config.PodPreemptor.SetNominatedNodeName(preemptor, nodeName) |
SetNominatedNodeName
的具体实现为:
1 | func (p *podPreemptor) SetNominatedNodeName(pod *v1.Pod, nominatedNodeName string) error { |
接着删除因抢占而需要被剔除的pod。
1 | err := sched.config.PodPreemptor.DeletePod(victim) |
PodPreemptor.DeletePod
的具体实现就是删除具体的pod。
1 | func (p *podPreemptor) DeletePod(pod *v1.Pod) error { |
如果抢占算法得出的node对象为nil,则将pod的Status.NominatedNodeName
属性设置为空。
1 | // Clearing nominated pods should happen outside of "if node != nil". Node could |
RemoveNominatedNodeName
的具体实现如下:
1 | func (p *podPreemptor) RemoveNominatedNodeName(pod *v1.Pod) error { |
2.2. NominatedNodeName
Pod.Status.NominatedNodeName
的说明:
nominatedNodeName
是调度失败的pod抢占别的pod的时候,被抢占pod的运行节点。但在剔除被抢占pod之前该调度失败的pod不会被调度。同时也不保证最终该pod一定会调度到nominatedNodeName
的机器上,也可能因为之后资源充足等原因调度到其他节点上。最终该pod会被加到调度的队列中。
其中加入到调度队列的具体过程如下:
1 | func NewConfigFactory(args *ConfigFactoryArgs) scheduler.Configurator { |
addPodToSchedulingQueue:
1 | func (c *configFactory) addPodToSchedulingQueue(obj interface{}) { |
PriorityQueue.Add:
1 | // Add adds a pod to the active queue. It should be called only when a new pod |
addNominatedPodIfNeeded:
1 | // addNominatedPodIfNeeded adds a pod to nominatedPods if it has a NominatedNodeName and it does not |
NominatedNodeName:
1 | // NominatedNodeName returns nominated node name of a Pod. |
3. genericScheduler.Preempt
抢占算法依然是在ScheduleAlgorithm
接口中定义。
1 | // ScheduleAlgorithm is an interface implemented by things that know how to schedule pods |
Preempt
的具体实现为genericScheduler
结构体。
Preempt
的主要实现是找到可以调度的节点和上面因抢占而需要被剔除的pod。
基本流程如下:
- 根据调度失败的原因对所有节点先进行一批筛选,筛选出潜在的被调度节点列表。
- 通过
selectNodesForPreemption
筛选出需要牺牲的pod和其节点。 - 基于拓展抢占逻辑再次对上述筛选出来的牺牲者做过滤。
- 基于上述的过滤结果,选择一个最终可能因抢占被调度的节点。
- 基于上述的候选节点,找出该节点上优先级低于当前被调度pod的牺牲者pod列表。
完整代码如下:
此部分代码位于pkg/scheduler/core/generic_scheduler.go
1 | // preempt finds nodes with pods that can be preempted to make room for "pod" to |
以下对genericScheduler.Preempt
分段进行分析。
3.1. selectNodesForPreemption
selectNodesForPreemption
并行地所有节点中找可能被抢占的节点。
1 | nodeToVictims, err := selectNodesForPreemption(pod, g.cachedNodeInfoMap, potentialNodes, g.predicates,g.predicateMetaProducer, g.schedulingQueue, pdbs) |
selectNodesForPreemption
主要基于selectVictimsOnNode
构造一个checkNode的函数,然后并发执行该函数。
selectNodesForPreemption
具体实现如下:
1 | // selectNodesForPreemption finds all the nodes with possible victims for |
3.1.1. selectVictimsOnNode
selectVictimsOnNode
找到应该被抢占的给定节点上的最小pod集合,以便给调度失败的pod安排足够的空间。该函数最终返回的是一个pod的数组。当有更低优先级的pod可能被选择的时候,较高优先级的pod不会被选入该待剔除的pod集合。
基本流程如下:
- 先检查当该节点上所有低于预被调度pod优先级的pod移除后,该pod能否被调度到当前节点上。
- 如果上述检查可以,则将该节点的所有低优先级pod按照优先级来排序。
1 | // selectVictimsOnNode finds minimum set of pods on the given node that should |
3.2. processPreemptionWithExtenders
processPreemptionWithExtenders
基于selectNodesForPreemption
选出的牺牲者进行扩展的抢占逻辑继续筛选牺牲者。
1 | // We will only check nodeToVictims with extenders that support preemption. |
processPreemptionWithExtenders
完整代码如下:
1 | // processPreemptionWithExtenders processes preemption with extenders |
3.3. pickOneNodeForPreemption
pickOneNodeForPreemption
从筛选出的node中再挑选一个节点作为最终调度节点。
1 | candidateNode := pickOneNodeForPreemption(nodeToVictims) |
pickOneNodeForPreemption
完整代码如下:
1 | // pickOneNodeForPreemption chooses one node among the given nodes. It assumes |
3.4. getLowerPriorityNominatedPods
getLowerPriorityNominatedPods
的基本流程如下:
- 获取候选节点上的pod列表。
- 获取待调度pod的优先级值。
- 遍历该节点的pod列表,如果低于待调度pod的优先级则放入低优先级pod列表中。
genericScheduler.Preempt中相关代码如下:
1 | // Lower priority pods nominated to run on this node, may no longer fit on |
getLowerPriorityNominatedPods
代码如下:
此部分代码位于pkg/scheduler/core/generic_scheduler.go
1 | // getLowerPriorityNominatedPods returns pods whose priority is smaller than the |
4. 总结
4.1. Scheduler.preempt
当pod调度失败的时候,会抢占低优先级pod的空间来给高优先级的pod。其中入参为调度失败的pod对象和调度失败的err。
抢占的基本流程如下:
- 判断是否有关闭抢占机制,如果关闭抢占机制则直接返回。
- 获取调度失败pod的最新对象数据。
- 执行抢占算法
Algorithm.Preempt
,返回预调度节点和需要被剔除的pod列表。 - 将抢占算法返回的node添加到pod的
Status.NominatedNodeName
中,并删除需要被剔除的pod。 - 当抢占算法返回的node是nil的时候,清除pod的
Status.NominatedNodeName
信息。
整个抢占流程的最终结果实际上是更新Pod.Status.NominatedNodeName
属性的信息。如果抢占算法返回的节点不为空,则将该node更新到Pod.Status.NominatedNodeName
中,否则就将Pod.Status.NominatedNodeName
设置为空。
4.2. genericScheduler.Preempt
Preempt
的主要实现是找到可以调度的节点和上面因抢占而需要被剔除的pod。
基本流程如下:
- 根据调度失败的原因对所有节点先进行一批筛选,筛选出潜在的被调度节点列表。
- 通过
selectNodesForPreemption
筛选出需要牺牲的pod和其节点。 - 基于拓展抢占逻辑再次对上述筛选出来的牺牲者做过滤。
- 基于上述的过滤结果,选择一个最终可能因抢占被调度的节点。
- 基于上述的候选节点,找出该节点上优先级低于当前被调度pod的牺牲者pod列表。
参考:
赞赏一下