写点什么

kube-apiserver 调度器核心实现

作者:申屠鹏会
  • 2022 年 6 月 09 日
  • 本文字数:4705 字

    阅读完需:约 15 分钟

随着 k8s 的发展,调度器的实现也在变化,本文将从 1.23 版本源码角度解析 k8s 调度器的核心实现。

调度器总览

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


  1. 获取待调度 Pod 对象:通过sched.NextPod()从优先级队列中获取一个优先级最高的待调度 Pod 资源对象,该过程是阻塞模式的,当优先级队列中不存在任何 Pod 资源对象时,sched.config.NextPod 函数处于等待状态。

  2. 调度阶段:通过sched.Algorithm.Schedule(schedulingCycleCtx, sched.Extenders, fwk, state, pod)调度函数执行预选调度算法和优选调度算法,为 Pod 资源对象选择一个合适的节点。

  3. 抢占阶段:当高优先级的 Pod 资源对象没有找到合适的节点时,调度器会通过 sched.preempt 函数尝试抢占低优先级的 Pod 资源对象的节点。

  4. 绑定阶段:当调度器为 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。按照这个机制,那么参与过滤的节点数=1000*42%=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),官方主要定义了:

  1. InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性,InterPodAffinity 实现了 PreFilterExtensions,因为抢占调度的 Pod 可能与当前的 Pod 具有亲和性或者反亲和性;

  2. NodePorts: 检查 Pod 请求的端口在 Node 是否可用,NodePorts 未实现 PreFilterExtensions;

  3. NodeResourcesFit: 检查 Node 是否拥有 Pod 请求的所有资源,NodeResourcesFit 未实现 PreFilterEtensions;

  4. PodTopologySpread: 实现 Pod 拓扑分布;

  5. ServiceAffinity: 检查属于某个服务(Service)的 Pod 与配置的标签所定义的 Node 集合是否适配,这个插件还支持将属于某个服务的 Pod 分散到各个 Node,ServiceAffinity 实现了 PreFilterExtensions 接口;

  6. VolumeBinding: 检查 Node 是否有请求的卷,是否可以绑定请求的卷,VolumeBinding 未实现 PreFilterExtensions 接口;


    过滤插件在早期版本叫做预选算法,但在较新的版本已经删除了/pkg/scheduler/algorithem 这个包,因为用过滤更贴切一点。在这个目录下可以找到所有的插件实现:



  1. 基本上通过名字就知道是做什么的,不赘述,如

  2. InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性;

  3. NodeAffinity: 实现了 Node 选择器和节点亲和性

  4. NodeLabel: 根据配置的标签过滤 Node;

  5. NodeName: 检查 Pod 指定的 Node 名称与当前 Node 是否匹配;

  6. 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}
复制代码

实现该接口的插件有:

  1. ImageLocality: 选择已经存在 Pod 运行所需容器镜像的 Node,这样可以省去下载镜像的过程,对于镜像非常大的容器是一个非常有价值的特性,因为启动时间可以节约几秒甚至是几十秒;

  2. InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性;

  3. NodeAffinity: 实现了 Node 选择器和节点亲和性

  4. NodeLabel: 根据配置的标签过滤 Node;

  5. NodePreferAvoidPods: 基于 Node 的注解 scheduler.alpha.kubernetes.io/preferAvoidPods 打分;

  6. NodeResourcesBalancedAllocation: 调度 Pod 时,选择资源分配更为均匀的 Node;

  7. NodeResourcesLeastAllocation: 调度 Pod 时,选择资源分配较少的 Node;

  8. NodeResourcesMostAllocation: 调度 Pod 时,选择资源分配较多的 Node;

  9. RequestedToCapacityRatio: 根据已分配资源的配置函数选择偏爱 Node;

  10. PodTopologySpread: 实现 Pod 拓扑分布;

  11. SelectorSpread: 对于属于 Services、ReplicaSets 和 StatefulSets 的 Pod,偏好跨多节点部署;

  12. ServiceAffinity: 检查属于某个服务(Service)的 Pod 与配置的标签所定义的 Node 集合是否适配,这个插件还支持将属于某个服务的 Pod 分散到各个 Node;

  13. 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 的调度满足生产需要。

发布于: 刚刚阅读数: 3
用户头像

申屠鹏会

关注

enjoy~ 2018.11.08 加入

https://xabc.site

评论

发布
暂无评论
kube-apiserver调度器核心实现_k8s_申屠鹏会_InfoQ写作社区