kubernetes 调度的目的就是为 pod 寻找一个最合适的 node。
最合适 node 的挑选过程分为两步:
1)从集群所有的节点中根据调度算法选出所有可运行该 pod 的节点
2)从上一步的结果中,再根据调度算法挑选出一个最符合条件的节点运行 pod
在调度过程中,默认调度器首先会调用一组叫做 Predicates 的调度算法选出一组合适的 node,然后再运行 Priority 调度算法,来给通过 Predicates 算法选出的 node 进行打分,最终分数最高的那个节点即是 pod 运行的节点。
声明一个点:调度器对一个 pod 调度成功,实际上就是将它的 spec.nodeName 字段填上调度结果的 node 名字。
调度原理示意图如下所示:
上图中 informer path 启动一系列 informer,用来监听 etcd 中的 Pod、Node、Service 等与调度相关的 API 对象,当一个 pod 被创建出来之后,就将这个 pod 添加到待调度队列(优先级队列)。
上面 informer path 将 pod 加入到待调度队列中,下面的 scheduling path 从调度队列中取出 pod,通过 Predicates 调度算法,从 cache 中拿到 node 信息,选出 pod 可运行的 node,然后再调用 Priorities 算法为选出的 node 进行打分,从 0 到 10,得分最高者,就是 pod 最终运行的 node。
上述挑选过程执行完成之后,将 pod 中的 nodename 设置为 node 的名字,这个步骤被称为 Bind。
上述 bind 阶段只会更新 cache 中 node 和 pod 的信息。这种“乐观”假设的 API 对象的更新方式,在 kubernetes 里被称作 Assume。
当 assume 完成之后,调度器才会创建一个 Goroutine 来异步的向 APIServer 发起更新请求,来完成真正的 Bind 操作。当调度器的“乐观”绑定之后,当 pod 在 node 上运行之后,该节点上的 kubelet 还会执行一次操作,确认该 pod 真正可以在该节点上运行。
上面描述了 pod 的调度过程,提到了 Predicates 调度算法和 Priorities 调度算法,下面是对 Predicates 调度算法的一些描述。
Predicates 可以看过是 Filter,通过 Filter 选出当前 pod 可运行的 node,是硬性条件。在 kubernetes 中,默认的调度策略有如下几种:
第一种:GeneralPredicates;主要是对宿主机的 CPU 和内存资源以及其他各种基础条件进行检测。
第二种:与 volume 相关的过滤规则;
第三种:是宿主机相关的过滤规则。
第四种:是 pod 相关的过滤规则。比如 pod 之间的亲和性和反亲和性。
上述是 Predicates 算法相关的过滤规则,执行完之后,再执行 Priorities 算法对选出的 node 进行优选。主要是通过一些算法对 node 进行打分,选出分数得分最高的 node 去运行 pod。
上述是 pod 的大致调度流程,此外默认调度器还有优先级和抢占机制。
首先优先级和抢占机制是解决 pod 调度失败的问题。
当一个 pod 调度失败之后它会被暂时搁置起来,知道 pod 更新或者集群状态发生变化,调度器才会对这个 pod 重新进行调度。
此时 kubernetes 有一个优先级的概念,当优先级高的 pod 调度失败之后该 pod 不会被搁置,而是会挤走某个 node 上优先级低的 pod,这样保证高优先级 pod 被调度成功。这个机制在 1.10 之后才逐步可用。要使用这个机制,首先要在集群中提交一个 PriorityClass:
apiVersion: sceduling.k8s.io/v1beat1
kind: PriorityClass
metadata:
name: hige-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for high priority service pods only."
上述指定了一个优先级为 1000000 的 PriorityClass。在 kubernetes 中,优先级是一个 32bit 的整数,最大值不能超哥 10 亿,并且数字越大优先级越高,超出 10 亿部分作为保留部分被 k8s 自身使用。如果没有声明的话默认是 0,可以通过以下命令在集群中查看:
kubectl describe po redis-cluster-operator-7b987d6477-xdrhw | grep Pri
Priority: 0
定义了优先级在声明 pod 时指定 priorityClassName 即可:
spec: containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
上述是关于优先级的描述。
当一个高优先级的 pod 调度失败的时候,调度器的抢占能力就会被触发。这时调度器从集群中寻找一个节点,使得当这个节点上的一个 pod 被删除,待调度的高优先级 pod 就可以被调度到这个节点上,这就是抢占。
抢占过程发生时,并不会立刻被调度到被强占的 Node 上,而是将抢占者的 nominatedNodeName 字段设置为被强占的 Node 的名字。然后强占者会重新进入下一个调度周期,然后在调度周期来决定是不是要运行在被强占的节点上。
调度器的抢占机制原理:
调度器抢占算法的一个最重要的设计就是在调度队里,使用了两个不同的队列,第一个叫做 ActiveQ,凡是在这个队列中的 pod 都是下一个调度周期需要调度的对象。第二个叫做 unschedulableQ,专门用来存放调度失败的队列。当 unschedulableQ 中的 pod 被更新之后,调度器会自动把这个 pod 移动到 activeQ 中,从而给 pod 重新调度的机会。
在抢占者调度失败之后,抢占者被放倒 unschedulableQ 中,这时会触发会抢占者寻找失败者的流程:
第一:调度器会检查这次失败事件的原因,来确认抢占是不是可以帮助抢占者找到一个新节点。
第二:如果确定抢占可以发生,那么调度器就会把自己缓存的所有节点信息复制一份,然后使用这个副本来模拟抢占过程。检查副本中的每一个节点,确认该节点上的 pod 删除之后抢占者是否可以在这个节点上运行,一旦可以运行,记录下来,这就是抢占的过程。
当模拟抢占过程成功之后调度器就会开始真正的抢占操作:
第一步:调度器会检查牺牲者列表,清理这些 Pod 所携带的 nominatedNodeName 字段。
第二步:调度器会把抢占者的 nominatedNodeName,设置为被抢占的 Node 的名字
第三部:调度器会开启一个 Goroutine,同步地删除牺牲者。
接下来调度器就通过正常的调度流程调度抢占者。
关于 pod 的优先级以及抢占调度在 kubernetes1.11 之后是 beta 版本,可以根据个人需要来决定是否开启。
以上整理自极客时间“深入剖析 kubernetes”