乐趣区

关于kubernetes:kubeapiserver-调度器核心实现

随着 k8s 的倒退,调度器的实现也在变动,本文将从 1.23 版本源码角度解析 k8s 调度器的外围实现。

调度器总览

整个调度过程由
kubernetes/pkg/scheduler/scheduler.go#L421 的 func (sched *Scheduler) scheduleOne(ctx context.Context) 实现。这个函数有两百多行,能够分为四个局部:

获取待调度 Pod 对象:通过 sched.NextPod()从优先级队列中获取一个优先级最高的待调度 Pod 资源对象,该过程是阻塞模式的,当优先级队列中不存在任何 Pod 资源对象时,sched.config.NextPod 函数处于期待状态。
调度阶段:通过 sched.Algorithm.Schedule(schedulingCycleCtx, sched.Extenders, fwk, state, pod) 调度函数执行预选调度算法和优选调度算法,为 Pod 资源对象抉择一个适合的节点。
抢占阶段:当高优先级的 Pod 资源对象没有找到适合的节点时,调度器会通过 sched.preempt 函数尝试抢占低优先级的 Pod 资源对象的节点。
绑定阶段:当调度器为 Pod 资源对象抉择了一个适合的节点时,通过 sched.bind 函数将适合的节点与 Pod 资源对象绑定在一起。
调度过程

进入过滤阶段前的节点数量计算

在初始化调度器的时候,kube-scheduler 会对节点数量进行优化。如下图:
门路:

其中红框是调度器的一个性能优化,通过 PercentageOfNodesToScore 机制,在集群节点数量很多的时候,只加载指定百分比的节点,这样在大集群中,能够显著优化调度性能;这个百分比数值能够调整,默认为 50,即加载一半的节点;具体的节点数量由一个不简单的计算过程得出:

其中,minFeasibleNodesToFind为预设的参加预选的最小可用节点数,当初的值为 100。见上图 172 行,当集群节点数量小于该值或 percentageOfNodesToScore 百分比大于等于 100 时候,间接返回所有节点。当大于 100 个节点的时候,应用了一个公式,adaptivePercentage = basePercentageOfNodesToScore - numAllNodes/125,翻译一下的话就是自适应百分比数 = 默认百分比数 - 所有节点数 /125,见 178 行,默认百分比为 50,假如有 1000 个节点,那么自适应百分比数 =50-1000/125=42;180 和 181 行则是指定了一个百分比上限 minFeasibleNodesPercentageToFind,当初的值为 5。即后面算进去的百分比如果小于 5,则取上限 5。依照这个机制,那么参加过滤的节点数 =100042%=420 个。当这个节点数小于 minFeasibleNodesToFind 的时候,则返回 minFeasibleNodesToFind。因而,1000 个节点的集群最终参加预选的是 420 个;同理能够计算,5000 个节点的集群,参加预选的是 5000(50-5000/125)%=500 个。能够看到,只管节点数量从 1000 减少到了 5000,但参加预选的只从 420 减少到了 500。

过滤阶段

通过 PercentageOfNodesToScore 失去参加预选调度的节点数量之后,scheduler 会通过 podInfo := sched.NextPod() 从调度队列中获取 pod 信息;而后进入 Schedule,这是一个定义了 schedule 的接口,k8s 实现了一个 genericScheduler,如果要自定义本人的调度器,实现该接口,而后在 deployment 中指定用该调度器就行。

type ScheduleAlgorithm interface {Schedule(context.Context, []framework.Extender, framework.Framework, *framework.CycleState, *v1.Pod) (scheduleResult ScheduleResult, err error)
}

进入 genericScheduler 后,首先就进入预选阶段 findNodesThatFitPod,或者称为过滤阶段,此阶段会取得过滤之后可用的所有节点,供下一阶段应用,即 feasibleNodes。

findNodesThatFitPod 供蕴含以下四局部:

fwk.RunPreFilterPlugins:运行过滤前的解决插件。RunPreFilterPlugins 负责运行一组框架已配置的 PreFilter 插件。如果任何插件返回除 Success 之外的任何内容,它将设置返回的 *Status::code 为 non-success。则调度周期停止。
g.evaluateNominatedNode:将某个节点独自执行过滤。如果 Pod 指定了某个 Node 上运行,这个节点很可能是惟一适宜 Pod 的候选节点,那么会在过滤所有节点之前,查看该 Node,具体条件为:len(pod.Status.NominatedNodeName) > 0 && feature.DefaultFeatureGate.Enabled(features.PreferNominatedNode),这个机制也叫“提名节点”。
g.findNodesThatPassFilters:将所有节点进行预选过滤。这个函数会创立一个可用 node 的节点feasibleNodes := make([]*v1.Node, numNodesToFind),而后通过 checkNode 遍历 node,查看 node 是否合乎运行 Pod 的条件,即运行所有的预选调度算法(如下所示),如果合乎则退出 feasibelNodes 列表。

for _, pl := range f.filterPlugins {pluginStatus := f.runFilterPlugin(ctx, pl, state, pod, nodeInfo)
        if !pluginStatus.IsSuccess() {if !pluginStatus.IsUnschedulable() {
                // Filter plugins are not supposed to return any status other than
                // Success or Unschedulable.
                errStatus := framework.AsStatus(fmt.Errorf("running %q filter plugin: %w", pl.Name(), pluginStatus.AsError())).WithFailedPlugin(pl.Name())
                return map[string]*framework.Status{pl.Name(): errStatus}
            }
            pluginStatus.SetFailedPlugin(pl.Name())
            statuses[pl.Name()] = pluginStatus
            if !f.runAllFilters {
                // Exit early if we don't need to run all filters.
                return statuses
            }
        }
    }

findNodesThatPassExtenders:将上一步通过预选的 Node 再通过扩大过滤器过滤一遍。这个其实是 k8s 留给用户的自定义过滤器。它遍历所有的 extender 来确定是否关怀对应的资源,如果关怀就会调用 Filter 接口来进行近程调用 feasibleList, failedMap, failedAndUnresolvableMap, err := extender.Filter(pod, feasibleNodes),并将筛选后果传递给下一个 extender,逐渐放大筛选汇合。近程调用是一个 http 的实现,如下图:

至此,预选阶段完结。整个预选过程逻辑上很天然,预处理 -> 过滤 -> 用户自定义过滤 -> 完结。
在预处理阶段 (PreFilterPlugin),官网次要定义了:
InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性,InterPodAffinity 实现了 PreFilterExtensions,因为抢占调度的 Pod 可能与以后的 Pod 具备亲和性或者反亲和性;
NodePorts: 查看 Pod 申请的端口在 Node 是否可用,NodePorts 未实现 PreFilterExtensions;
NodeResourcesFit: 查看 Node 是否领有 Pod 申请的所有资源,NodeResourcesFit 未实现 PreFilterEtensions;
PodTopologySpread: 实现 Pod 拓扑散布;
ServiceAffinity: 查看属于某个服务(Service) 的 Pod 与配置的标签所定义的 Node 汇合是否适配,这个插件还反对将属于某个服务的 Pod 扩散到各个 Node,ServiceAffinity 实现了 PreFilterExtensions 接口;
VolumeBinding: 查看 Node 是否有申请的卷,是否能够绑定申请的卷,VolumeBinding 未实现 PreFilterExtensions 接口;
过滤插件在晚期版本叫做预选算法,但在较新的版本曾经删除了 /pkg/scheduler/algorithem 这个包,因为用过滤更贴切一点。在这个目录下能够找到所有的插件实现:

基本上通过名字就晓得是做什么的, 不赘述,如
InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性;
NodeAffinity: 实现了 Node 选择器和节点亲和性
NodeLabel: 依据配置的标签过滤 Node;
NodeName: 查看 Pod 指定的 Node 名称与以后 Node 是否匹配;
NodePorts: 查看 Pod 申请的端口在 Node 是否可用;

优选阶段

预选的后果是 true 或 false,意味着一个节点要么满足 Pod 的运行要求,要么不满足;失去泛滥满足的节点后,最终决定 Pod 调度到哪个节点。

在调度器中,优选的过程由 prioritizeNodes 负责,它会返回一个带分数的节点列表,定义如下:

// NodeScore is a struct with node name and score.
type NodeScore struct {
    Name  string
    Score int64
}

最终由 selectHost 返回一个 node 名字,作为最终的 ScheduleResult. 上面进行具体分析。
prioritizeNodes 分为三局部,运行打分前解决插件,运行所有的打分插件,将所有分数相加:

优选阶段最次要的就是运行各种打分插件,kube-scheduler 会调用 ScorePlugin 对通过 FilterPlugin 的 Node 评分,所有 ScorePlugin 的评分都有一个明确的整数范畴,比方[0, 100],这个过程称之为标准化评分。在标准化评分之后,kube-scheduler 将依据配置的插件权重合并所有插件的 Node 评分得出 Node 的最终评分。依据 Node 的最终评分对 Node 进行排序,得分最高者就是最合适 Pod 的 Node。

type ScorePlugin interface {
    Plugin
    // 计算节点的评分,此时须要留神的是参数 Node 名字,而不是 Node 对象。// 如果实现了 PreScorePlugin 就从 CycleState 获取状态,如果没实现,调度框架在创立插件的时候传入了句柄,能够获取指定的 Node。// 返回值的评分是一个 64 位整数,是一个由插件自定义实现取值范畴的分数。Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)
    // 返回 ScoreExtensions 接口,此类设计与 PreFilterPlugin 类似
    ScoreExtensions() ScoreExtensions}

// ScorePlugin 的扩大接口
type ScoreExtensions interface {// ScorePlugin().Score()返回的分数没有任何束缚,然而多个 ScorePlugin 之间须要标准化分数范畴,否则无奈合并分数。// 比方 ScorePluginA 的分数范畴是[0, 10],ScorePluginB 的分数范畴是[0, 100],那么 ScorePluginA 的分数再高对于 ScorePluginB 的影响也是十分无限的。NormalizeScore(ctx context.Context, state *CycleState, p *v1.Pod, scores NodeScoreList) *Status
}

实现该接口的插件有:

ImageLocality: 抉择曾经存在 Pod 运行所需容器镜像的 Node,这样能够省去下载镜像的过程,对于镜像十分大的容器是一个十分有价值的个性,因为启动工夫能够节约几秒甚至是几十秒;
InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性;
NodeAffinity: 实现了 Node 选择器和节点亲和性
NodeLabel: 依据配置的标签过滤 Node;
NodePreferAvoidPods: 基于 Node 的注解 scheduler.alpha.kubernetes.io/preferAvoidPods 打分;
NodeResourcesBalancedAllocation: 调度 Pod 时,抉择资源分配更为平均的 Node;
NodeResourcesLeastAllocation: 调度 Pod 时,抉择资源分配较少的 Node;
NodeResourcesMostAllocation: 调度 Pod 时,抉择资源分配较多的 Node;
RequestedToCapacityRatio: 依据已分配资源的配置函数抉择偏爱 Node;
PodTopologySpread: 实现 Pod 拓扑散布;
SelectorSpread: 对于属于 Services、ReplicaSets 和 StatefulSets 的 Pod,偏好跨多节点部署;
ServiceAffinity: 查看属于某个服务(Service) 的 Pod 与配置的标签所定义的 Node 汇合是否适配,这个插件还反对将属于某个服务的 Pod 扩散到各个 Node;
TaintToleration: 实现了污点和容忍度;
打分之后通过 selectHost 抉择最终 pod 将被调度的节点:

func (g *genericScheduler) selectHost(nodeScoreList framework.NodeScoreList) (string, error) {if len(nodeScoreList) == 0 {return "", fmt.Errorf("empty priorityList")
    }
    maxScore := nodeScoreList[0].Score
    selected := nodeScoreList[0].Name
    cntOfMaxScore := 1
    for _, ns := range nodeScoreList[1:] {
        if ns.Score > maxScore {
            maxScore = ns.Score
            selected = ns.Name
            cntOfMaxScore = 1
        } else if ns.Score == maxScore {
            cntOfMaxScore++
            if rand.Intn(cntOfMaxScore) == 0 {
                // Replace the candidate with probability of 1/cntOfMaxScore
                selected = ns.Name
            }
        }
    }
    return selected, nil
}

至此,优选阶段完结。

总结

总结,k8s 定义了调度的接口,并实现了 genericScheduler(也是 k8s 中惟一的官网调度器)以及泛滥的插件,这层形象其实为开发人员自定义调度器提供了很大的便当。往小的说,各类插件以及扩大插件也提供了丰盛的细粒度管制。当然,最简略的还是去依据理论须要调整优选的打分逻辑,使得 Pod 的调度满足生产须要。

退出移动版