k8s集群调度2

调度过程

image-20220103164141068

Kubernetes 集群架构,它包括了一个 kube-ApiServer,一组 webhooks 的 Controller,以及一个默认的调度器 kube-Scheduler,还有两台物理机节点 Node1 和 Node2,分别在上面部署了两个 kubelet。

向这个 Kubernetes 集群提交一个 pod,它的调度过程是什么样的一个流程

  • 写好了一个 yaml 文件,定义一个pod,往 kube-ApiServer 里面提交这个 yaml 文件。
  • ApiServer 会先把这个待创建的请求路由给我们的 webhooks 的 Controlles 进行校验。
  • 通过校验之后,ApiServer 会在集群里面生成一个 pod,但此时生成的 pod,它的 nodeName 是空的,并且它的 phase 是 Pending 状态
  • 生成了这个 pod 之后,kube-Scheduler 以及 kubelet 都能 watch 到这个 pod 的生成事件,kube-Scheduler 发现这个 pod 的 nodeName 是空的之后,会认为这个 pod 是处于未调度状态。
  • 它会把这个 pod 拿到自己里面进行调度,通过一系列的调度算法,包括一系列的过滤和打分的算法后,Schedule 会选出一台最合适的节点,并且把这一台节点的名称绑定在这个 pod 的 spec 上,完成一次调度的过程。
  • pod 的 spec 上,nodeName 已经更新成了 Node1 这个 node,更新完 nodeName 之后,在 Node1 上的这台 kubelet 会 watch 到这个 pod 是属于自己节点上的一个 pod。
  • 它会把这个 pod 拿到节点上进行操作,包括创建一些容器 storage 以及 network,最后等所有的资源都准备完成,kubelet 会把状态更新为 Running,这样一个完整的调度过程就结束了。

概括一下调度过程:它其实就是在做一件事情,就是把 pod 放到合适的 node 上。

什么是合适呢?这里给出了几点合适定义的特点:

  • 首先要满足 pod 的资源要求
  • 其次要满足 pod 的一些特殊关系的要求
  • 再次要满足 node 的一些限制条件的要求
  • 最后还要做到整个集群资源的合理利用

资源调度

如何满足 Pod 资源要求

image-20220103202356614

resources 其实包含两个部分:第一部分是 request;第二部分是 limits。

这两部分里面的内容是一模一样的,但是它代表的含义有所不同:

  • request 代表的是对这个 pod 基本保底的一些资源要求;

  • limit 代表的是对这个 pod 可用能力上限的一种限制。

具体的 request、limit 的理念,其实都是一个 resources 的一个 map 结构,它里面可以填不同的资源的 key。

四大类的基础资源:

  • CPU 资源;
  • memory;
  • ephemeral-storage,一种临时存储;
  • 通用的扩展资源,比如说像 GPU。

img

QoS pod 服务质量配置

不同的 QoS 表现

不同的 Qos,它其实在调度和底层表现上都有一些不一样。

  • 调度表现,调度器只会使用 request 进行调度,也就是不管你配了多大的 limit,它都不会进行调度使用,它只会使用 request 进行调度。

  • 在底层上,它其实是按 request 来划分权重的,不同的 Qos,它的 request 是完全不一样的,比如说像 Burstable 和 BestEffort,它可能 request 可以填很小的数字或者不填,这样的话,它的权重其实是非常低的。像 BestEffort,它的权重可能是只有 2,而 Burstable 或 Guaranteed,它的权重可以多到几千。

当我们开启了 kubelet 的一个特性,叫 cpu-manager-policy=static 的时候,我们 Guaranteed Qos,

  • 如果它的 request 是一个整数的话,比如说配了 2,它会对 Guaranteed Pod 进行绑核。也就是具体像下面这个例子,它分配 CPU0 和 CPU1 给 Guaranteed Pod。

  • 非整数的 Guaranteed/Burstable/BestEffort,它们的 CPU 会放在一块,组成一个 CPU share pool。比如说像上面这个例子,这台节点假如说有 8 个核,已经分配了 2 个核给整数的 Guaranteed 绑核,那么剩下的 6 个核 CPU2~CPU7,它会被非整数的 Guaranteed/Burstable/BestEffort 共享,然后它们会根据不同的权重划分时间片来使用 6 个核的 CPU。

image-20220103210845462

memory

上也会按照不同的 Qos 进行划分:OOMScore。比如说

  • Guaranteed,它会配置默认的 -998 的 OOMScore;
  • Burstable 的话,它会根据内存设计的大小和节点的关系来分配 2-999 的 OOMScore。
  • BestEffort 会固定分配 1000 的 OOMScore

OOMScore 得分越高的话,在物理机出现 OOM 的时候会优先被 kill 掉。

eviction (驱逐)上

  • 不同的 Qos 也是不一样的,比如说发生 eviction 的时候,会优先考虑驱逐 BestEffort 的 pod。

所以不同的 Qos 其实在底层的表现是截然不同的。这也反过来要求我们在生产过程中,根据不同业务的要求和属性来配置资源的 Limits 和 Request,做到合理的规划 Qos Class。

资源 Quota

ResourceQuota 方法。它可以做到限制 namespace 资源用量

img

看到它的 spec 包括了一个 hard 和 scopeSelector。hard 内容其实和 Resourcelist 很像,这里可以填一些基础的资源。但是它比 ResourceList 更丰富一点,它还可以填写一些 Pod,这样可以限制 Pod 数量能力。然后 scopeSelector 为这个 Resource 方法定义更丰富的索引能力。

当我们创建了这样的 ResourceQuota 作用于集群,如果用户真的用超了资源,表现的行为是:它在提交 Pod spec 时,会收到一个 forbidden 的 403 错误,提示 exceeded quota。这样用户就无法再提交 cpu 或者是 memory,或者是 Pod 数量的资源。

假如再提交一个没有包含在这个 ResourceQuota 方案里面的资源,还是能成功的。这就是 Kubernetes 里 ResourceQuota 的基本用法。 我们可以用 ResourceQuota 方法来做到限制每一个 namespace 的资源用量,从而保证其他用户的资源使用。

总结如何满足 Pod 资源要求

Pod 要配置合理的资源要求

  • CPU/Memory/EphemeralStorage/GPU

通过 Request 和 Limit 来为不同业务特点的 Pod 选择不同的 QoS

  • Guaranteed:敏感型,需要业务保障

  • Burstable:次敏感型,需要弹性业务
  • BestEffort:可容忍性业务

为每个 NS 配置 ResourceQuota 来防止过量使用,保障其他人的资源可用

满足 Pod 与 Pod 关系要求

首先是 Pod 和 Pod 的关系调度。我们在平时使用中可能会遇到一些场景:比如说一个 Pod 必须要和另外一个 Pod 放在一起,或者不能和另外一个 Pod 放在一起。

Kubernetes 提供了两类能力:

  • 第一类能力称之为 Pod 亲和调度:PodAffinity;
  • 第二类就是 Pod 反亲和调度:PodAntAffinity。

亲和优先级

img

在这个例子中,必须要调度到带了 key: k1 的 Pod 所在的节点,并且打散粒度是按照节点粒度去打散索引的。这种情况下,假如能找到带 key: k1 的 Pod 所在节点,就会调度成功。假如这个集群不存在这样的 Pod 节点,或者是资源不够的时候,那就会调度失败。这是一个严格的亲和调度,我们叫做尝试亲和调度。

img

有些时候我们并不需要这么严格的调度策略。这时候可以把 required 改成 preferred,变成一个优先亲和调度。也就是优先可以调度带 key: k2 的 Pod 所在节点。并且这个 preferred 里面可以是一个 list 选择,可以填上多个条件,比如权重等于 100 的是 key: k2,权重等于 10 的是 key: k1。那调度器在调度的时候会优先把这个 Pod 分配到权重分更高的调度条件节点上去。

反亲和调度

上面介绍了亲和调度,而反亲和调度其实是与亲和调度比较像的。功能上是取反的,在语法上基本上是一样的,只是 podAffinity 换成了 podAntiAffinity,做到的效果也是 required 强制反亲和,以及一个 preferred 优先反亲和。

除了 In 这个 Operator 语法之外,还提供了更多丰富的语法组合来给大家使用。比如说 In/NotIn/Exists/DoesNotExist 这些组合方式。上图的例子用的是 In,比如说第一个强制反亲和例子里面,相当于我们必须要禁止调度到带了 key: k1 标签的 Pod 所在节点。

同样的功能也可以使用 Exists,Exists 范围可能会比 In 范围更大,当 Operator 填了 Exists,就不需要再填写 values。它做到的效果就是禁止调度到带了 key: k1 标签的 Pod 所在节点,不管 values 是什么值,只要带了 k1 这个 key 标签的 Pod 所在节点,都不能调度过去。

img

满足pod与node关系调度

Pod 与 Node 的关系调度又称之为 Node 亲和调度

NodeSelector label选择

img

场景:必须要调度 Pod 到带了 k1: v1 标签的 Node 上,这时可以在 Pod 的 spec 中填写一个 nodeSelector 要求。nodeSelector 其实是一个 map 结构,里面可以直接写上对 node 标签的要求,比如 k1: v1。这样我的 Pod 就会强制调度到带了 k1: v1 标签的 Node 上。

NodeAffinity 优先调度

img

它和 PodAffinity 有点类似,也提供了两类调度的策略:

  • 第一类是 required,必须调度到某一类 Node 上;
  • 第二类是 preferred,就是优先调度到某一类 Node 上。

它的基本语法和上文中的 PodAffinity 以及 PodAntiAffinity 也是类似的。在 Operator 上,NodeAffinity 提供了比 PodAffinity 更丰富的 Operator 内容。增加了 Gt 和 Lt,数值比较的玩法。当使用 Gt 的时候,values 只能填写数字。

Node 标记/容忍 禁止pod调度到Node

可以通过给 Node 打一些标记,来限制 Pod 调度到某些 Node 上。Kubernetes 把这些标记称之为 Taints,它的字面意思是污染。

img

限制 Pod 调度到某些 Node 上呢?比如说现在有个 node 叫 demo-node,这个节点有问题,我想限制一些 Pod 调度上来。这时可以给这个节点打一个 taints,taints 内容包括 key、value、effect:

  • key 就是配置的键值

  • value 就是内容

  • effect 是标记了这个 taints 行为是什么

目前 Kubernetes 里面有三个 taints 行为

  1. NoSchedule 禁止新的 Pod 调度上来;
  2. PreferNoSchedul 尽量不调度到这台;
  3. NoExecute 会 evict 没有对应 toleration 的 Pods,并且也不会调度新的上来。这个策略是非常严格的,大家在使用的时候要小心一点。

如上图绿色部分,给这个 demo-node 打了 k1=v1,并且 effect 等于 NoSchedule 之后。它的效果是:新建的 Pod 没有专门容忍这个 taint,那就没法调度到这个节点上去了。

假如有些 Pod 是可以调度到这个节点上的,应该怎么来做呢?这时可以在 Pod 上打一个 Pod Tolerations。从上图中蓝色部分可以看到:在 Pod 的 spec 中填写一个 Tolerations,它里面也包含了 key、value、effect,这三个值和 taint 的值是完全对应的,taint 里面的 key,value,effect 是什么内容,Tolerations 里面也要填写相同的内容。

Tolerations 还多了一个选项 Operator,Operator 有两个 value:Exists/Equal。Equal 的概念是必须要填写 value,而 Exists 就跟上文说的 NodeAffinity 一样,不需要填写 value,只要 key 值对上了,就认为它跟 taints 是匹配的。

上图中的例子,给 Pod 打了一个 Tolerations,只有打了这个 Tolerations 的 Pod,才能调度到绿色部分打了 taints 的 Node 上去。这样的好处是 Node 可以有选择性的调度一些 Pod 上来,而不是所有的 Pod 都可以调度上来,这样就做到了限制某些 Pod 调度到某些 Node 的效果。

总结

Pod/Node 的特殊关系和条件调度,来做一下小结

首先假如有需求是处理 Pod 与 Pod 的时候,比如 Pod 和另一个 Pod 有亲和的关系或者是互斥的关系,可以给它们配置下面的参数:

  • PodAffinity
  • PodAntiAffinity

假如存在 Pod 和 Node 有亲和关系,可以配置下面的参数:

  • NodeSelector
  • NodeAffinity

假如有些 Node 是限制某些 Pod 调度的,比如说一些故障的 Node,或者说是一些特殊业务的 Node,可以配置下面的参数:

  • Node – Taints
  • Pod – Tolerations

高级调度能力

优先级调度和抢占,主要概念有:

  • Priority
  • Preemption

调度过程提到的四个特点,我们如何做到集群的合理利用?当集群资源足够的话,只需要通过基础调度能力就能组合出合理的使用方式。但是假如资源不够,我们怎么做到集群的合理利用呢?通常的策略有两类:

  • 先到先得策略 (FIFO) -简单、相对公平,上手快
  • 优先级策略 (Priority) - 符合日常公司业务特点

在实际生产中,如果使用先到先得策略,是一种不公平的策略,因为公司业务里面肯定是有高优先级的业务和低优先级的业务,所以优先级策略会比先到先得策略更能够符合日常公司业务特点。

优先级抢占调度的一个流程

img

比如说有一个 Node 已经被一个 Pod 占用了,这个 Node 只有 2 个 CPU。另一个高优先级 Pod 来的时候,低优先级的 Pod 应该把这两个 CPU 让给高优先级的 Pod 去使用。低优先级的 Pod 需要回到等待队列,或者是业务重新提交。

在 Kubernetes 里,PodPriority 和 Preemption,就是优先级和抢占的特点,在 v1.14 版本中变成了 stable。并且 PodPriority 和 Preemption 默认都是开启的。

优先级调度配置

如何使用优先级调度呢?需要创建一个 priorityClass,然后再为每个 Pod 配置上不同的 priorityClassName,这样就完成了优先级以及优先级调度的配置。

img

首先来看一下如何创建一个 priorityClass。上图右侧定义了两个 demo:

  • 一个是创建名为 high 的 priorityClass,它是高优先级,得分为 10000;
  • 然后还创建了一个 low 的 priorityClass,它的得分是 100。

并且在第三部分给 Pod 配置上了 high,Pod2 上配置了 low priorityClassName,蓝色部分显示了 pod 的 spec 的配置位置,就是在 spec 里面填写一个 priorityClassName: high。这样 Pod 和 priorityClass 做完配置,就为集群开启了一个 priorityClass 调度。

内置优先级配置

Kubernetes 里面还内置了默认的优先级。如 DefaultpriorityWhenNoDefaultClassExistis,如果集群中没有配置 DefaultpriorityWhenNoDefaultClassExistis,那所有的 Pod 关于此项数值都会被设置成 0。

  • 用户可配置最大优先级限制:HighestUserDefinablePriority = 10000000000(10 亿)

  • 系统级别优先级:SystemCriticalPriority = 20000000000(20 亿)

内置系统级别优先级:

  • system-cluster-critical
  • system-node-critical

这就是优先级调度的基本配置以及内置的优先级配置。

优先级调度过程

只触发优先级调度但是没有触发抢占调度的流程

Pod1 高优先级,Pod2 低优先级。同时提交 Pod1 和 Pod2 到调度队列里。

img

  • 调度器处理队列的时候会挑选一个高优先级的 Pod1 进行调度,经过调度过程把 Pod1 绑定到 Node1 上。

  • 其次再挑选一个低优先的 Pod2 进行同样的过程,绑定到 Node1 上。

优先级抢占过程

高优先级的 Pod 在调度的时候没有资源,那么会是一个怎么样的流程呢

提前在 Node1 上放置了 Pod0,占去了一部分资源。同样有 Pod1 和 Pod2 待调度,Pod1 的优先级大于 Pod2。

img

  • 假如先把 Pod2 调度上去,它经过一系列的调度过程绑定到了 Node1 上。

  • 紧接着再调度 Pod1,因为 Node1 上已经存在了两个 Pod,资源不足,所以会遇到调度失败。

  • 在调度失败时 Pod1 会进入抢占流程,这时会进行整个集群的节点筛选,最后挑出要抢占的 Pod 是 Pod2,此时调度器会把 Pod2 从 Node1 上移除数据。

  • 再把 Pod1 调度到 Node1 上。这样就完成了一次抢占调度的流程。

优先级抢占策略

具体的抢占策略和抢占的流程是什么样的。

img

优先级抢占的调度流程,也就是 kube-scheduler 的工作流程

首先一个 Pod 进入抢占的时候,会判断 Pod 是否拥有抢占的资格,有可能上次已经抢占过一次。

如果符合抢占资格,它会先对所有的节点进行一次过滤,过滤出符合这次抢占要求的节点,如果不符合就过滤掉这批节点。

接着从过滤剩下的节点中,挑选出合适的节点进行抢占。这次抢占的过程会模拟一次调度,也就是把上面优先级低的 Pod 先移除出去,再把待抢占的 Pod 尝试能否放置到此节点上。然后通过这个过程选出一批节点。

进入下一个过程叫 ProcessPreemptionWithExtenders。这是一个扩展的钩子,用户可以在这里加一些自己抢占节点的策略,如果没有扩展的钩子,这里面是不做任何动作的。

接下来的流程叫做 PickOneNodeForPreemption,就是从上面 selectNodeForPreemption list 里面挑选出最合适的一个节点策略

  • 优先选择打破 PodDisruptionBudget(控制器可以设置应用POD集群处于运行状态最低个数,也可以设置应用POD集群处于运行状态的最低百分比) 最少的节点;
  • 其次选择待抢占 Pods 中最大优先级最小的节点;
  • 再次选择待抢占 Pods 优先级加和最小的节点;
  • 接下来选择待抢占 Pods 数目最小的节点;
  • 最后选择拥有最晚启动 Pod 的节点;

通过这五步串行策略过滤之后,会选出一个最合适的节点。然后对这个节点上待抢占的 Pod 进行 delete,这样就完成了一次待抢占的过程。