调度逻辑-schedulerOne

在本节主要讲述schedulerOne的基本调度流程,其中用到的预选调度策略、优选调度策略将在后几节介绍。

scheduler源码存放的另一个文件夹:

/pkg/scheduler/,包含scheduler的核心代码,执行scheduler的调度逻辑。文件机构如下:

scheduler
├── algorithm     # 包含scheduler的调度算法
│   ├── BUILD
│   ├── doc.go
│   ├── predicates     # 预选策略
│   ├── priorities     # 优选策略
│   ├── scheduler_interface.go     # 定义SchedulerExtender接口
│   └── types.go
├── algorithm_factory.go
├── algorithm_factory_test.go
├── algorithmprovider
│   ├── BUILD
│   ├── defaults     # 初始化默认算法,包括预选和优选策略
│   ├── plugins.go     # 通过feature gates 应用算法
│   ├── plugins_test.go
│   ├── registry.go     # 注册表:是所有可用的algorithm providers的集合
│   └── registry_test.go
├── api
│   ├── BUILD
│   └── well_known_labels.go     # 外部云供应商在node上设置taint
├── apis
│   ├── config     # kubescheduler调度逻辑所需要的配置
│   └── extender
├── BUILD
├── core     # scheduler的核心代码
│   ├── BUILD
│   ├── extender.go     # 用户配置extender的一系列函数
│   ├── extender_test.go
│   ├── generic_scheduler.go     # 包含调度器的调度逻辑,后面会详细介绍
│   └── generic_scheduler_test.go
├── eventhandlers.go
├── eventhandlers_test.go
├── factory.go
├── factory_test.go
├── framework     # framework框架
│   ├── BUILD
│   ├── plugins     # 包含一系列的plugins,用于校验和过滤
│   └── v1alpha1     # framework的架构设计,包括配置参数及调用plugins
├── internal     # scheduler调度时,使用到的内部资源或工具
│   ├── cache     # scheduler 缓存
│   ├── heap     # heap 主要作用于items
│   └── queue     # 队列,在pod调度前,在队列中排序,通过pop推出pod进行调度
├── listers     # 获取pod和node的信息及表单list
│   ├── BUILD
│   ├── fake
│   ├── listers.go
│   └── listers_test.go
├── metrics     # 一系列指标,与prometheus交互使用
│   ├── BUILD
│   ├── metric_recorder.go
│   ├── metric_recorder_test.go
│   └── metrics.go
├── nodeinfo     # 节点信息,包括节点上已有的pod、volume、taint、resource等
│   ├── BUILD
│   ├── host_ports.go
│   ├── host_ports_test.go
│   ├── node_info.go
│   ├── node_info_test.go
│   └── snapshot    # 为节点创建快照
├── OWNERS
├── scheduler.go     # sched.Run的入口函数及schedulerOne的调度逻辑
├── scheduler_test.go
├── testing     # 测试
│   ├── BUILD
│   ├── framework_helpers.go
│   ├── workload_prep.go
│   └── wrappers.go
├── util
│   ├── BUILD
│   ├── clock.go
│   ├── error_channel.go
│   ├── error_channel_test.go
│   ├── utils.go
│   └── utils_test.go
└── volumebinder     # 绑定 volume
    ├── BUILD
    └── volume_binder.go

1. sched.Run

代码在pkg/scheduler/scheduler.go

等待缓存同步后,开始scheduler调度逻辑,此处为具体调度逻辑的入口,代码如下:

第3行,调用cache.WaitForCacheSync函数,遍历cacheSyncs并使用bool值来判断同步是否完成,代码在staging/src/k8s.io/client-go/tools/cache/shared_informer.go,如下:

在同一文件下,controller.WaitForNamedCacheSync是对cache.WaitForCacheSync的一层封装,当cache.WaitForCacheSync函数同步失败时,找出不能同步缓存的cotroller名称,记录不同cotroller的缓存同步的日志。

controller.WaitForNamedCacheSync的代码如下:

第7行sched.scheduleOne,scheduler的调度逻辑,在标题2详细介绍。

第六行sched.SchedulingQueue.Run(),使用队列存储pod,并开启goroutines管理队列

第八行sched.SchedulingQueue.Close(),关闭SchedulerQueue,等待队列 pop items,goroutine正常退出。

这里与queue相关的函数和接口在/pkg/scheduler/internal/queue/scheduling_queue.go

2. sched.scheduleOne

代码在/pkg/scheduler/scheduler.go

scheduleOne()函数是对pod调度的完整逻辑:

  1. 先从队列中使用pop方式弹出需要调度的pod

  2. 通过具体的调度策略(预选和优选策略)为该pod筛选出合适的节点

  3. 如果上述调度策略失败,则执行抢占式调度逻辑,经过这一系列的操作后选出最佳节点。

  4. 将pod设置为assumed状态(与选中的节点进行假性绑定),并执行reserve操作(在选中的节点上为该pod预留资源)

  5. 在假性绑定后,继续调度其他的pod。这里注意:pod的调度是同步的,即同一时间只能调度一个pod;而在pod的绑定阶段是异步的,即同一时间可以同时绑定多个pod

  6. 调用sched.bind()函数,执行真实绑定,将pod调度到选中的节点上,交给kubelet进行初始化和管理。

  7. 绑定结果返回后,将通知cache该pod的绑定已经结束并将结果反馈给apiserver;如果绑定失败,从缓存中删除该pod,并触发unreserve机制,释放节点中为该pod预留的资源。

在这个调度逻辑中,还穿插着不同的plugins对pod与node进行过滤,包括QueueSortPlugin、scorePlugins、reservePlugins等。

schedulerOne的具体代码如下:

scheduleOne()函数中使用了很多plugins,这里不做详细介绍。下面是对该函数的的重要代码进行分析:

3. sched.Framework

第3行,fwk := sched.Framework是声明一些与scheduler相关的plugins作为扩展点

4. NextPod

第5行,sched.NextPod(),pod通过queue的方式进行存储,NextPod用来取出下一个待调度的pod。pod的调度队列也是一个核心知识点,将在后续介绍。

其中:在第7行,pod.DeletionTimestamp,检查要调度的pod的时间戳,如果pod的DeletionTimestamp不为空,则直接返回,因为这个pod已经被下达了删除命令,不需要进行调度。

5. 设定预选的节点数量

在第19行,代码如下:

在开始调度pod之前,先设置筛选出节点的容量,这种做法是为了避免:当集群内的节点过多时,将所有节点过滤一遍,浪费资源和时间。可以看到代码里使用的percent(百分比)的概念,进入到frameworkMetricsSamplePercent常量后,可以发现:

这里将节点的筛选百分比设置成百分之十,有以下几种情况:

  1. 当节点数过多并超过100时,只筛选这些节点总数的百分之十作为最后筛选出的节点。

  2. 当节点数小于100时,则将所有节点筛选一遍

6. sched.Algorithm.Schedule

第25行,此处是scheduelr两个调度策略(预选和优选策略)的核心逻辑。

代码在/pkg/scheduler/core/generic_scheduler.go

sched.Algorithm.Schedule()函数的具体代码实现如下:

接下来对Scheduler()函数中的重要代码进行分析

6.1 podPassesBasicChecks

第8行,podPassesBasicChecks对pod进行一些基本检查,目前只对pvc(PersistentVolumeClaim)进行检查。

podPassesBasicChecks()函数的具体实现如下:

6.2 snapshot

第13行,snapshot()将遍历cache中的所有node,更新node的信息,包括:cache NodeInfo and NodeTree order。此snapshot作用于所有预选和优选函数

snapshot()函数的具体实现如下:

6.4 NodeInfoList

第18行,对node的列表长度进行判断,检查是否有可用的节点。

6.5 findNodesThatFit

具体findNodesThatFit的代码细节将在后续的独立文章中分析

第30行,g.findNodesThatFit()函数是预选策略的调度逻辑。

6.6 prioritizeNodes

具体prioritizeNodes的代码细节将在后续的独立文章中分析

第56行,g.priorityMetaProducer()函数是优选策略的调度逻辑,优选策略是对预选出的节点进行二次筛选,

其中:

第3行,对预选出的节点进行判断:当节点的个数为1时,直接使用这个节点,就不需要再进行优选策略了,这是检验的正常逻辑。

第14行,这里返回的值是priorityList,表明优选策略选出的是包含多个最佳节点的列表,而不是选出唯一的一个节点,优选策略是通过score的计算来对节点进行过滤的。

6.7 selectHost

第79行,g.selectHost()函数的参数为priorityList,即优选策略选出的最佳节点的列表。selectHost从这个列表出选出分数最高的节点,作为最后pod绑定的节点。当有多个节点的分数相同时,在k8s中定义了一个更详细的选择算法,这里就不叙述了。

selectHost()函数的具体实现为:

这个函数的逻辑就是对节点的分数进行比较,最后选出节点分数最高的那个节点,返回结果。

7.sched.preempt

具体的sched.preempt的代码细节将在后续的文章单独分析

第37行,sched.preempt()函数是抢占逻辑的实现。当预选和优选策略均失败时,就会进行抢占式调度,将某个节点上的低优先级的pod驱逐,将优先级更高的pod调度到这个节点上。

8. sched.assume

第85行,sched.assume()函数对pod假性绑定,是pod在bind行为真正发生前的状态,与bind操作异步,从而让调度器不用等待耗时的bind操作返回结果,提升调度器的效率。在assume pod执行时,还穿插着assume pod volumes、bind volumes等逻辑。

此处的代码如下:

其中具体的代码分析如下:

8.1 assumedPodInfo

第3行,assumedPodInfo将pod和它假性绑定的nodeName存入到scheduler cache 中,方便继续执行调度逻辑。

8.2 AssumePodVolumes

第13行,sched.VolumeBinder.Binder.AssumePodVolumes()函数是在pod假性绑定前,对pod上的volumes先进行假性绑定。注释写的很清楚:volumes的绑定发生在pod假性绑定之后,pod真实bind之前。代码如下:

具体来说,这里总共与四种状态,assume volumes(假性绑定volumes)、assume pod(假性绑定pod)、bind volumes(真实绑定volumes)、bind pod(真实绑定pod)。这四个状态的执行顺序为:

assume volumes -> assume pod -> bind volumes -> bind pod

8.3 RunReservePlugins

第21行,fwk.RunReservePlugins()函数运行reserve插件,使pod将要绑定的节点为该pod预留出它所需要的资源。该事件发生在调度器将pod绑定到节点之前,目的是避免调度器在等待 pod 与节点绑定的过程中调度新的 Pod 到节点上(因为绑定pod到节点上是异步操作),发生实际使用资源超出可用资源的情况。

8.4 assume

第29行,sched.assume()对pod假性绑定,将scheduler cache中pod的nodeName设置成scheduleResult.SuggestedHost(最后调度结果:节点的host)。

sched.assume()函数的具体代码实现:

如果假性绑定成功,将结果发送给apiserver;如果假性绑定失败,scheduler将立即释放分配给假定pod的资源

。在代码中还有一处校验,如果假定的pod是 nominated pod,将从内部缓存中删除它,nominated pod 的使用涉及到preemt(抢占策略)的逻辑,就不在这赘述了。

在假性绑定成功之后,进行pod的异步绑定操作。

9. sched.bindVolumes

第125行,验证所有的volumes是否绑定成功,volumes的绑定发生在pod真实绑定之前。

bindVolumes()函数的具体代码实现如下:

10. sched.bind

第152行,bind()函数将assumed pod真实绑定到node上。

bind()函数的具体代码实现:

其中:

第12行,如果绑定成功,调用sched.GetBinder()函数,像apiserver发送bind请求。apiserver将处理这个请求,发送到节点上的kubelete。由kubelet进行后续的操作,并返回结果。

第28行,如果绑定失败,调用sched.SchedulerCache.ForgetPod()函数,从cache中移除该assumed pod。

在绑定失败后,会触发unserver事件,将node上为该pod预留的资源释放掉。

Last updated

Was this helpful?