gitbook/深入剖析Kubernetes/docs/70211.md
2022-09-03 22:05:03 +08:00

13 KiB
Raw Permalink Blame History

42 | Kubernetes默认调度器调度策略解析

你好我是张磊。今天我和你分享的主题是Kubernetes默认调度器调度策略解析。

在上一篇文章中我主要为你讲解了Kubernetes默认调度器的设计原理和架构。在今天这篇文章中我们就专注在调度过程中Predicates和Priorities这两个调度策略主要发生作用的阶段。

首先我们一起看看Predicates。

Predicates在调度过程中的作用可以理解为Filter它按照调度策略从当前集群的所有节点中“过滤”出一系列符合条件的节点。这些节点都是可以运行待调度Pod的宿主机。

而在Kubernetes中默认的调度策略有如下四种。

第一种类型叫作GeneralPredicates。

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

当然我在前面已经提到过PodFitsResources检查的只是 Pod 的 requests 字段。需要注意的是Kubernetes 的调度器并没有为 GPU 等硬件资源定义具体的资源类型,而是统一用一种名叫 Extended Resource的、Key-Value 格式的扩展字段来描述的。比如下面这个例子:

apiVersion: v1
kind: Pod
metadata:
  name: extended-resource-demo
spec:
  containers:
  - name: extended-resource-demo-ctr
    image: nginx
    resources:
      requests:
        alpha.kubernetes.io/nvidia-gpu: 2
      limits:
        alpha.kubernetes.io/nvidia-gpu: 2

可以看到,我们这个 Pod 通过alpha.kubernetes.io/nvidia-gpu=2这样的定义方式,声明使用了两个 NVIDIA 类型的 GPU。

而在PodFitsResources里面调度器其实并不知道这个字段 Key 的含义是 GPU而是直接使用后面的 Value 进行计算。当然,在 Node 的Capacity字段里你也得相应地加上这台宿主机上 GPU的总数比如alpha.kubernetes.io/nvidia-gpu=4。这些流程,我在后面讲解 Device Plugin 的时候会详细介绍。

而PodFitsHost检查的是宿主机的名字是否跟Pod的spec.nodeName一致。

PodFitsHostPorts检查的是Pod申请的宿主机端口spec.nodePort是不是跟已经被使用的端口有冲突。

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

可以看到像上面这样一组GeneralPredicates正是Kubernetes考察一个Pod能不能运行在一个Node上最基本的过滤条件。所以GeneralPredicates也会被其他组件比如kubelet直接调用。

我在上一篇文章中已经提到过kubelet在启动Pod前会执行一个Admit操作来进行二次确认。这里二次确认的规则就是执行一遍GeneralPredicates。

第二种类型是与Volume相关的过滤规则。

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

其中NoDiskConflict检查的条件是多个Pod声明挂载的持久化Volume是否有冲突。比如AWS EBS类型的Volume是不允许被两个Pod同时使用的。所以当一个名叫A的EBS Volume已经被挂载在了某个节点上时另一个同样声明使用这个A Volume的Pod就不能被调度到这个节点上了。

而MaxPDVolumeCountPredicate检查的条件则是一个节点上某种类型的持久化Volume是不是已经超过了一定数目如果是的话那么声明使用该类型持久化Volume的Pod就不能再调度到这个节点了。

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

此外这里还有一个叫作VolumeBindingPredicate的规则。它负责检查的是该Pod对应的PV的nodeAffinity字段是否跟某个节点的标签相匹配。

在前面的第29篇文章《PV、PVC体系是不是多此一举从本地持久化卷谈起》我曾经为你讲解过Local Persistent Volume本地持久化卷必须使用nodeAffinity来跟某个具体的节点绑定。这其实也就意味着在Predicates阶段Kubernetes就必须能够根据Pod的Volume属性来进行调度。

此外如果该Pod的PVC还没有跟具体的PV绑定的话调度器还要负责检查所有待绑定PV当有可用的PV存在并且该PV的nodeAffinity与待考察节点一致时这条规则才会返回“成功”。比如下面这个例子

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-local-pv
spec:
  capacity:
    storage: 500Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - my-node

可以看到,这个 PV 对应的持久化目录,只会出现在名叫 my-node 的宿主机上。所以,任何一个通过 PVC 使用这个 PV 的 Pod都必须被调度到 my-node 上才可以正常工作。VolumeBindingPredicate正是调度器里完成这个决策的位置。

第三种类型,是宿主机相关的过滤规则。

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

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

备注这里你也可以再回顾下第21篇文章《容器化守护进程的意义DaemonSet》中的相关内容。

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

第四种类型,是 Pod 相关的过滤规则。

这一组规则,跟 GeneralPredicates大多数是重合的。而比较特殊的是PodAffinityPredicate。这个规则的作用是检查待调度 Pod 与 Node 上的已有Pod 之间的亲密affinity和反亲密anti-affinity关系。比如下面这个例子

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-antiaffinity
spec:
  affinity:
    podAntiAffinity: 
      requiredDuringSchedulingIgnoredDuringExecution: 
      - weight: 100  
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security 
              operator: In 
              values:
              - S2
          topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: docker.io/ocpqe/hello-pod

这个例子里的podAntiAffinity规则就指定了这个 Pod 不希望跟任何携带了 security=S2 标签的 Pod 存在于同一个 Node 上。需要注意的是PodAffinityPredicate是有作用域的比如上面这条规则就仅对携带了Key 是kubernetes.io/hostname标签的 Node 有效。这正是topologyKey这个关键词的作用。

而与podAntiAffinity相反的就是podAffinity比如下面这个例子

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity: 
      requiredDuringSchedulingIgnoredDuringExecution: 
      - labelSelector:
          matchExpressions:
          - key: security 
            operator: In 
            values:
            - S1 
        topologyKey: failure-domain.beta.kubernetes.io/zone
  containers:
  - name: with-pod-affinity
    image: docker.io/ocpqe/hello-pod

这个例子里的 Pod就只会被调度到已经有携带了 security=S1标签的 Pod 运行的 Node 上。而这条规则的作用域,则是所有携带 Key 是failure-domain.beta.kubernetes.io/zone标签的 Node。

此外上面这两个例子里的requiredDuringSchedulingIgnoredDuringExecution字段的含义是这条规则必须在Pod 调度时进行检查requiredDuringScheduling但是如果是已经在运行的Pod 发生变化,比如 Label 被修改,造成了该 Pod 不再适合运行在这个 Node 上的时候Kubernetes 不会进行主动修正IgnoredDuringExecution

上面这四种类型的Predicates就构成了调度器确定一个 Node 可以运行待调度 Pod 的基本策略。

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

需要注意的是,在为每个 Node 执行 Predicates 时,调度器会按照固定的顺序来进行检查。这个顺序,是按照 Predicates 本身的含义来确定的。比如宿主机相关的Predicates 会被放在相对靠前的位置进行检查。要不然的话,在一台资源已经严重不足的宿主机上,上来就开始计算 PodAffinityPredicate是没有实际意义的。

接下来,我们再来看一下 Priorities。

在 Predicates 阶段完成了节点的“过滤”之后Priorities 阶段的工作就是为这些节点打分。这里打分的范围是0-10分得分最高的节点就是最后被 Pod 绑定的最佳节点。

Priorities 里最常用到的一个打分规则是LeastRequestedPriority。它的计算方法可以简单地总结为如下所示的公式

score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2

可以看到这个算法实际上就是在选择空闲资源CPU 和 Memory最多的宿主机。

而与LeastRequestedPriority一起发挥作用的还有BalancedResourceAllocation。它的计算公式如下所示

score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10

其中,每种资源的 Fraction 的定义是 Pod 请求的资源/节点上的可用资源。而 variance 算法的作用,则是计算每两种资源 Fraction 之间的“距离”。而最后选择的,则是资源 Fraction 差距最小的节点。

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

此外还有NodeAffinityPriority、TaintTolerationPriority和InterPodAffinityPriority这三种 Priority。顾名思义它们与前面的PodMatchNodeSelector、PodToleratesNodeTaints和 PodAffinityPredicate这三个 Predicate 的含义和计算方法是类似的。但是作为 Priority一个 Node 满足上述规则的字段数目越多,它的得分就会越高。

在默认 Priorities 里还有一个叫作ImageLocalityPriority的策略。它是在 Kubernetes v1.12里新开启的调度规则,即:如果待调度 Pod 需要使用的镜像很大,并且已经存在于某些 Node 上那么这些Node 的得分就会比较高。

当然,为了避免这个算法引发调度堆叠,调度器在计算得分的时候还会根据镜像的分布进行优化,即:如果大镜像分布的节点数目很少,那么这些节点的权重就会被调低,从而“对冲”掉引起调度堆叠的风险。

以上,就是 Kubernetes 调度器的 Predicates 和 Priorities 里默认调度规则的主要工作原理了。

在实际的执行过程中,调度器里关于集群和 Pod 的信息都已经缓存化,所以这些算法的执行过程还是比较快的。

此外对于比较复杂的调度算法来说比如PodAffinityPredicate它们在计算的时候不只关注待调度 Pod 和待考察 Node还需要关注整个集群的信息比如遍历所有节点读取它们的 Labels。这时候Kubernetes 调度器会在为每个待调度 Pod 执行该调度算法之前,先将算法需要的集群信息初步计算一遍,然后缓存起来。这样,在真正执行该算法的时候,调度器只需要读取缓存信息进行计算即可,从而避免了为每个 Node 计算 Predicates 的时候反复获取和计算整个集群的信息。

总结

在本篇文章中,我为你讲述了 Kubernetes 默认调度器里的主要调度算法。

需要注意的是除了本篇讲述的这些规则Kubernetes 调度器里其实还有一些默认不会开启的策略。你可以通过为kube-scheduler 指定一个配置文件或者创建一个 ConfigMap ,来配置哪些规则需要开启、哪些规则需要关闭。并且,你可以通过为 Priorities 设置权重,来控制调度器的调度行为。

思考题

请问,如何能够让 Kubernetes 的调度器尽可能地将 Pod 分布在不同机器上,避免“堆叠”呢?请简单描述下你的算法。

感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。