1. kube-scheduler的设计
Scheduler在整个系统中承担了“承上启下”的重要功能。“承上”是指它负责接受Controller Manager创建的新Pod,为其安排Node;“启下”是指安置工作完成后,目标Node上的kubelet服务进程接管后续工作。Pod是Kubernetes中最小的调度单元,Pod被创建出来的工作流程如图所示:
在这张图中
- 第一步通过apiserver REST API创建一个Pod。
- 然后apiserver接收到数据后将数据写入到etcd中。
- 由于kube-scheduler通过apiserver watch API一直在监听资源的变化,这个时候发现有一个新的Pod,但是这个时候该Pod还没和任何Node节点进行绑定,所以kube-scheduler就进行调度,选择出一个合适的Node节点,将该Pod和该目标Node进行绑定。绑定之后再更新消息到etcd中。
- 这个时候一样的目标Node节点上的kubelet通过apiserver watch API检测到有一个新的Pod被调度过来了,他就将该Pod的相关数据传递给后面的容器运行时(container runtime),比如Docker,让他们去运行该Pod。
- 而且kubelet还会通过container runtime获取Pod的状态,然后更新到apiserver中,当然最后也是写入到etcd中去的。
通过这个流程我们可以看出整个过程中最重要的就是apiserver watch API和kube-scheduler的调度策略。
总之,kube-scheduler的功能是为还没有和任何Node节点绑定的Pods逐个地挑选最合适Pod的Node节点,并将绑定信息写入etcd中。整个调度流程分为,预选(Predicates)和优选(Priorities)两个步骤。
- 预选(Predicates):kube-scheduler根据预选策略(xxx Predicates)过滤掉不满足策略的Nodes。例如,官网中给的例子node3因为没有足够的资源而被剔除。
- 优选(Priorities):优选会根据优先策略(xxx Priority)为通过预选的Nodes进行打分排名,选择得分最高的Node。例如,资源越富裕、负载越小的Node可能具有越高的排名。
2. kube-scheduler 源码分析
kubernetes 版本: v1.21
2.1 scheduler.New()
初始化scheduler结构体
在程序的入口,是通过一个runCommand函数来唤醒scheduler的操作的。首先会进入Setup函数,它会根据命令参数和选项创建一个完整的config和scheduler。创建scheduler的方式就是使用New函数。
Scheduler结构体:
- SchedulerCache:通过SchedulerCache做出的改变将被NodeLister和Algorithm观察到。
- NextPod :应该是一个阻塞直到下一个 Pod存在的函数。之所以不使用channel结构,是因为调度 pod 可能需要一些时间,k8s不希望 pod 位于通道中变得陈旧。
- Error:在出现错误的时候被调用。如果有错误,它会传递有问题的 pod信息,和错误。
- StopEverything:通过关闭它来停止scheduler。
- SchedulingQueue:保存着正在准备被调度的pod列表。
- Profiles:调度的策略。
scheduler.New() 方法是初始化 scheduler 结构体的,该方法主要的功能是初始化默认的调度算法以及默认的调度器 GenericScheduler。
-
创建 scheduler 配置文件
-
根据默认的 DefaultProvider 初始化
schedulerAlgorithmSource
然后加载默认的预选及优选算法,然后初始化GenericScheduler
-
若启动参数提供了 policy config 则使用其覆盖默认的预选及优选算法并初始化
GenericScheduler
,不过该参数现已被弃用kubernetes/pkg/scheduler/scheduler.go:189
// New函数创建一个新的scheduler
func New(client clientset.Interface, informerFactory informers.SharedInformerFactory,recorderFactory profile.RecorderFactory, stopCh <-chan struct{
},opts ...Option) (*Scheduler, error) {
//查看并设置传入的参数
……
snapshot := internalcache.NewEmptySnapshot()
// 创建scheduler的配置文件
configurator := &Configurator{
……}
metrics.Register()
var sched *Scheduler
source := options.schedulerAlgorithmSource
switch {
case source.Provider != nil:
// 根据Provider创建config
sc, err := configurator.createFromProvider(*source.Provider)
……
case source.Policy != nil:
// 根据用户指定的策略(policy source)创建config
// 既然已经设置了策略,在configuation内设置extender为nil
// 如果没有,从Configuration的实例里设置extender
configurator.extenders = policy.Extenders
sc, err := configurator.createFromConfig(*policy)
……
}
// 对配置器生成的配置进行额外的调整
sched.StopEverything = stopEverything
sched.client = client
addAllEventHandlers(sched, informerFactory)
return sched, nil
}
在New函数里提供了两种初始化scheduler的方式,一种是 source.Provider,一种是source.Policy,最后生成的config信息都会通过sched = sc
创建新的调度器。Provider方法对应的是createFromProvider
函数,Policy方法对应的是createFromConfig
函数,最后它们都会调用Create函数,实例化podQueue,返回配置好的Scheduler结构体。
2.2 Run()
启动主逻辑
kubernetes 中所有组件的启动流程都是类似的,首先会解析命令行参数、添加默认值,kube-scheduler 的默认参数在 k8s.io/kubernetes/pkg/scheduler/apis/config/v1alpha1/defaults.go
中定义的。然后会执行 run 方法启动主逻辑,下面直接看 kube-scheduler 的主逻辑 run 方法执行过程。
Run()
方法主要做了以下工作:
-
配置了Configz参数
-
启动事件广播器,健康检测服务,http server
-
启动所有的 informer
-
执行
sched.Run()
方法,执行主调度逻辑kubernetes/cmd/kube-scheduler/app/server.go:136
// Run 函数根据指定的配置执行调度程序。当出现错误或者上下文完成的时候才会返回。
func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *scheduler.Scheduler) error {
// 为了帮助debug,先记录Kubernetes的版本号
klog.V(1).Infof("Starting Kubernetes Scheduler version %+v", version.Get())
// 1、配置Configz
if cz, err := configz.New("componentconfig"); err == nil {
……}
// 2、准备事件广播管理器,此处涉及到Events事件
cc.EventBroadcaster.StartRecordingToSink(ctx.Done())
// 3、启动 http server,进行健康监控服务器监听
if cc.InsecureServing != nil {
……}
if cc.InsecureMetricsServing != nil {
……}
if cc.SecureServing != nil {
……}
// 4、启动所有 informer
cc.InformerFactory.Start(ctx.Done())
// 等待所有的缓存同步后再进行调度。
cc.InformerFactory.WaitForCacheSync(ctx.Done())
// 5、因为Master节点可以存在多个,选举一个作为Leader。通过 LeaderElector 运行命令直到完成并退出。
if cc.LeaderElection != nil {
cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
close(waitingForLeader)
// 6、执行 sched.Run() 方法,执行主调度逻辑
sched.Run(ctx)
},
// 钩子函数,开启Leading时运行调度,结束时打印报错
OnStoppedLeading: func() {
klog.Fatalf("leaderelection lost")
},
}
leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection)
// 参加选举的会持续通信
leaderElector.Run(ctx)
return fmt.Errorf("lost lease")
}
// 领导者选举失败,所以runCommand函数会一直运行直到完成
close(waitingForLeader)
// 6、执行 sched.Run() 方法,执行主调度逻辑
sched.Run(ctx)
return fmt.Errorf("finished without leader elect")
}
- 这里相比16版本增加了一个
waitingForLeader
的channel用来监听信号 - Setup函数中提到了Informer。k8s中有各种类型的资源,包括自定义的。而Informer的实现就将调度和资源结合了起来。pod informer 的启动逻辑是,只监听
status.phase
不为 succeeded 以及 failed 状态的 pod,即非 terminating 的 pod。
2.3 sched.Run()
开始监听和调度
然后继续看 Run()
方法中最后执行的 sched.Run()
调度循环逻辑,若 informer 中的 cache 同步完成后会启动一个循环逻辑执行 sched.scheduleOne
方法。
kubernetes/pkg/scheduler/scheduler.go:313
// Run函数开始监视和调度。SchedulingQueue开始运行。一直处于调度状态直到Context完成一直阻塞。
func (sched *Scheduler) Run(ctx context.Context) {
sched.SchedulingQueue.Run()
wait.UntilWithContext(ctx, sched.scheduleOne, 0)
sched.SchedulingQueue.Close()
}
sched.SchedulingQueue.Run()
:会将backoffQ中的Pods节点和unschedulableQ中的节点移至activeQ中。即将之前运行失败的节点和已经等待了很长时间超过时间设定的节点重新进入活跃节点队列中。- backoffQ 是并发编程中常见的一种机制,就是如果一个任务重复执行,但依旧失败,则会按照失败的次数提高重试等待时间,避免频繁重试浪费资源。
sched.SchedulingQueue.Close()
,关闭调度之后,对队列也进行关闭。SchedulingQueue是一个优先队列。- 优先作为实现SchedulingQueue的实现,其核心数据结构主要包含三个队列:activeQ、podBackoffQ、unschedulableQ内部通过cond来实现Pop操作的阻塞与通知。当前队列中没有可调度的pod的时候,则通过cond.Wait来进行阻塞,然后在往activeQ中添加pod的时候通过cond.Broadcast来实现通知。
wait.UntilWithContext()
中出现了sched.scheduleOne函数,它负责了为单个 Pod 执行整个调度工作流程,也是本次研究的重点,接下来将会详细地进行分析。
2.4 scheduleOne()
分配pod的流程
scheduleOne()
每次对一个 pod 进行调度,主要有以下步骤:
-
从 scheduler 调度队列中取出一个 pod,如果该 pod 处于删除状态则跳过
-
执行调度逻辑
sched.schedule()
返回通过预算及优选算法过滤后选出的最佳 node -
如果过滤算法没有选出合适的 node,则返回 core.FitError
-
若没有合适的 node 会判断是否启用了抢占策略,若启用了则执行抢占机制
-
执行 reserve plugin
-
pod 对应的 spec.NodeName 写上 scheduler 最终选择的 node,更新 scheduler cache
-
执行 permit plugin
-
执行 prebind plugin
-
进行绑定,请求 apiserver 异步处理最终的绑定操作,写入到 etcd
-
执行 postbind plugin
kubernetes/pkg/scheduler/scheduler.go:441
- 准备工作
// scheduleOne为单个pod做整个调度工作流程。它被序列化在调度算法的主机拟合上。
func (sched *Scheduler) scheduleOne(ctx context.Context) {
// podInfo就是从队列中获取到的Pod对象
podInfo := sched.NextPod()