K8S里面的调度整理

       K8S是分布式系统里面的操作系统,Pod更像是操作系统里面的进程组,如此以来当一个Pod想要运行的时候,就必须要依赖于K8S的调度策略来完成这些Pod的调度。

本篇文章就是来整理和介绍k8s的调度这些事,文章从下面几个方面来进行整理:

  1. k8s的调度依赖于哪些资源,这些资源为了配合k8s的调度都做了哪些设计?

  2. k8s的整个调度过程是怎么样子的,他是如何完成调度的?

  3. 如果有些pod需要被优先调度起来应该怎么办?k8s是如何处理的?

一、k8s的资源调度策略

      操作系统中对于一个进程来说,如果希望运行必须需要cpu和存储才行,同样的道理一个pod想要运行,也必须有这两部分才行,于是k8s把pod运行所需要的资源划分成了两大类:可压缩资源和不可压缩资源。

1. k8s的资源模型:

可压缩资源:指的是cpu这一类资源,这类资源的特点是,在资源不够的时候,只会导致pod等运行的时间越来越久也就是会导致“饥饿”,并不会退出。

不可压缩资源:指的是mem这一类,一旦资源不足,就会被内核杀死,并强制pod退出。

      为了描述这些资源信息,k8s将这部分资源与pod绑定,又因为k8s里面一个pod是由多个容器组成的,所以pod里面的资源就是容器资源的总和,其中两个比较重要的指标CPU和Memory。

  CPU 属于可压缩资源:K8S里面描述CPU的单位是millicpu,例如:500m,指的就是 500 millicpu,也就是 0.5 个 CPU 的意思。

   Memory属于不可压缩资源:K8S里面使用这些Ei、Pi、Ti、Gi、Mi、Ki(或者 E、P、T、G、M、K)的方式来作为 bytes 的值,其中带i结尾的是2的幂次方,例如:1Mi=1024*1024;1M=1000*1000。

k8s将这些资源划分成预期和限制两种方式来描述,如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: db
    image: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
  - name: wp
    image: wordpress
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

在调度的时候,kube-scheduler 只会按照 requests 的值进行计算,表示的是分配的资源大小。而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置,表示的使用资源的大小。

2.k8s的Qos模型

至于为什么k8s如此设计,原因如下所示:
Kubernetes 这种对 CPU 和内存资源限额的设计,实际上参考了 Borg 论文中对“动态资源边界”的定义:容器化作业在提交时所设置的资源边界,并不一定是调度系统所必须严格遵守的,这是因为在实际场景中,大多数作业使用到的资源其实远小于它所请求的资源限额。

正是基于k8s的这个资源模型设计理念,k8s又设计了下面几种Qos 模型,用来解决资源不足的情况,k8s到底回收哪一些pod的一个标准。

1)Guaranteed 类别:当 Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等的时候,这个 Pod 就属于 Guaranteed 类别。

2)Burstable 类别:当 Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests,那么这个 Pod 就会被划分到 Burstable 类别。

3)BestEffort类别:如果一个 Pod 既没有设置 requests,也没有设置 limits,那么它的 QoS 类别就是 BestEffort。

   当宿主机的 Eviction 阈值达到后,就会进入 MemoryPressure 或者 DiskPressure 状态,从而避免新的 Pod 被调度到这台宿主机上。当 Eviction 发生的时候,kubelet 具体会挑选哪些 Pod 进行删除操作,就需要参考这些 Pod 的 QoS 类别了。

首当其冲的,自然是 BestEffort 类别的 Pod。

其次,是属于 Burstable 类别、并且发生“饥饿”的资源使用量已经超出了 requests 的 Pod。

最后,才是 Guaranteed 类别。并且,Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态时,Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。

补充说明:对于同 QoS 类别的 Pod 来说,Kubernetes 还会根据 Pod 的优先级来进行进一步排序和选择。

二、K8S的调度过程

在 Kubernetes 项目中,默认调度器的主要职责,就是为一个新创建出来的 Pod,寻找一个最合适的节点(Node)。主要有两步操作:

1、从集群所有的节点中,根据调度算法挑选出所有可以运行该 Pod 的节点;

2、从第一步的结果中,再根据调度算法挑选一个最符合条件的节点作为最终结果。

调度流程如下所示:

1.默认调度器会首先调用一组叫作 Predicate 的调度算法,来检查每个 Node。

2.再调用一组叫作 Priority 的调度算法,来给上一步得到的结果里的每个 Node 打分,最终的调度结果,就是得分最高的那个 Node。

3.调度器对一个 Pod 调度成功,实际上就是将它的 spec.nodeName 字段填上调度结果的节点名字。

1.调度机制的工作原理

K8S 调度器的核心,实际上就是两个相互独立的控制循环。调度机制的工作原理,如下所示:

c82490cd1349c9d9f1072bca20ff9daf.png

第一个控制循环,我们可以称之为 Informer Path

1.启动一系列 Informer,用来监听(Watch)Etcd 中 Pod、Node、Service 等与调度相关的 API 对象的变化。

2.当一个待调度 Pod(即:它的 nodeName 字段是空的)被创建出来之后,调度器就会通过 Pod Informer 的 Handler,将这个待调度 Pod 添加进调度队列。

同时Kubernetes 的默认调度器还要负责对调度器缓存(即:scheduler cache)进行更新。

第二个控制循环,是调度器负责 Pod 调度的主循环,我们可以称之为 Scheduling Path。

3.不断地从调度队列里出队一个 Pod。

4.调用 Predicates 算法进行“过滤”。这一步“过滤”得到的一组 Node,就是所有可以运行这个 Pod 的宿主机列表。

(备注:Predicates 算法需要的 Node 信息,都是从 Scheduler Cache 里直接拿到的。)。

5.调度器再调用 Priorities 算法为上述列表里的 Node 打分,分数从 0 到 10。得分最高的 Node,就会作为这次调度的结果。

6.调度算法执行完成后,进行Bind操作:调度器将 Pod 对象的 nodeName 字段的值,修改为上述 Node 的名字。但是,为了不在关键调度路径里远程访问 APIServer,Kubernetes 的默认调度器在 Bind 阶段,只会更新 Scheduler Cache 里的 Pod 和 Node 的信息,这叫做Assume。

7.Assume 之后,调度器才会创建一个 Goroutine 来异步地向 APIServer 发起更新 Pod 的请求,来真正完成 Bind 操作。

2.调度算法

1)Predicates 调度算法:

Predicates 在调度过程中的作用,可以理解为 Filter,即:它按照调度策略,从当前集群的所有节点中,“过滤”出一系列符合条件的节点。k8s里面主要有下面几种调度策略:
第一种, GeneralPredicates:

这一组过滤规则,负责的是最基础的调度策略,例如:PodFitsResources 计算的就是宿主机的 CPU 和内存资源等是否够用。

PodFitsHost 检查的是,宿主机的名字是否跟 Pod 的 spec.nodeName 一致。PodFitsHostPorts 检查的是,Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用的端口有冲突。

PodMatchNodeSelector 检查的是,Pod 的 nodeSelector 或者 nodeAffinity 指定的节点,是否与待考察节点匹配,等等。

第二种, Volume 相关的过滤规则:
这一组过滤规则,负责的是跟容器持久化 Volume 相关的调度策略。

NoDiskConflict 检查的条件,是多个 Pod 声明挂载的持久化 Volume 是否有冲突,冲突的话就不能调度到这个节点了。

MaxPDVolumeCountPredicate 检查的条件,则是一个节点上某种类型的持久化 Volume 是不是已经超过了一定数目,超过了就不能调度到这个节点了。

VolumeZonePredicate,则是检查持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配。

VolumeBindingPredicate 的规则负责检查该 Pod 对应的 PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配。

第三种,宿主机相关的过滤规则:

这一组规则,主要考察待调度 Pod 是否满足 Node 本身的某些条件。

PodToleratesNodeTaints,负责检查的就是我们前面经常用到的 Node 的“污点”机制,只有当 Pod 的 Toleration 字段与 Node 的 Taint 字段能够匹配的时候,这个 Pod 才能被调度到该节点上。

NodeMemoryPressurePredicate,检查的是当前节点的内存是不是已经不够充足,如果是的话,那么待调度 Pod 就不能被调度到该节点上。

第四种,Pod 相关的过滤规则:

这一组规则,跟 GeneralPredicates 大多数是重合的。

比较特殊的,是 PodAffinityPredicate,这个规则的作用,是检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系。

执行阶段: 开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个 Goroutine,来并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod 的宿主机列表。

2)Priorities调度算法:

Priorities 阶段的工作就是为Predicates阶段选出来的这些节点按照 0-10 分进行打分,得分最高的节点就是最后被 Pod 绑定的最佳节点。常用的几个打分规则如下所示:
LeastRequestedPriority:选择空闲资源(CPU 和 Memory)最多的宿主机。

BalancedResourceAllocation:选择的是调度完成后,所有节点里各种资源分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况。

ImageLocalityPriority :Kubernetes v1.12 里新开启的调度规则,即:如果待调度 Pod 需要使用的镜像很大,并且已经存在于某些 Node 上,那么这些 Node 的得分就会比较高。

NodeAffinityPriority、TaintTolerationPriority 和 InterPodAffinityPriority 这三种 Priority,一个 Node 满足上述规则的字段数目越多,它的得分就会越高。

三、K8S的优先级调度策略

调度并不都是成功的,一旦Pod 调度失败时应该怎么办呢?

k8s模仿操作系统,通过优先级和抢占机制来解决这个难题。

1.优先级:

Kubernetes 规定,优先级是一个 32 bit 的整数,最大值不超过 1000000000(10 亿,1 billion),并且值越大代表优先级越高。超出 10 亿的值是被 Kubernetes 保留下来分配给系统 Pod 使用的,这样做的目的,就是保证系统 Pod 不会被用户抢占掉。

优先级的实现是通过一个优先级队列,当调度的时候,优先级高的pod会先被pop出来。

2. 抢占:

一个高优先级的 Pod 调度失败的时候,调度器的抢占能力就会被触发。这时,调度器就会试图从当前集群里寻找一个节点,使得当这个节点上的一个或者多个低优先级 Pod 被删除后,待调度的高优先级 Pod 就可以被调度到这个节点上。

抢占过程:
1)调度器只会将抢占者的 spec.nominatedNodeName 字段,设置为被抢占的 Node 的名字,抢占者并不会立刻被调度到被抢占的 Node 上。

2)抢占者会重新进入下一个调度周期,然后在新的调度周期里来决定是不是要运行在被抢占的节点上。

备注:这意味着,即使在下一个调度周期,调度器也不会保证抢占者一定会运行在被抢占的节点上。

3.抢占机制的设计简介:

1)抢占发生后,将抢占者存到unschedulableQ队列

2a7712b84de8f30869baff25d1fd821e.png

2)失败事件触发调度器为抢占者寻找牺牲者的流程

第一步,调度器会检查这次失败事件的原因,来确认抢占是不是可以帮助抢占者找到一个新节点,这是因为有很多 Predicates 的失败是不能通过抢占来解决的。

d9a16d0116a28e91ea349fc64dd51bbe.png

在得到了最佳的抢占结果之后,这个结果里的 Node,就是即将被抢占的 Node,被删除的 Pod 列表,就是牺牲者。

3)抢占操作

第一步,调度器会检查牺牲者列表,清理这些 Pod 所携带的 nominatedNodeName 字段。

第二步,调度器会把抢占者的 nominatedNodeName,设置为被抢占的 Node 的名字。// 让抢占者在下一个调度周期重新进入调度流程。

第三步,调度器会开启一个 Goroutine,同步地删除牺牲者。

4)抢占成功:调度器就会通过正常的调度流程把抢占者调度成功。

参看资料:

极客时间:深入剖析 Kubernetes

K8S权威指南第5版

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值