Skip to main content
  1. Docs/

Kubernetes Scheduler

·8 mins· ·
Owl Dawn
Author
Owl Dawn
Table of Contents

kube-scheduler 是 kubernetes 核心组件中的一个,负责将新创建的 pod 调度到集群中合适的节点上运行。

Scheduler 在整个系统中承担了"承上启下"的重要功能。它负责接受 Controller Manager 创建的新 Pod,为其安排合适的 Node;另一方面又在安置工作完成后,目标 Node 上的 kubelet 服务进程接管后续工作。Pod 是 Kubernetes 中最小的调度单元。

入口
#

kube-scheduler 的核心调度逻辑通过 sched.Run(ctx) 启动:

// Run begins watching and scheduling. It starts scheduling and blocked until the context is done.
func (sched *Scheduler) Run(ctx context.Context) {
	logger := klog.FromContext(ctx)
    // 启动调度队列,调度队列 watch 待调度的 pod
	sched.SchedulingQueue.Run(logger)

	// We need to start scheduleOne loop in a dedicated goroutine,
	// because scheduleOne function hangs on getting the next item
	// from the SchedulingQueue.
	// If there are no new pods to schedule, it will be hanging there
	// and if done in this goroutine it will be blocking closing
	// SchedulingQueue, in effect causing a deadlock on shutdown.
    // 轮询执行 sched.scheduleOne,其会消费调度队列中待调度的 pod,执行完成 pod 的调度
	go wait.UntilWithContext(ctx, sched.scheduleOne, 0)

	<-ctx.Done()
    // 清理释放资源
	sched.SchedulingQueue.Close()
}

调度模型
#

经过入口之后,就开始了 kube-scheduler 的调度过程。scheduler 调度模型如图:

主要分为三个部分:

  • Policy:Scheduler 的调度策略启动配置目前支持三种方式,配置文件/命令行参数/ConfigMap。调度策略可以配置指定调度主流程中要用哪些过滤器(Predicates)、打分器(Priorities) 、外部扩展的调度器(Extenders),以及 SchedulerFramwork 的自定义扩展点(Plugins)
  • Informer:Scheduler 在启动的时候通过 K8s 的 informer 机制通过 List&Watch 从 kube-apiserver 获取调度需要的数据。例如: Pods、Nodes、Persistant Volume (PV), Persistant Volume Claim(PVC)等等,并将这些数据做一定的预处理,作为调度器的的 Cache。
  • 调度流水线:通过 Informer 将需要调度的 Pod 插入 Queue 中,Pipeline 会循环从 Queue Pop 等待调度的 Pod 放入 Pipeline 执行。调度流水线(Schedule Pipeline)主要有三个阶段:Scheduler Thread,WaitThread,Bind Thread.
    • Scheduler Thread 阶段:Schduler Thread 会经历 Pre Filter -> Filter -> Post Filter-> Score -> Reserve,可以简单理解为 Filter -> Score -> Reserve。Filter 阶段用于选择符合 Pod Spec 描述的 Nodes;Score 阶段用于从 Filter 过后的 Nodes 进行打分和排序;Reserve 阶段将 Pod 预留资源到排序后的最优 Node 的 NodeCache 中,表示这个 Pod 已经分配到这个 Node 上, 让下一个等待调度的 Pod 对这个 Node 进行 Filter 和 Score 的时候能看到刚才分配的 Pod
    • Wait Thread 阶段:这个阶段可以用来等待 Pod 关联的资源的 Ready 等待,例如等待 PVC 的 PV 创建成功,或者 Gang 调度中等待关联的 Pod 调度成功等;
    • Bind Thread 阶段:用于将 pod 和 node 的关系持久化到 apiserver。

整个调度流水线只有在 Scheduler Thread 阶段是串行的一个 Pod 一个 Pod 的进行调度,在 Wait 和 Bind 阶段 Pod 都是异步并行执行。

scheduling framework
#

kubernetes 在 1.15 版本之后,引入了这种比较灵活的调度框架。调度框架是面向 Kubernetes 调度器的一种插件架构, 它由一组直接编译到调度程序中的“插件” API 组成。这些 API 允许大多数调度功能以插件的形式实现,同时使调度“核心”保持简单且可维护。(调度框架的设计提案

调度框架定义了一些扩展点。调度器插件注册后在一个或多个扩展点处被调用。这些插件中的一些可以改变调度决策,而另一些仅用于提供信息。每个插件支持不同的调度扩展点,一个插件可以在多个扩展点处注册,以执行更复杂或有状态的任务。

每次调度一个 Pod 的尝试都分为两个阶段,即调度周期绑定周期调度周期为 Pod 选择一个节点,绑定周期将该决策应用于集群。二者一起被称为“调度上下文”。调度周期是串行运行的,而绑定周期可能是同时运行的。如果确定 Pod 不可调度或者存在内部错误,则可以终止调度周期或绑定周期,Pod 将返回队列并重试。

调度扩展点:

kube-scheduler 调度逻辑运行依赖 Scheduler 的实现,其中包含了调度逻辑运行所依赖的接口和方法。其创建如下:

// cmd/kube-scheduler/app/server.go
func Setup(ctx context.Context, opts *options.Options, outOfTreeRegistryOptions ...Option) (*schedulerserverconfig.CompletedConfig, *scheduler.Scheduler, error) {
    // 设置 kubeSchedulerConfiguration 的默认配置,后续 opt.CommontConfig 会根据具体文件重新配置
	if cfg, err := latest.Default(); err != nil {
		return nil, nil, err
	} else {
		opts.ComponentConfig = cfg
	}

	if errs := opts.Validate(); len(errs) > 0 {
		return nil, nil, utilerrors.NewAggregate(errs)
	}
    
    // 检查 *options.Options 合法性
	c, err := opts.Config(ctx)
	if err != nil {
		return nil, nil, err
	}

	// Get the completed config 补全需要的字段
	cc := c.Complete()

    // 初始化 runtime.Registry 实例,保存所有的 Out-Of-tree 插件
	outOfTreeRegistry := make(runtime.Registry)
	for _, option := range outOfTreeRegistryOptions {
		if err := option(outOfTreeRegistry); err != nil {
			return nil, nil, err
		}
	}

	recorderFactory := getRecorderFactory(&cc)
	completedProfiles := make([]kubeschedulerconfig.KubeSchedulerProfile, 0)
	// Create the scheduler. 穿件调度器实例,用来运行所有的 kube scheduler 调度逻辑
	sched, err := scheduler.New(ctx,
		cc.Client,
		cc.InformerFactory,
		cc.DynInformerFactory,
		recorderFactory,
        // option 模式来配置
		scheduler.WithComponentConfigVersion(cc.ComponentConfig.TypeMeta.APIVersion),
		scheduler.WithKubeConfig(cc.KubeConfig),
		scheduler.WithProfiles(cc.ComponentConfig.Profiles...), // 自定义的调度策略
		scheduler.WithPercentageOfNodesToScore(cc.ComponentConfig.PercentageOfNodesToScore),
		scheduler.WithFrameworkOutOfTreeRegistry(outOfTreeRegistry),
		scheduler.WithPodMaxBackoffSeconds(cc.ComponentConfig.PodMaxBackoffSeconds),
		scheduler.WithPodInitialBackoffSeconds(cc.ComponentConfig.PodInitialBackoffSeconds),
		scheduler.WithPodMaxInUnschedulablePodsDuration(cc.PodMaxInUnschedulablePodsDuration),
		scheduler.WithExtenders(cc.ComponentConfig.Extenders...),
		scheduler.WithParallelism(cc.ComponentConfig.Parallelism),
		scheduler.WithBuildFrameworkCapturer(func(profile kubeschedulerconfig.KubeSchedulerProfile) {
			// Profiles are processed during Framework instantiation to set default plugins and configurations. Capturing them for logging
			completedProfiles = append(completedProfiles, profile)
		}),
	)
	if err != nil {
		return nil, nil, err
	}
	if err := options.LogOrWriteConfig(klog.FromContext(ctx), opts.WriteConfigTo, &cc.ComponentConfig, completedProfiles); err != nil {
		return nil, nil, err
	}

	return &cc, sched, nil
}

// pkg/scheduler/scheduler.go
type schedulerOptions struct {
	clock                  clock.WithTicker
	componentConfigVersion string
	kubeConfig             *restclient.Config
	// Overridden by profile level percentageOfNodesToScore if set in v1.
    // node 得分的百分比,如果在 v1 中设置了 profile 级别的 percentageOfNodesToScore,就会被覆盖
	percentageOfNodesToScore          int32
	podInitialBackoffSeconds          int64         // pod 初始退避时间
	podMaxBackoffSeconds              int64         // pod 最大退避时间
	podMaxInUnschedulablePodsDuration time.Duration // 最大不可调度的 pod 时间
	// Contains out-of-tree plugins to be merged with the in-tree registry.
    // 包含外部插件,用于和内部插件合并·
	frameworkOutOfTreeRegistry frameworkruntime.Registry
    // 调度器配置文件
	profiles                   []schedulerapi.KubeSchedulerProfile
    // 调度器扩展
	extenders                  []schedulerapi.Extender
    // 用于捕获调度框架的函数
	frameworkCapturer          FrameworkCapturer
    // 调度器的并行度
	parallelism                int32
    // 是否应用默认配置文件
	applyDefaultProfile        bool
}
调度插件
#

kube-scheduler 通过一系列的调度插件最终完成 pod 的调度。在启动 kube-scheduler 时,首先加载调度插件。对于调度插件,有 in-tree 和 out-of-tree 两种。

  • In-Tree:内建插件,作为 Kubernetes 核心组件的一部分直接编译和交付的, 与 Kubernetes 的源代码一起维护,并与 Kubernetes 版本保持同步。这些插件以静态库形式打包到 kubescheduler 二进制文件中,因此在使用时不需要单独安装和配置。一些常见的 in-tree 插件包括默认的调度算法、Packed Scheduling 等。
  • Out-of-tree:外部插件,作为独立项目开发和维护的,与 Kubernetes 核心代码分开,并且可以单独部署和更新。本质上,out-of-tree 插件是基于 Kubernetes 的调度器扩展点进行开发,这些插件以独立的二进制文件形式存在,并通过自定义的方式与 kube-scheduler 进行集成,需要单独安装和配置,并在 kube-scheduler 的配置中指定。

kube-scheduler 首先加载 out-of-tree 插件,在 main 文件中需要调用 app.NewSchedulerCommand() 来创建一个 scheduler application,示例。在调用 app.NewSchedulercommand 时,我们通过 app.WithPlugin option 模式,传入了我们期望加载到 kube-scheduler 中的 out-of-tree 插件。一般情况下:

  • 开发 out-of-tree 插件时,为了避免改动 kubernetes 源码仓库中的 kube-scheduler 源码,我们一般会另起一个项目,例如:scheduler-plugins。在新项目中我们调用 Kubernetes 源码仓库中的 app 包,来创建-个跟 kube-scheduler 完全一致的调度组件;
  • 创建应用时直接调用 k8s.io/kubernetes/cmd/kube-scheduler/app.NewSchedulerCommand(),所以创建的调度器可以和 kube-scheduler 保持兼容。

一个具体的外部实现可以参考 PodState 插件

对于 In-Tree 插件具体实现在 pkg/scheduler/framework/plugins/ 目录下。如 NodeName 插件的实现。

调度策略
#

调度策略用来执行具体的调度逻辑。kube-scheduler 在调度时,会选定一个调度策略,并使用该调度策略进行调度。同时,kube-scheduler 也支持自定义调度策略。 kube-scheduler 支持以下3种调度策略:

  • Scheduler Extender:社区最初提供的方案是通过 Extender 的形式来扩展 scheduler。Extender 是外部服务,支持 Filter、Preempt、Prioritize 和 Bind 的扩展,schneduler 运行到相应阶段时,通过调用 Extender 注册的 webhook 来运行扩展的逻辑,影响调度流程中各阶段的决策结果。

  • Multiple schedulers:在 Kubernetes 集群中其实类似于一个特殊的 Controller,通过监听 Pod和 Node 的信息,给 Pod 挑选最佳的节点、更新 Pod 的 spec,NodeName 的信息来将调度结果同步到节点所以对于部分有特殊的调度需求的用户,有些开发者通过自研 Custom Scheduler 来完成以上的流程,然后通过和 default-scheduler 同时部署的方式,来支持自己特殊的调度需求。在 Pod.Spec.SchedulerName 字段中,可以设置该 Pod 的调度策略,默认为: default;

  • Scheduling Framework:在原有的调度流程中,定义了丰富扩展点接口,开发者可以通过实现扩展点所定义的接口来实现插件,将插件注册到扩展点。Scheduling Framework 在执行调度流程时,运行到相应的扩展点时,会调用用户注册的插件,影响调度决策的结果。通过这种方式来将用户的调度逻辑集成到 Scheduling Framework 中。

调度队列管理
#

PriorityQueue 优先队列,包含三个子队列:activeQbackoffQunschedulableQ

  • activeQ:Scheduler 启动的时候所有等待被调度的 Pod 都会进入 activieQ,activeQ 按照 Pod 的 priority 进行排序,Scheduler Pipepline 从 activeQ 获取一个 Pod 并执行调度流程(Pipepline),当调度失败之后会直接根据情况选择进入 unschedulableQ 或者 backoffQ ,如果在当前 Pod 调度期间 Node Cache、Pod Cache 等 Scheduler Cache 有变化就进入 backoffQ,否则进入 unschedulableQ .

  • backoffQ:退避队列。持有从 unschedulablePods 中移走的 Pod,并将在其 backoff periods 退避期结束时移动到 activeQ 队列。Pod 在退避队列中等待并定期尝试进行重新调度。重新调度的频率会按照一个指数级的算法定时增加,从而充分探索可用的资源,直到找到可以分配给该Pod 的节点。

  • unschedulableQ:不可调度 Pod 的列表。持有已尝试进行调度且当前确定为不可调度的 Pod。

unschedulableQ 会定期较长时间(例如 60 秒)刷入 activeQ 或者 backoffQ,或者在 SchedulerCache 发生变化的时候触发关联的 Pod 刷入 activeQ 或者 backoffQ; backoffQ 会以 backoff 机制相比 unschedulableQ 比较快地让待调度的 Pod 进入 activeQ 进行重新调度。

调度 pod
#

当获取到一个待调度的 Pod 后,就会调用 sched.schedulingcycle 进入到调度循环中。schedulingCycle 函数执行 Pod 调度逻辑。 schedulingCycle 函数中,会按顺序依次执行以下调度扩展点: Prefilter、Filter、PostFilter、PreScore、Score、Reserve、Permit

调度引擎中,真正影响调度效果的是调度插件。行业中,针对不同的调度场景(例如:在线场景、离线场景、AI训练场景等),实现了不同的 kube-scheduler 调度插件,kube-scheduler 源码仓库中,也内嵌了一些常用的调度插件。

部分调度插件实现:

Related

性能优化
·7 mins