一 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资源对象寻找合适节点的过程就是一个调度周期。
三 启动流程
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")
}