Kubernetes调度器-Kube Scheduler

在 Kubernetes 中,调度 是指将 Pod 放置到合适的 Node 上,然后对应 Node 上的 Kubelet 才能够运行这些 pod。

那么到底什么是调度器呢?调度器通过 kubernetes 的 watch 机制来发现集群中新创建且尚未被调度到 Node 上的 Pod。调度器会将发现的每一个未调度的 Pod 调度到一个合适的 Node 上来运行。调度器会依据下文的调度原则来做出调度选择。 如果你想要理解 Pod 为什么会被调度到特定的 Node 上,或者你想要尝试实现一个自定义的调度器,这篇文章将帮助你了解调度。

1、Kube-Scheduler

Kube-scheduler 是 Kubernetes 集群的默认调度器,并且是集群 控制面 的一部分。如果你真的希望或者有这方面的需求,kube-scheduler 在设计上是允许你自己写一个调度组件并替换原有的 kube-scheduler。kube-scheduler主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、更加充分的利用集群的资源,这也是我们选择使用 kubernetes 的⼀个非常重要的理由。如果⼀门新的技术不能帮助企业节约成本、提供效率,我相信是很难推进的。

对每一个新创建的 Pod 或者是未被调度的 Pod,kube-scheduler 会选择一个最优的 Node 去运行这个 Pod。然而,Pod 内的每一个容器对资源都有不同的需求,而且 Pod 本身也有不同的资源需求。因此,Pod 在被调度到 Node 上之前,根据这些特定的资源调度需求,需要对集群中的 Node 进行一次过滤。在一个集群中,满足一个 Pod 调度请求的所有 Node 称之为 可调度节点。如果没有任何一个 Node 能满足 Pod 的资源请求,那么这个 Pod 将一直停留在未调度状态直到调度器能够找到合适的 Node。

调度器先在集群中找到一个 Pod 的所有可调度节点,然后根据一系列函数对这些可调度节点打分,然后选出其中得分最高的 Node 来运行 Pod。之后,调度器将这个调度决定通知给 kube-apiserver,这个过程叫做 绑定。在做调度决定时需要考虑的因素包括:单独和整体的资源请求、硬件/软件/策略限制、亲和以及反亲和要求、数据局域性、负载间的干扰等等。

2、调度流程

默认情况下,kube-scheduler 提供的默认调度器能够满足我们绝大多数的要求,前⾯屋面基本上用的是默认策略,都可以保证我们的 Pod 可以被分配到资源充足的节点上运行。但是在实际的线上项目中,可能我们自己会比 kubernetes 更加了解自己的应用,比如我们希望⼀个Pod 只能运行在特定的几个节点上,或者这几个节点只能用来运行特定类型的应用,这就需要调度器能够可控。

kube-scheduler 是 kubernetes 的调度器,它的主要作⽤就是根据特定的调度算法和调度策略将 Pod调度到合适的 Node 节点上去,是⼀个独立的二进制程序,启动之后会⼀直监听 API Server,获取到PodSpec.NodeName 为空的 Pod,对每个 Pod 都会创建⼀个 binding。

img

这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:

  • 如何保证全部的节点调度的公平性?要知道并不是所有节点资源配置都是⼀样的。
  • 如何保证每个节点都能被分配资源?
  • 集群资源如何能够被高效利用?
  • 集群资源如何才能被最大化使用?
  • 如何保证 Pod 调度的性能和效率?
  • 用户是否可以根据自己的实际需求定制自己的调度策略?

考虑到实际环境中的各种复杂情况,kubernetes 的调度器采用插件化的形式实现,可以方便用户进行定制或者⼆次开发,我们可以自定义⼀个调度器并以插件形式和 kubernetes 进行集成。

img

调度主要分为以下几个部分:

  • 首先是预选过程,过滤掉不满足条件的节点,这个过程称为Predicates;
  • 然后是优选过程,对通过的节点按照优先级排序,称之为Priorities;
  • 最后从中选择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误。

Predicates阶段首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的 Node 将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么 Pod 将会一直处于 Pending 状态,直到有节点满足条件,在这期间调度器会不断的重试。

Priorities阶段即再次对节点进行筛选,如果有多个节点都满足条件的话,那么系统会按照节点的优先级(priorites)大小对节点进行排序,最后选择优先级最高的节点来部署 Pod 应用。

注:所以我们在部署应用的时候,如果发现有 Pod 一直处于 Pending 状态,那么就是没有满足调度条件的节点,这个时候可以去检查下节点资源是否可用。

当然,kube-scheduler调度器还有更详细的流程。kube-scheduler调度器给一个Pod做调度选择包括两个步骤:过滤和打分。过滤阶段会将所有满足 Pod 调度需求的 Node 选出来。例如,PodFitsResources 过滤函数会检查候选 Node 的可用资源能否满足 Pod 的资源请求。在过滤之后,得出一个 Node 列表,里面包含了所有可调度节点;通常情况下,这个 Node 列表包含不止一个 Node。如果这个列表是空的,代表这个 Pod 不可调度。 在打分阶段,调度器会为 Pod 从所有可调度节点中选取一个最合适的 Node。根据当前启用的打分规则,调度器会给每一个可调度节点进行打分。 最后,kube-scheduler 会将 Pod 调度到得分最高的 Node 上。如果存在多个得分最高的 Node,kube-scheduler 会从中随机选取一个。

kube-scheduler 有一系列的默认调度策略、当然也提供过滤策略和打分策略。具体如下:

过滤策略:

  • PodFitsHostPorts:如果 Pod 中定义了 hostPort 属性,那么需要先检查这个指定端口是否 已经被 Node 上其他服务占用了。
  • PodFitsHost:若 pod 对象拥有 hostname 属性,则检查 Node 名称字符串与此属性是否匹配。
  • PodFitsResources:检查 Node 上是否有足够的资源(如,cpu 和内存)来满足 pod 的资源请求。
  • PodMatchNodeSelector:检查 Node 的 标签 是否能匹配 Pod 属性上 Node 的 标签 值。
  • NoVolumeZoneConflict:检测 pod 请求的 Volumes 在 Node 上是否可用,因为某些存储卷存在区域调度约束。
  • NoDiskConflict:检查 Pod 对象请求的存储卷在 Node 上是否可用,若不存在冲突则通过检查。
  • MaxCSIVolumeCount:检查 Node 上已经挂载的 CSI 存储卷数量是否超过了指定的最大值。
  • CheckNodeMemoryPressure:如果 Node 上报了内存资源压力过大,而且没有配置异常,那么 Pod 将不会被调度到这个 Node 上。
  • CheckNodePIDPressure:如果 Node 上报了 PID 资源压力过大,而且没有配置异常,那么 Pod 将不会被调度到这个 Node 上。
  • CheckNodeDiskPressure:如果 Node 上报了磁盘资源压力过大(文件系统满了或者将近满了), 而且配置没有异常,那么 Pod 将不会被调度到这个 Node 上。
  • CheckNodeCondition:Node 可以上报其自身的状态,如磁盘、网络不可用,表明 kubelet 未准备好运行 pod。 如果 Node 被设置成这种状态,那么 pod 将不会被调度到这个 Node 上。
  • PodToleratesNodeTaints:检查 pod 属性上的 tolerations 能否容忍 Node 的 taints。
  • CheckVolumeBinding:检查 Node 上已经绑定的和未绑定的 PVCs 能否满足 Pod 对象的存储卷需求。

打分策略:

  • SelectorSpreadPriority:尽量将归属于同一个 Service、StatefulSet 或 ReplicaSet 的 Pod 资源分散到不同的 Node 上。
  • InterPodAffinityPriority:遍历 Pod 对象的亲和性条目,并将那些能够匹配到给定 Node 的条目的权重相加,结果值越大的 Node 得分越高。
  • LeastRequestedPriority:空闲资源比例越高的 Node 得分越高。换句话说,Node 上的 Pod 越多,并且资源被占用的越多,那么这个 Node 的得分就会越少。
  • MostRequestedPriority:空闲资源比例越低的 Node 得分越高。这个调度策略将会把你所有的工作负载(Pod)调度到尽量少的 Node 上。
  • RequestedToCapacityRatioPriority:为 Node 上每个资源占用比例设定得分值,给资源打分函数在打分时使用。
  • BalancedResourceAllocation:优选那些使得资源利用率更为均衡的节点。
  • NodePreferAvoidPodsPriority:这个策略将根据 Node 的注解信息中是否含有 scheduler.alpha.kubernetes.io/preferAvoidPods 来 计算其优先级。使用这个策略可以将两个不同 Pod 运行在不同的 Node 上。
  • NodeAffinityPriority:基于 Pod 属性中 PreferredDuringSchedulingIgnoredDuringExecution 来进行 Node 亲和性调度。你可以通过这篇文章 Pods 到 Nodes 的分派 来了解到更详细的内容。
  • TaintTolerationPriority:基于 Pod 中对每个 Node 上污点容忍程度进行优先级评估,这个策略能够调整待选 Node 的排名。
  • ImageLocalityPriority:Node 上已经拥有 Pod 需要的 容器镜像 的 Node 会有较高的优先级。
  • ServiceSpreadingPriority:这个调度策略的主要目的是确保将归属于同一个 Service 的 Pod 调度到不同的 Node 上。如果 Node 上 没有归属于同一个 Service 的 Pod,这个策略更倾向于将 Pod 调度到这类 Node 上。最终的目的:即使在一个 Node 宕机之后 Service 也具有很强容灾能力。
  • CalculateAntiAffinityPriorityMap:这个策略主要是用来实现pod反亲和。
  • EqualPriorityMap:将所有的 Node 设置成相同的权重为 1。

kubernetes 调度器的源码位于 kubernetes/pkg/scheduler 中,大体的代码目录结构大家可以自行下载源码了解:https://github.com/kubernetes/kubernetes/tree/v1.17.3 Kubernetes现在更新的特别快、很多功能模块也在更新迭代中不停变化。更多内容大家可以自行上官方文档和Github查看学习。后期我也会更新关于 kube-scheduler 调度器的

3、调度器扩展

实现“调度扩展程序“:默认调度器kube-scheduler在进行预选时会调用该扩展程序进行过滤节点;在优选时会调用该扩展程序进行给节点打分,或者在bind操作时,调用该扩展器进行bind操作。

通俗点讲就是:kube-scheduler在启动的时候可以通过 –policy-config-file参数来指定调度策略文件,通过启动参数的policy配置,选用某些默认调度器中的预选、优选调度算法的同时,也可以调用外部扩展调度程序的算法,计算得到最优的调度节点,无需修改kube-scheduler代码,只需要在启动参数中增加配置文件即可将默认调度程序和扩展调度程序相互关联。当然,我们也可以根据我们自己的需要来组装Predicates和Priority函数。

具体流程如下:

img

kube-scheduler在调度pod实例时,首先获取到Node1、Node2、Node3三个节点信息,进行默认的预选阶段,筛选满足要求的节点,其次再调用扩展程序中的预选算法,选出剩下的节点,假设预选阶段Node3上资源不足被过滤掉,预选结束后只剩Node1和Node2;Node1和Node2进入kube-scheduler默认的优选阶段进行节点打分,其次再调用扩展调度程序中的优选算法进行打分,kube-scheduler会将所有算法的打分结果进行加权求和,获得分数最高的节点作为pod最终bind节点,然后kube-scheduler调用apiserver进行bind操作。

官方Policy文件示例如下:

{
  "kind" : "Policy",
  "apiVersion" : "v1",
  "predicates" : [
      {"name" : "PodFitsHostPorts"},
      {"name" : "PodFitsResources"},
      {"name" : "NoDiskConflict"},
      {"name" : "NoVolumeZoneConflict"},
      {"name" : "MatchNodeSelector"},
      {"name" : "HostName"}
  ],
  "priorities" : [
      {"name" : "LeastRequestedPriority", "weight" : 1},
      {"name" : "BalancedResourceAllocation", "weight" : 1},
      {"name" : "ServiceSpreadingPriority", "weight" : 1},
      {"name" : "EqualPriority", "weight" : 1}
  ]
}

4、多调度器

Kubernetes 自带了一个默认调度器,其详细描述请查阅这里。 如果默认调度器不适合我们的需求,我们可以实现自己的调度器。 不仅如此,我们甚至可以伴随着默认调度器同时运行多个调度器,并告诉 Kubernetes 为每个 pod 使用什么调度器。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  schedulerName: my-scheduler  # 选择使用自定义调度器 my-scheduler
  containers:
  - name: nginx
    image: nginx:1.10

要开发自己的调度器也是比较容易的,比如这里的 my-scheduler:

  • 首先需要通过指定的 API 获取节点和 Pod;
  • 然后选择phase=Pending和schedulerName=my-scheduler的pod;
  • 计算每个 Pod 需要放置的位置之后,调度程序将创建一个Binding对象;
  • 然后根据我们自定义的调度器的算法计算出最适合的目标节点。

当然,我们还可以在集群中运行多个调度器,指定Pod的调度器等操作。详细内容请小伙伴们自定查看官方文档、这里就不再详细描述了。

多调度器官方文档:https://kubernetes.io/zh/docs/tasks/administer-cluster/configure-multiple-schedulers/

5、优先级调度

与前面所讲的调度优选策略中的优先级(Priorities)不同,前面所讲的优先级指的是节点优先级,而我们这里所说的优先级 pod priority 指的是 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

另外一个值得注意的是当节点没有足够的资源供调度器调度 Pod,导致 Pod 处于 pending 时,抢占(preemption)逻辑就会被触发。Preemption会尝试从一个节点删除低优先级的 Pod,从而释放资源使高优先级的 Pod 得到节点资源进行部署。

现在我们通过下面的图再去回顾下 kubernetes 的调度过程:

img

推荐文章