kube-scheduler是k8s的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将Pod调度到最优的工作节点上,从而更加合理、更加充分的利用集群的资源,这也就是选择使用k8s一个非常重要的理由。
调度流程
默认情况下,kube-scheduler提供的默认调度器能够满足我们绝大多数的要求,前面默认的调度器策略,都可以保证我们Pod可以被分配到资源充足的节点上运行。但是在实际的线上项目中,可能我们自己会比k8s更加了解我们自己的应用,比如我们希望一个Pod只能运行在特定的几个节点上,或者这几个节点只能用来运行特定类型的应用,这就需要我们的调度器能够可控。
kube-scheduler的主要作用 就是根据特定的调度器算法和调度策略将Pod调度到合适的Node节点上去,是一个独立的二进制程序,启动之后会一直监听API Server,获取到Pod.Spec.NodeName为空的Pod,对每个Pod都会创建一个binding。
这个过程我们看起来比较简单,但在实际生产环境中,需要考虑的问题就很多了:
- 如何保证全部的节点调度的公平性?要知道并不是所有节点资源配置一定都是一样的
- 如何保证每个节点都能被分配资源
- 集群资源如何能够被高效利用
- 集群资源如何才能被最大化利用
- 如何保证Pod的调度的性能和效率
- 用户能否根据自己的实际需求定制自己的调度策略
考虑到实际环境的各种复杂情况,k8s的调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发,我们可以自定义一个调度器并以插件形式和k8s进行集成。
k8s调度器源码位于kubernetes/pkg/scheduler中,其中Scheduler创建和运行的核心程序对应的代码在pkg/scheduler/scheduler.go,如果要查看kube-scheduler的入口程序,对应的代码在cmd/kube-scheduler/scheduler.go。
调度主要分为以下几个部分:
- 首先是预选过程,过滤掉不满足条件的节点,这个过程成称为
Predicates(过滤) - 然后是优选过程,对通过的节点按照优先级排序,称之为
Priorities(打分) - 最后从中选择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误
Predicates阶段首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的节点将被记录并作为第二阶段的输入,如果所有节点都不满足条件,那么Pod将会一直处于Pending状态,直到有节点满足条件,在这期间调度器会不断的重试。
所以在部署应用的时候,如果发现有Pod一直处于Pending状态,那么就是没有满足调度条件的节点,这个时候可以去检查下节点资源是否可用。
Priorities阶段即再次对节点进行筛选,如果有多个节点满足条件的话,那么系统会按照节点的优先级(priorities)大小对节点进行排序,最后选择优先级最高的节点来部署Pod应用。
下面是调度过程的简单示意图:
更详细的流程:
- 首先,客户端通过API Server的REST API或者kubectl工具创建Pod资源
- API Server收到用户请求后,存储相关数据到etcd数据库中
- 调度器监听API Server查看到还未被调度(binding)的Pod列表,循环遍历的为每个Pod尝试分配节点,这个分配过程就是上面提到的两个阶段:
- 预选阶段(Predicates),过滤节点,调度器用一组规则过滤掉不符合要求的Node节点,比如Pod设置了资源的request,那么可用资源比Pod需要的资源少的主机显然就会被过滤掉
- 优选阶段(Priorities),为节点的优先级打分,将上一阶段过滤出来的Node列表进行打分,调度器会考虑一些整体的优化策略,比如把Deployment控制的多个Pod副本尽量分布到不同的主机上,使用最低负载的主机等等策略
- 经过上面的阶段过滤后选择打分最高的Node节点和Pod进行
binding操作,然后将结果存储到etcd中最后被选择出来的Node节点对应的kubelet去执行创建Pod的相关操作(当然也是watch API Server发现的)
目前调度器已经全部通过插件的方式实现了调度框架,默认开启了调度插件如以下代码所示:
// pkg/scheduler/algorithmprovider/registry.gofunc getDefaultConfig() *schedulerapi.Plugins {return &schedulerapi.Plugins{QueueSort: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: queuesort.Name},},},PreFilter: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: noderesources.FitName},{Name: nodeports.Name},{Name: podtopologyspread.Name},{Name: interpodaffinity.Name},{Name: volumebinding.Name},},},Filter: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: nodeunschedulable.Name},{Name: noderesources.FitName},{Name: nodename.Name},{Name: nodeports.Name},{Name: nodeaffinity.Name},{Name: volumerestrictions.Name},{Name: tainttoleration.Name},{Name: nodevolumelimits.EBSName},{Name: nodevolumelimits.GCEPDName},{Name: nodevolumelimits.CSIName},{Name: nodevolumelimits.AzureDiskName},{Name: volumebinding.Name},{Name: volumezone.Name},{Name: podtopologyspread.Name},{Name: interpodaffinity.Name},},},PostFilter: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: defaultpreemption.Name},},},PreScore: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: interpodaffinity.Name},{Name: podtopologyspread.Name},{Name: tainttoleration.Name},},},Score: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: noderesources.BalancedAllocationName, Weight: 1},{Name: imagelocality.Name, Weight: 1},{Name: interpodaffinity.Name, Weight: 1},{Name: noderesources.LeastAllocatedName, Weight: 1},{Name: nodeaffinity.Name, Weight: 1},{Name: nodepreferavoidpods.Name, Weight: 10000},// Weight is doubled because:// - This is a score coming from user preference.// - It makes its signal comparable to NodeResourcesLeastAllocated.{Name: podtopologyspread.Name, Weight: 2},{Name: tainttoleration.Name, Weight: 1},},},Reserve: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: volumebinding.Name},},},PreBind: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: volumebinding.Name},},},Bind: &schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: defaultbinder.Name},},},}}
从上面可以看出调度器的一系列算法由各种插件在调度的不同阶段来完成,先来了解下调度框架。
调度器调优
作为k8s集群的默认调度器,kube-scheduler主要负责将Pod调度到集群的Node上。在一个集群中,满足一个Pod调度请求的所有节点称之为可调度Node,调度器现在集群中找到一个Pod的可调度Node,然后根据一系列函数对这些可调度Node进行打分,之后选出其中得分最高的Node来运行Pod,最后调度器将这个调度决定告知kube-apiserver,这个过程叫做绑定。
在k8s 1.12版本之前,kube-scheduler会检查集群中所有节点的可调度性,并且给可调度节点打分。k8s 1.12版本添加了一个新功能,允许调度器在找到一定数量的可调度节点之后就停止继续寻找可调度节点。该功能提高调度器在大规模集群的调度性能,这个数值是集群规模的百分比,这个百分比通过percentageOfNodeToScore参数来进行配置,其值的范围在1到100之间,最大值就是100%,如果设置为0就代表没有提供这个参数配置。k8s 1.14版本有加入一个特性,在该参数没有被用户配置的情况下,调度器会根据集群的规模自动设置一个集群比例,然后通过这个比例筛选出一定数量的可调度节点进入打分阶段。该特性使用线性公式计算出集群比例,
比如100个节点的集群下会取50%,在5000个节点的集群下取10%,这个自动设置的参数的最小值是5%,换句话说,调度器至少会对集群中5%的节点进行打分,除非用户将改参数设置的低于5。
当集群的可调度节点少于50个时,调度器仍然会去检查所有节点,因为可调度节点太少,不足以停止调度器最初的过滤选择。如果想关掉这个范围参数,可以将percentageofnodetoscore值设置成100。
percentageofnodetoscore的值必须在1到100之间,而且其默认值是通过集群的规模计算得来的,另外50个Node的数值是硬编码在程序里面的,设置这个值的作用在于:当集群的规模是数百个节点并且percentageofnodestoscore参数设置的过低的时候,调度器筛选到的可调度节点数目基本不会受到该参数影响。当集群规模较小时,这个设置对调度器性能提升并不明显,但是在超过1000个Node的集群中,将调优参数设置为一个较低的值可以很明显的提升调度器性能。
不过值得注意的是,该参数设置后可能会导致只有集群中少数节点被选为可调度节点,很多Node都不会进入打分阶段,由于这个原因,一个本来可以在打分阶段得分很高的Node甚至都不能进入打分阶段,由于这个原因,所以这个参数不应该被设置成一个很低的值,通常的做法是不会将这个参数的值设置的低于10,很低的参数一般在调度器的吞吐量很高且对Node的打分不重要的情况下使用。换句话说,只有当你更倾向于在可调度节点中人员选择一个Node来运行这个Pod,才能使用很低的参数设置。
如果你的集群只有数百个节点甚至更少,实际上并不推荐你讲这个参数设置的比默认值更低,因为这种情况下不太会有效的提高调度器性能。
优先级调度
与前面所讲的调度优选策略中的优先级(Priorities)不同,前面所说的优先级指的是节点优先级,而这里所说的优先级指的是Pod的优先级,高优先级的Pod会优先被调度,或者在资源不足的情况下牺牲低优先级的Pod,以便于重要的Pod能够得到资源部署。
要定义Pod优先级,就需要先定义PriorityClass对象,该对象没有Namespace的限制:
apiversion: v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."
其中:
- value为32位证书的优先级,该值越大,优先级越高
- globalDefault用于未配置PriorityClassName的Pod,整个集群中应该只有一个PriorityClass将其设置为true
然后通过在Pod的spec.priorityClassName中指定已定义的PriorityClass名称即可:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
PriorityClassName: high-priority
另外一个值得注意的是dang节点没有足够的资源供调度器调度Pod,导致Pod处于Pending时,抢占(preemption)逻辑就会被触发,抢占会尝试从一个节点删除低优先级的Pod,从而释放资源使得高优先级的Pod得到节点资源进行部署。
