一 Scheduler简介

kube-scheduler组件是Kubernetes系统的核心组件之一,主要负责整个集群Pod资源对象的调度,根据内置或扩展的调度算法(预选与优选调度算法),将未调度的Pod资源对象调度到最优的工作节点上,从而更加合理、更加充分地利用集群的资源。

二 Sheduler架构

kube-scheduler是Kubernetes的默认调度器,其架构设计本身并不复杂,但Kubernetes系统在后期引入了优先级和抢占机制及亲和性调度等功能。

kube-scheduler调度器在为Pod资源对象选择合适节点时,有如下两种最优解。
● 全局最优解:是指每个调度周期都会遍历Kubernetes集群中的所有节点,以便找出全局最优的节点。
● 局部最优解:是指每个调度周期只会遍历部分Kubernetes集群中的节点,找出局部最优的节点。

全局最优解和局部最优解可以解决调度器在小型和大型Kubernetes集群规模上的性能问题。目前kube-scheduler调度器对两种最优解都支持。当集群中只有几百台主机时,例如100台主机,kube-scheduler使用全局最优解。当集群规模较大时,例如其中包含5000多台主机,kube-scheduler使用局部最优解。

kube-scheduler组件的主要逻辑在于,如何在Kubernetes集群中为一个Pod资源对象找到合适的节点。调度器每次只调度一个Pod资源对象,为每一个Pod资源对象寻找合适节点的过程就是一个调度周期。

kubesphere按照mysql8 kube schedule_源码解读

三 启动流程

kubesphere按照mysql8 kube schedule_scheduler_02

1.内置调度算法的注册

kube-scheduler组件启动后的第一件事情是,将Kubernetes内置的调度算法注册到调度算法注册表中。调度算法注册表与Scheme资源注册表类似,都是通过map数据结构存放的
调度算法分为两类,第一类是预选调度算法,第二类是优选调度算法。两类调度算法都存储在调度算法注册表中,该表由3个map数据结构构成

// path:kubernetes/pkg/scheduler/factory/plugins.go
type Snapshot struct {
	fitPredicateMap        map[string]FitPredicateFactory //存储所有的预选调度算法
	mandatoryFitPredicates sets.String
	priorityFunctionMap    map[string]PriorityConfigFactory //存储所有的优选调度算法
	algorithmProviderMap   map[string]AlgorithmProviderConfig //存储所有类型的调度算法
}

内置调度算法的注册过程与kube-apiserver资源的注册过程类似,它们都通过Go语言的导入和初始化机制触发。当引用k8s.io/kubernetes/pkg/scheduler/algorithmprovider包时,就会自动调用包下的init初始化函数

//path:kubernetes/pkg/scheduler/algorithmprovider/defaults/defaults.go
func init() {
	registerAlgorithmProvider(defaultPredicates(), defaultPriorities())
}

func registerAlgorithmProvider(predSet, priSet sets.String) {
	//注册算法提供者。 默认情况下,我们使用“ DefaultProvider”,但用户可以通过指定标志来指定要使用的一个。
	factory.RegisterAlgorithmProvider(factory.DefaultProvider, predSet, priSet)
	//集群自动缩放器调度算法
	factory.RegisterAlgorithmProvider(ClusterAutoscalerProvider, predSet,
		copyAndReplace(priSet, priorities.LeastRequestedPriority, priorities.MostRequestedPriority))
}


//path:kubernetes/pkg/scheduler/factory/defaults.go
//RegisterAlgorithmProvider使用算法注册表注册新的算法提供程序。 应该从提供程序插件中的init函数调用此函数。
func RegisterAlgorithmProvider(name string, predicateKeys, priorityKeys sets.String) string {
	schedulerFactoryMutex.Lock()
	defer schedulerFactoryMutex.Unlock()
	//验证算法名称或模具
	validateAlgorithmNameOrDie(name)
	//插入map
	algorithmProviderMap[name] = AlgorithmProviderConfig{
		FitPredicateKeys:     predicateKeys,
		PriorityFunctionKeys: priorityKeys,
	}
	return name
}

registerAlgorithmProvider函数负责预选调度算法集(defaultPredicates)和优选调度算法集(defaultPriorities)的注册。通过factory.RegisterAlgorithmProvider将两类调度算法注册至algorithmProviderMap中

2.Cobra命令行参数解析

首先kube-scheduler组件通过options.NewOptions函数初始化各个模块的默认配置,例如HTTP或HTTPS服务等。然后通过Validate函数验证配置参数的合法性和可用性,并通过Complete函数填充默认的options配置参数。最后将cc(kube-scheduler组件的运行配置)对象传入Run函数,Run函数定义了kube-scheduler组件启动的逻辑,它是一个运行不退出的常驻进程。至此,完成了kube-scheduler组件启动之前的环境配置

//path: kubernetes/cmd/kube-scheduler/app/server.go
func NewSchedulerCommand(registryOptions ...Option) *cobra.Command {
	opts, err := options.NewOptions()
	if err != nil {
		klog.Fatalf("unable to initialize command options: %v", err)
	}

	cmd := &cobra.Command{
		Use: "kube-scheduler",
		Long: 
		...
		Run: func(cmd *cobra.Command, args []string) {
			if err := runCommand(cmd, args, opts, registryOptions...); err != nil {
				fmt.Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}
		},
	}
	fs := cmd.Flags()
	namedFlagSets := opts.Flags()
	verflag.AddFlags(namedFlagSets.FlagSet("global"))
	globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name())
	for _, f := range namedFlagSets.FlagSets {
	//AddFlagSet将一个FlagSet添加到另一个。 如果f中已经存在一个标志,则newSet中的标志将被忽略。
		fs.AddFlagSet(f)
	}

	usageFmt := "Usage:\n  %s\n"
	cols, _, _ := term.TerminalSize(cmd.OutOrStdout())
	//SetUsageFunc设置使用功能。 用法可以由应用程序定义。
	cmd.SetUsageFunc(func(cmd *cobra.Command) error {
		fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine())
		cliflag.PrintSections(cmd.OutOrStderr(), namedFlagSets, cols)
		return nil
	})
	// 自定义帮助信息
	cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
		fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine())
		cliflag.PrintSections(cmd.OutOrStdout(), namedFlagSets, cols)
	})
	//MarkFlagFilename将BashCompFilenameExt注释添加到命名标志(如果存在)。生成的bash自动补全将为该标志选择文件名,如果提供,则限制为命名扩展。
	cmd.MarkFlagFilename("config", "yaml", "yml", "json")

	return cmd
}

func runCommand(cmd *cobra.Command, args []string, opts *options.Options, registryOptions ...Option) error {
	//是否是查看版本的flag
	verflag.PrintAndExitIfRequested()
	utilflag.PrintFlags(cmd.Flags())

	if len(args) != 0 {
		fmt.Fprint(os.Stderr, "arguments are not supported\n")
	}

	if errs := opts.Validate(); len(errs) > 0 {
		fmt.Fprintf(os.Stderr, "%v\n", utilerrors.NewAggregate(errs))
		os.Exit(1)
	}
	...
	cc := c.Complete()
	...
	return Run(cc, stopCh, registryOptions...)
}

3.实例化Scheduler对象

Scheduler对象是运行kube-scheduler组件的主对象,它包含了kube-scheduler组件运行过程中的所有依赖模块对象。Scheduler对象的实例化过程可分为3部分:

//path: kubernetes/cmd/kube-scheduler/app/server.go
sched, err := scheduler.New(cc.Client,
	cc.InformerFactory.Core().V1().Nodes(),
	cc.PodInformer,
	cc.InformerFactory.Core().V1().PersistentVolumes(),
	cc.InformerFactory.Core().V1().PersistentVolumeClaims(),
	cc.InformerFactory.Core().V1().ReplicationControllers(),
	cc.InformerFactory.Apps().V1().ReplicaSets(),
	cc.InformerFactory.Apps().V1().StatefulSets(),
	cc.InformerFactory.Core().V1().Services(),
	cc.InformerFactory.Policy().V1beta1().PodDisruptionBudgets(),
	cc.InformerFactory.Storage().V1().StorageClasses(),
	cc.InformerFactory.Storage().V1beta1().CSINodes(),
	cc.Recorder,
	cc.ComponentConfig.AlgorithmSource,
	stopCh,
	registry,
	cc.ComponentConfig.Plugins,
	cc.ComponentConfig.PluginConfig,
	scheduler.WithName(cc.ComponentConfig.SchedulerName),
	scheduler.WithHardPodAffinitySymmetricWeight(cc.ComponentConfig.HardPodAffinitySymmetricWeight),
	scheduler.WithPreemptionDisabled(cc.ComponentConfig.DisablePreemption),
	scheduler.WithPercentageOfNodesToScore(cc.ComponentConfig.PercentageOfNodesToScore),
	scheduler.WithBindTimeoutSeconds(*cc.ComponentConfig.BindTimeoutSeconds))
if err != nil {
	return err
}
  • 第1部分,实例化所有的Informer;

kube-scheduler组件依赖于多个资源的Informer对象,用于监控相应资源对象的事件。例如,通过PodInformer监控Pod资源对象,当某个Pod被创建时,kube-scheduler组件监控到该事件并为该Pod根据调度算法选择出合适的节点(Node)。在Scheduler对象的实例化过程中,对NodeInformer、PodInformer、PersistentVolumeInformer、PersistentVolumeClaimInformer、ReplicationControllerInformer、ReplicaSetInformer、StatefulSetInformer、ServiceInformer、PodDisruptionBudgetInformer、StorageClassInformer资源通过Informer进行监控

  • 第2部分,实例化调度算法函数;
//SchedulerAlgorithmSource是调度程序算法的源。 必须指定一个源字段,并且源字段是互斥的。
type SchedulerAlgorithmSource struct {
	//策略是基于策略的算法源。
	Policy *SchedulerPolicySource
	//提供者是要使用的调度算法提供者的名称。
	Provider *string
}

在前面的章节中,内置调度算法的注册过程中只注册了调度算法的名称,在此处,为已经注册名称的调度算法实例化对应的调度算法函数,有两种方式实例化调度算法函数,它们被称为调度算法源(Scheduler
Algorithm Source)
● Policy:通过定义好的Policy(策略)资源的方式实例化调度算法函数。该方式可通过–policy-config-file参数指定调度策略文件。
● Provider:通用调度器,通过名称的方式实例化调度算法函数,这也是kube-scheduler的默认方式。

  • 第3部分,为所有Informer对象添加对资源事件的监控。

AddAllEventHandlers函数为所有Informer对象添加对资源事件的监控并设置回调函数,以podInformer为例

//path: kubernetes/pkg/scheduler/eventhandlers.go
podInformer.Informer().AddEventHandler(
		cache.FilteringResourceEventHandler{
			...
			Handler: cache.ResourceEventHandlerFuncs{
				AddFunc:    sched.addPodToCache,
				UpdateFunc: sched.updatePodInCache,
				DeleteFunc: sched.deletePodFromCache,
			},
		},
	)

podInformer对象监控Pod资源对象,当该资源对象触发Add(添加)、Update (更新)、Delete(删除)事件时,触发对应的回调函数。例如,在触发Add事件后,podInformer将其放入SchedulingQueue调度队列中,等待kube-scheduler调度器为该Pod资源对象分配节点

4.运行EventBroadcaster事件管理器

Kubernetes的事件(Event)是一种资源对象(Resource Object),用于展示集群内发生的情况,kube-scheduler组件会将运行时产生的各种事件上报给Kubernetes API Server。例如,调度器做了什么决定,为什么从节点中驱逐某些Pod资源对象等。可以通过kubectl get event或kubectldescribe pod 命令显示事件,用于查看Kubernetes集群中发生了哪些事件,这些命令只会显示最近(1小时内)发生的事件。更多关于EventBroadcaster事件管理器的内容,请参考5.5节“EventBroadcaster事件管理器”。

//kubernetes/cmd/kube-scheduler/app/server.go
//准备事件广播
if cc.Broadcaster != nil && cc.EventClient != nil {
	cc.Broadcaster.StartRecordingToSink(stopCh)
}
if cc.CoreBroadcaster != nil && cc.CoreEventClient != nil {
	cc.CoreBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: cc.CoreEventClient.Events("")})
}

cc.Broadcaster通过StartLogging自定义函数将事件输出至klog stdout标准输出,通过StartRecordingToSink自定义函数将关键性事件上报给Kubernetes API Server

5.运行HTTP或HTTPS服务

kube-scheduler组件也拥有自己的HTTP服务,但功能仅限于监控及监控检查等,其运行原理与kube-apiserver组件的类似,故不再赘述。kube-apiserver HTTP服务提供了如下几个重要接口。
●/healthz:用于健康检查。

//健康检查
//path:kubernetes/cmd/kube-scheduler/app/server.go
if cc.InsecureServing != nil {
	separateMetrics := cc.InsecureMetricsServing != nil
	handler := buildHandlerChain(newHealthzHandler(&cc.ComponentConfig, separateMetrics, checks...), nil, nil)
	if err := cc.InsecureServing.Serve(handler, 0, stopCh); err != nil {
		return fmt.Errorf("failed to start healthz server: %v", err)
	}
}

●/metrics:用于监控指标,一般用于Prometheus指标采集。

//path:kubernetes/cmd/kube-scheduler/app/server.go
	if cc.InsecureMetricsServing != nil {
		handler := buildHandlerChain(newMetricsHandler(&cc.ComponentConfig), nil, nil)
		if err := cc.InsecureMetricsServing.Serve(handler, 0, stopCh); err != nil {
			return fmt.Errorf("failed to start metrics server: %v", err)
		}
	}

●/debug/pprof:用于pprof性能分析

//path:kubernetes/cmd/kube-scheduler/app/server.go
if cc.SecureServing != nil {
	handler := buildHandlerChain(newHealthzHandler(&cc.ComponentConfig, false, checks...), cc.Authentication.Authenticator, cc.Authorization.Authorizer)
	// TODO: handle stoppedCh returned by c.SecureServing.Serve
	if _, err := cc.SecureServing.Serve(handler, 0, stopCh); err != nil {
		// fail early for secure handlers, removing the old error loop from above
		return fmt.Errorf("failed to start secure server: %v", err)
	}
}

6.运行Informer同步资源

//path:kubernetes/cmd/kube-scheduler/app/server.go
	//启动informer
	go cc.PodInformer.Informer().Run(stopCh)
	cc.InformerFactory.Start(stopCh)

	// Wait for all caches to sync before scheduling.
	//在调度之前,请等待所有缓存同步
	cc.InformerFactory.WaitForCacheSync(stopCh)

通过Informer监控NodeInformer、PodInformer、PersistentVolumeInformer、PersistentVolumeClaimInformer、ReplicationControllerInformer、ReplicaSetInformer、StatefulSetInformer、ServiceInformer、PodDisruptionBudgetInformer、StorageClassInformer资源。在正式启动Scheduler调度器之前,须通过cc.InformerFactory.WaitForCacheSync函数等待所有运行中的Informer的数据同步,使本地缓存数据与Etcd集群中的数据保持一致

7.领导者选举实例化

领导者选举机制的目的是实现Kubernetes组件的高可用(High Availability)。在领导者选举实例化的过程中,会定义Callbacks函数

//如果启用了领导者选举,请通过LeaderElector运行Command,直到完成并退出
if cc.LeaderElection != nil {
	cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
		OnStartedLeading: run,
		OnStoppedLeading: func() {
			klog.Fatalf("leaderelection lost")
		},
	}
	leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection)
	if err != nil {
		return fmt.Errorf("couldn't create leader elector: %v", err)
	}

	leaderElector.Run(ctx)

	return fmt.Errorf("lost lease")
}

// Leader election is disabled, so runCommand inline until done.
//领导者选举已禁用,因此内联运行命令直到完成
run(ctx)
return fmt.Errorf("finished without leader elect")

LeaderCallbacks中定义了两个回调函数:OnStartedLeading函数是当前节点领导者选举成功后回调的函数,该函数定义了kube-scheduler组件的主逻辑;OnStoppedLeading函数是当前节点领导者被抢占后回调的函数,在领导者被抢占后,会退出当前的kube-scheduler进程。通过leaderelection.NewLeaderElector函数实例化LeaderElector对象,通过leaderElector.Run函数参与领导者选举,该函数会一直尝试使节点成为领导者

8.运行sched.Run调度器

在正式运行kube-scheduler组件的主逻辑之前,通过sched.config.WaitForCacheSync函数再次确认,所有运行中的Informer的数据是否已同步到本地

//path: kubernetes/cmd/kube-scheduler/app/server.go
	//准备可重用的runCommand函数
	run := func(ctx context.Context) {
		sched.Run()
		<-ctx.Done()
	}
	
//path: kubernetes/pkg/scheduler/scheduler.go
//运行开始监视和调度。 它等待缓存同步,然后启动goroutine并立即返回。
func (sched *Scheduler) Run() {
	if !sched.WaitForCacheSync() {
		return
	}

	go wait.Until(sched.scheduleOne, 0, sched.StopEverything)
}

sched.scheduleOne是kube-scheduler组件的调度主逻辑,它通过wait.Until定时器执行,内部会定时调用sched.scheduleOne函数,当sched.config.StopEverything Chan关闭时,该定时器才会停止并退出。

9.Run代码

//运行根据给定的配置执行调度程序。 它仅在出错时或在stopCh关闭时返回。
func Run(cc schedulerserverconfig.CompletedConfig, stopCh <-chan struct{}, registryOptions ...Option) error {
	// To help debugging, immediately log version
	klog.V(1).Infof("Starting Kubernetes Scheduler version %+v", version.Get())

	registry := framework.NewRegistry()
	for _, option := range registryOptions {
		if err := option(registry); err != nil {
			return err
		}
	}

	// Prepare event clients.
	//准备event客户端
	if _, err := cc.Client.Discovery().ServerResourcesForGroupVersion(eventsv1beta1.SchemeGroupVersion.String()); err == nil {
		cc.Broadcaster = events.NewBroadcaster(&events.EventSinkImpl{Interface: cc.EventClient.Events("")})
		cc.Recorder = cc.Broadcaster.NewRecorder(scheme.Scheme, cc.ComponentConfig.SchedulerName)
	} else {
		recorder := cc.CoreBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: cc.ComponentConfig.SchedulerName})
		cc.Recorder = record.NewEventRecorderAdapter(recorder)
	}

	// Create the scheduler.
	//创建调度器
	sched, err := scheduler.New(cc.Client,
		cc.InformerFactory.Core().V1().Nodes(),
		cc.PodInformer,
		cc.InformerFactory.Core().V1().PersistentVolumes(),
		cc.InformerFactory.Core().V1().PersistentVolumeClaims(),
		cc.InformerFactory.Core().V1().ReplicationControllers(),
		cc.InformerFactory.Apps().V1().ReplicaSets(),
		cc.InformerFactory.Apps().V1().StatefulSets(),
		cc.InformerFactory.Core().V1().Services(),
		cc.InformerFactory.Policy().V1beta1().PodDisruptionBudgets(),
		cc.InformerFactory.Storage().V1().StorageClasses(),
		cc.InformerFactory.Storage().V1beta1().CSINodes(),
		cc.Recorder,
		cc.ComponentConfig.AlgorithmSource,
		stopCh,
		registry,
		cc.ComponentConfig.Plugins,
		cc.ComponentConfig.PluginConfig,
		scheduler.WithName(cc.ComponentConfig.SchedulerName),
		scheduler.WithHardPodAffinitySymmetricWeight(cc.ComponentConfig.HardPodAffinitySymmetricWeight),
		scheduler.WithPreemptionDisabled(cc.ComponentConfig.DisablePreemption),
		scheduler.WithPercentageOfNodesToScore(cc.ComponentConfig.PercentageOfNodesToScore),
		scheduler.WithBindTimeoutSeconds(*cc.ComponentConfig.BindTimeoutSeconds))
	if err != nil {
		return err
	}

	// Prepare the event broadcaster.
	//准备事件广播
	if cc.Broadcaster != nil && cc.EventClient != nil {
		cc.Broadcaster.StartRecordingToSink(stopCh)
	}
	if cc.CoreBroadcaster != nil && cc.CoreEventClient != nil {
		cc.CoreBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: cc.CoreEventClient.Events("")})
	}
	// Setup healthz checks.
	//设置运行状况检查
	var checks []healthz.HealthChecker
	if cc.ComponentConfig.LeaderElection.LeaderElect {
		checks = append(checks, cc.LeaderElection.WatchDog)
	}

	// Start up the healthz server.
	//健康检查
	if cc.InsecureServing != nil {
		separateMetrics := cc.InsecureMetricsServing != nil
		handler := buildHandlerChain(newHealthzHandler(&cc.ComponentConfig, separateMetrics, checks...), nil, nil)
		if err := cc.InsecureServing.Serve(handler, 0, stopCh); err != nil {
			return fmt.Errorf("failed to start healthz server: %v", err)
		}
	}
	if cc.InsecureMetricsServing != nil {
		handler := buildHandlerChain(newMetricsHandler(&cc.ComponentConfig), nil, nil)
		if err := cc.InsecureMetricsServing.Serve(handler, 0, stopCh); err != nil {
			return fmt.Errorf("failed to start metrics server: %v", err)
		}
	}
	if cc.SecureServing != nil {
		handler := buildHandlerChain(newHealthzHandler(&cc.ComponentConfig, false, checks...), cc.Authentication.Authenticator, cc.Authorization.Authorizer)
		// TODO: handle stoppedCh returned by c.SecureServing.Serve
		if _, err := cc.SecureServing.Serve(handler, 0, stopCh); err != nil {
			// fail early for secure handlers, removing the old error loop from above
			return fmt.Errorf("failed to start secure server: %v", err)
		}
	}

	// Start all informers.
	//启动informer
	go cc.PodInformer.Informer().Run(stopCh)
	cc.InformerFactory.Start(stopCh)

	// Wait for all caches to sync before scheduling.
	//在调度之前,请等待所有缓存同步
	cc.InformerFactory.WaitForCacheSync(stopCh)

	// Prepare a reusable runCommand function.
	//准备可重用的runCommand函数
	run := func(ctx context.Context) {
		sched.Run()
		<-ctx.Done()
	}

	ctx, cancel := context.WithCancel(context.TODO()) // TODO once Run() accepts a context, it should be used here
	defer cancel()

	go func() {
		select {
		case <-stopCh:
			cancel()
		case <-ctx.Done():
		}
	}()

	// If leader election is enabled, runCommand via LeaderElector until done and exit.
	//如果启用了领导者选举,请通过LeaderElector运行Command,直到完成并退出
	if cc.LeaderElection != nil {
		cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
			OnStartedLeading: run,
			OnStoppedLeading: func() {
				klog.Fatalf("leaderelection lost")
			},
		}
		leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection)
		if err != nil {
			return fmt.Errorf("couldn't create leader elector: %v", err)
		}

		leaderElector.Run(ctx)

		return fmt.Errorf("lost lease")
	}

	// Leader election is disabled, so runCommand inline until done.
	//领导者选举已禁用,因此内联运行命令直到完成
	run(ctx)
	return fmt.Errorf("finished without leader elect")
}