抢占调度是分布式调度中一种常见的设计,其核心目标是当不能为高优先级的任务分配资源的时候,会通过抢占低优先级的任务来进行高优先级的调度,本文主要学习k8s的抢占调度以及里面的一些有趣的算法
1. 抢占调度设计
1.1 抢占原理
抢占调度原理其实很简单就是通过高优先级的pod抢占低优先级的pod资源,从而满足高优先pod的调度
1.2 中断预算
在kubernetes中为了保证服务尽可能的高可用,设计PDB(PodDisruptionBudget)其核心目标就是在保证对应pod在指定的数量,主要是为了保证服务的可用性,在进行抢占的过程中,应尽可能遵守该设计,尽量不去抢占有PDB的资源,避免因为抢占导致服务的不可用
1.3 优先级反转
优先级反转是信号量里面的一种机制即因为低优先级任务的运行阻塞高优先级的任务运行
在k8s中抢占调度是通过高优先级抢占低优先级pod,如果高优先级pod依赖低优先级pod, 则会因为依赖问题,导致优先级失效,所以应该尽可能减少高优先级pod对低优先级的pod的依赖, 后面进行筛选源码分析时可以看到
1.4 抢占选择算法
抢占选择算法是指的通过抢占部分节点后,如何从被抢占的node数组中筛选出一个node节点,目前k8s中主要实现了5个算法
1.4.1 最少违反PDB
即最少违反PDB规则
1.4.2 最高优先级最小优先
比较所有node的最高优先级的pod,找到优先级最低的node
1.4.3 优先级总和最低优先
计算每个node上面的被抢占的pod优先级之和,选择优先级和最低的节点
1.4.4 最少抢占数量优先
计算需要抢占的节点数量最少的节点优先
1.4.5 最近更新节点优先
比较每个node中被驱逐的pod中最早启动的pod的启动时间,最近启动的pod的节点,会被选择
2. 源码设计
2.1 抢占核心流程
抢占的流程主要是通过Preempt来实现,其针对预选失败的节点来进行驱逐某些低优先级的pod来满足高优先级pod
func (g *genericScheduler) Preempt(pluginContext *framework.PluginContext, pod *v1.Pod, scheduleErr error) (*v1.Node, []*v1.Pod, []*v1.Pod, error) {
// 只允许预选失败的pod进行重试
fitError, ok := scheduleErr.(*FitError)
if !ok || fitError == nil {
return nil, nil, nil, nil
}
// 是否允许抢占其他提议的pod
if !podEligibleToPreemptOthers(pod, g.nodeInfoSnapshot.NodeInfoMap, g.enableNonPreempting) {
klog.V(5).Infof("Pod %v/%v is not eligible for more preemption.", pod.Namespace, pod.Name)
return nil, nil, nil, nil
}
// 获取当前集群中的所有node
allNodes := g.cache.ListNodes()
if len(allNodes) == 0 {
return nil, nil, nil, ErrNoNodesAvailable
}
// 初步筛选潜在的可以进行抢占操作的node
potentialNodes := nodesWherePreemptionMightHelp(allNodes, fitError)
if len(potentialNodes) == 0 {
klog.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
}
// 获取所有pdb
pdbs, err := g.pdbLister.List(labels.Everything())
if err != nil {
return nil, nil, nil, err
}
// 针对之前初步筛选的node尝试进行抢占和预选操作,返回结果中包含所有可以通过抢占低优先级pod完成pod调度的node节点与抢占的pod
nodeToVictims, err := g.selectNodesForPreemption(pluginContext, pod, g.nodeInfoSnapshot.NodeInfoMap, potentialNodes, g.predicates,
g.predicateMetaProducer, g.schedulingQueue, pdbs)
if err != nil {
return nil, nil, nil, err
}
// 调用extenders进行再一轮的筛选
nodeToVictims, err = g.processPreemptionWithExtenders(pod, nodeToVictims)
if err != nil {
return nil, nil, nil, err
}
// 从筛选结果中选择最适合抢占的node
candidateNode := pickOneNodeForPreemption(nodeToVictims)
if candidateNode == nil {
return nil, nil, nil, nil
}
// 如果candidateNode不为nil,则找到一个最优的执行抢占操作的node, 返回低优先的提议的pod
// 还有抢占的pod和当前节点
nominatedPods := g.getLowerPriorityNominatedPods(pod, candidateNode.Name)
if nodeInfo, ok := g.nodeInfoSnapshot.NodeInfoMap[candidateNode.Name]; ok {
return nodeInfo.Node(), nodeToVictims[candidateNode].Pods, nominatedPods, nil
}
return nil, nil, nil, fmt.Errorf(
"preemption failed: the target node %s