kubernetes-kube-scheduler进程源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hahachenchen789/article/details/87695614

kubernetes scheduler server是由kube-scheduler进程实现的,它运行在kubernetes的管理节点-Master上并主要负责完成从Pod到Node的调度过程。kubernetes scheduler server跟踪kubernetes集群中所有Node的资源利用情况,并采取合适的调度策略,确保调度的均衡性,避免集群中的某些节点过载。从某种意义来说,kubernetes scheduler server也是kubernetes集群的大脑。

谷歌作为公有云的重要供应商,积累了很多经验并且了解客户的需求。在谷歌看来,客户并不真正关心他们的服务究竟运行在哪台机器上,他们最关心服务的可靠性,希望发生故障后能自动恢复,遵循这一思想,kubernetes scheduler server实现了完全市场经济的调度原则并彻底抛弃了传统意义上的计划经济。

下面对其启动过程、关键代码分析及设计总结等方面进行深入分析和讲解。

进程启动过程:

kube-scheduler进程的入口类源码位置如下:

cmd/kube-scheduler/scheduler.go

入口main函数的逻辑如下:

对上述代码的风格和逻辑我们再熟悉不过了:创建一个schedulerserver对象,将命令行参数传入,并且进入schedulerserver的run方法,无限循环下去。

首先我们看看schedulerserver的数据结构(app/server.go),下面是其定义:

这里的关键属性有以下两个。

1.algorithmprovider:对应参数algorithm-provider,是AlgorithmProviderConfig的名称。

2.PolicyConfigFile:用来加载调度策略文件。

从代码上来看这两个参数的作用其实是一样的,都是加载一组调度规则,这组调度规则要么在程序里定义为一个AlgorithmProviderConfig,要么保存到文件中。下面的源码清楚地解释了这个过程:

创建了schedulerserver结构体实例后,调用此实例的方法func(s* APIServer) Run(_ []string),进入关键流程。首先创建一个Rest Client对象用于访问kubernetes API Server提供的API服务:

随后,创建一个HTTP server以提供必要的性能分析和性能指标度量(metrics)的rest服务:

接下来,启动程序构造了configfactory,这个结构体包括了创建一个scheduler所需的必要属性。

1.Podqueue:需要调度的Pod队列。

2.BindPodsRateLimiter:调度过程中限制Pod绑定速度的限速器。

3.modeler:这是用于优化Pod调度过程而设计的一个特殊对象,用于预测未来,一个pod被设计调度到机器A的事实被称为assumed调度,即假定调度。这些调度安排被保存到特定队列里,此时调度过程是能看到这个预安排的,因而会影响到其他Pod的调度。

4.PodLister:负责拉取已经调度过的,以及被假定调度过的Pod列表

5.NodeLister:负责拉取Node节点(minion)列表

6.ServiceLister:负责拉取kubernetes服务列表

7.scheduledPodLister、scheduledPodPopulator:controller框架创建过程中返回的store对象与controller对象,负责定期从kubernetes API server上拉取已经调度好的Pod列表,并将这些Pod从modeler的的假定调度过的队列中删除。

在构造configFactory的方法factory.NewConfigFactory(kubeclient)中,我们看到下面这段代码:

这里沿用了之前看到的controller framework的身影,上述controller实例所做的事情就是获取并监听已经调度的pod列表,并将这些Pod列表从modeler中的assumed队列中删除。

接下来,启动进程用上述创建好的configFactory对象作为参数来调用schedulerserver的createConfig方法,创建一个scheduler.config对象,而此段代码的关键逻辑几种在configfactory的createFromKeys这个函数里,其主要步骤如下:

1.创建一个与Pod相关的reflector对象并定期执行,该reflector负责查询并监测等待调度的Pod列表,即还没有分配主机的Pod,然后把它们放入configFactory的PodQueue中等待调度。

2.启动configfactory的scheduledPodPopulator controller对象,负责定期从kubernetes API server上拉取已经调度好的Pod列表,并将这些Pod从modeler中的假定(assumed)调度过的队列中删除。相关代码为:go f.scheduledPodPopulator.Run(f.StopEverything)。

3.创建一个Node相关的Reflector对象并定期执行,该Reflector负责查询并监测可用的Node列表(可用意味着Node的spec.unschedulable属性为false),这些Node被放入configFactory的NodeLister.store里,相关代码为:cache.NewReflector。

4.创建一个service相关的reflector对象并定期执行,该reflector负责查询并监测已定义的service列表,并放入configFactory的serviceLister.Store里,这个过程的目的是scheduler需要知道一个service当前创建的所有Pod,以便能正确进行调度。相关代码为:cache.NewReflector

5.创建一个实现了algorithm.scheduleAlgorithm接口的对象geneticscheduler,它负责完成从Pod到Node的具体调度工作。调度完成的Pod放入configFactory的PodLister里,相关代码为:algo:=scheduler.NewGenericScheduler()。

6.最后一步,使用之前的这些信息创建scheduler.config对象并返回。

从上面分析我们看出,其实在创建scheduler.config的过程中已经完成了kubernetes scheduler server进程中的很多启动工作,于是整个进程的启动过程的最后一步简单明了:使用刚刚创建好的config对象来构造一个scheduler对象并启动运行,即下面两行代码:

而scheduler的Run'方法就是不停执行scheduleOne方法:

scheduleOne方法的逻辑也比较清晰,即获取下一个待调度的Pod,然后交给genericscheduler进行调度(完成Pod到某个Node的绑定过程),调度成功后通知modeler。这个过程同时增加了限流和性能指标的逻辑。

关键代码分析:

上面对启动过程进行详细分析后。我们大致明白了kubernetes scheduler server的工作流程,但由于代码中涉及多个Pod队列和Pod状态切换逻辑,因此这里有必要对这个问题进行详细分析,以弄清在这个调度过程中Pod的来龙去脉。首先我们知道configFactory里的PodQueue是“待调度的Pod队列”,这个过程是通过无限循环执行一个reflector来从kubernetes API Server上获取待调度的Pod列表并填充到队列中实现的,因此Reflector框架已经实现了通用的代码,所以在kubernetes scheduler server这里,通过一行代码就能完成这个复杂的过程:

上述代码中的createUnassignedPodLW是查询和监测spec.nodeName为空的Pod列表,此外,我们注意到scheduler.config里提供了NextPod这个函数指针来从上述队列中消费一个元素,下面是相关代码片段:

然后,这个PodQueue是如何被消费的呢?就在之前提到的scheduler.scheduleOne的方法里,每次调用NextPod方法会获取一个可用的Pod,然后交给genericScheduler进行调度,下面是相关代码片段:

genericscheduler.schedule方法只是给出该Pod调度到的目标Node,如果调度成功,则设置该Pod的spec.nodeName为目标Node,然后通过HTTP Rest调用写入kubernetes API Server里完成Pod的Binding操作,最后通知ConfigFactory的modeler,将此Pod放入Assumed Pod队列,下面是相关代码片段:

当Pod执行Bind操作成功后,kubernetes API Server上Pod已经满足已调度的条件,因为spec.nodeName已经被设置为目标Node地址,此时ConfigFactory的scheduledPodPopulator这个controller就会监听到此变化,将此Pod从modeler中的assumed队列中删除,下面是相关代码片段:

谷歌的大神在源码中说明modeler的存在是为了调度的优化,那么这个优化具体体现在哪呢?由于Rest Watch API存在延时,当前已经调度好的Pod很可能还未通知给scheduler,于是为每个刚刚调度完成的Pod发放一个“暂住证”,安排暂住到assumed队列里,然后设计一个获取已调度的Pod队列的新方法,该方法合并assumed队列与watch缓存队列,这样一来,就得到了最佳答案。

接下来,我们深入分析Pod调度中所用到的流控技术,从下面这段代码开始:

上述代码中的BindPodsRateLimiter采用了开源项目juju的一个子项目ratelimit,项目地址为http://github.com/juju/ratelimit,它实现了一个高效的基于经典令牌桶(Token Bucket)的流控算法,如下图所示是经典令牌桶流控算法的原理:

简单地说,控制现场以固定速率向一个固定容量的桶(bucket)中投放令牌(token),消费者线程则等待并获取到一个令牌后才能继续接下来的任务,否则需要等待可用令牌的到来。具体来说,加入用户配置的平均限流速率为r,则每隔1/r秒就会有一个令牌被加入桶中,而令牌桶最多可以存储b个令牌,如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃。从长期运行结果来看,消费者的处理速率被限制成常量r,令牌桶流控算法除了能限制平均处理速度,还允许某种程度的突发速率。

juju的ratelimit模块通过以下API提供了构造一个令牌桶的简单做法,其中,rate参数表示每秒填充到桶里的令牌数量,capacity则是桶的容量:

我们回头再看看kubernetes scheduler server中BindPodsRateLimiter的赋值代码:

根据进去,发现它就是调用了刚才所提到的juju函数Limiter=ratelimit.NewBuckerWithRate(float 64(qps), init 64(burst)),其中qps目前常量为15,而burst为20。

最后我们一起深入分析kubernetes scheduler server中关于Pod调度的细节。首先,我们需要理解启动过程中国schedulerserver加载调度策略相关配置的这段代码:

这里加载了两组策略,其中predicateFuncs是一个map,key为FitPredicate的名称,value为对应的algorithm.FitPredicate函数,它表明一个候选的Node是否满足当前Pod的调度要求,FitPredicate函数的具体定义如下:

FitPredicate是Pod调度过程中必须满足的规则,只有顺利通过由多有FitPredicate组成的这道封锁线,一个Node才能成为一个合格的候选人,等待下一步评审。目前系统提供的具体的FitPredicate实现都在predicates.go里,系统默认加载注册FitPredicate的地方在defaultPredicates方法里。

当有一组Node通过筛选成为候选人时,需要有一种方法选择最优的Node,这就是我们接下来要介绍的priorityConfigs要做的事了。priorityConfigs是一个数组,类型为algorithm.PriorityConfig,PriorityConfig包括一个PriorityFunction函数,用来计算并给出一组Node的优先级,下面是相关代码:

如果看到这里还是不明白它的用途,那么认真读下面这段来自genericScheduler的计算候选节点优先级的PrioritizeNodes方法,你就顿悟了:一个候选节点的优先级总分是所有评委老师(PriorityConfig)一起给出的加权总分,评委越是好weight越大,评分的影响力越大。

接下来我们看看系统初始化加载的默认的predicate和priority有哪些,默认加载的代码位\pkg\scheduler\algorithmprovider\defaults\defaults.go中的init函数中:

跟踪进去后,可以看到系统默认加载的predicate有如下几种:

1.PodFitsResources

2.MatchNodeSelector

3,.HostName

而默认加载的priority有如下:

1.LeastRequestdPriority

2.BalancedResourceAllocation

3.ServiceSpreadingPriority

从上述信息看,kubernetes默认的调度指导原则是尽量均匀分布Pod到不同的Node上,并且确保各个Node上的资源利用率基本保持一致,也就是说如果有100台机器,则可能每个机器都被调度到,而不是只有20%,哪怕每台机器都只利用了不到10%的资源。

接下来我们以服务亲和性这个默认没有加载的Predicate为例,看看kubernetes如何通过policy文件注册加载它的,下面是我们定义的一个policy文件:

首先,这个文件被映射成api.policy对象(pkg/scheduler/api/types.go),下面是其结构体定义:

我们看到policy文件中的predicate部分被映射称为PredicatePolicy数组:

而PredicateArgument的定义如下,包括服务亲和性的相关属性serviceAffinity

策略文件被映射称为api.policy对象后,PredicatePolicy部分的处理逻辑则交给下面的函数进行处理(pkg/scheduler/factory/plugin.go)

在上面的代码中,当serviceaffinity属性不空时,就会调用predicate.NewServiceAffinityPredicate方法来创建一个处理服务亲和性的FitPredicate,随后被加载到全局的predicateFactory中生效。

最后,genericScheduler.Schedule方法才是真正实现Pod调度的方法,我们看看这段完整代码:

这段代码很简单,因为该干的活已经被predicate和priority干完了。

架构之美,在于程序逻辑分析分解到恰到好处。

猜你喜欢

转载自blog.csdn.net/hahachenchen789/article/details/87695614