K8s预选策略和优选函数简介

调度器选择策略:
 预选策略(Predicate)
  1. 根据运行Pod的资源限制来排除不符合要求的Node
  2. 根据运行Pod时,是否要求共享宿主机的网络名称空间来判断,如: 某Pod启动要共享宿主机的网络名称空间,启动80端口,而某些Node的80已经被占用,那它就不符合,就也要不排除。

 优选策略(Priority):
  此阶段会经过一系列函数,会把预选出来的节点的相关属性都输入到这些函数中,通过计算来获取每个节点的优先分值,然后在倒序排序,取得分最高者,作为运行Pod的节点,若计算后,发现有多个Node得分相同,此时将随机选取一个Node作为该Pod运行的Node。

 选定(Select):
  当优选选出一个Node后,该Pod将会被绑定到该Node上运行。

  我们在定义一个Pod要运行在那个Node,是有一定偏向性的,比如有些Pod需要运行的有SSD硬盘的Node上,而有些Pod则需要运行在有GPU的节点上等,这时就需要我们给Node上打上不同的标签,来标识整个集群中不同Node的特性,这些特性包括,特有硬件标识,地理位置标识,不同机房标识等。

  我们使用kubectl explain pod.spec 可以看到其中有nodeName 和 nodeSelector 两个属性,第一个是明确指定Node节点名,我就要运行在这个Node上,第二个就是通过匹配Node标签,来选择运行的Node。

调度方式有以下几类:
  1. 节点倾向性(或叫 节点亲和性调度)
   这种调度通常会采用 nodeSelector 来完成调度。

  2. Pod倾向性 和 Pod非倾向性(这也称为Pod亲和性和非亲和性)
   有时候我们需要让某些Pod能运行在同一处时,就需要使用Pod亲和性来定义,如:我们要定义一个NMT(Nginx+Tomcat+MySQL),若这三个Pod分别被调度三个不同地域的机房中,比如北京,上海,深圳等地,那肯定不是我们所希望的,这时我们就需要使用Pod亲和性来定义这三个Pod能运行在相同的Node 或 相邻的Node上。
  而在另外一些情况下,我们又希望某些Pod一定不能运行在相同Node上,这时就需要使用Pod的非亲和性了。

  3. 污点 和 污点容忍调度
   Taints(污点):是定义在Node上的
   Tolerations(容忍): 是定义在Pod上的
    污点:指有时我们希望不将某些Pod调度到某些Node上,这时我们可对某些Node上打上污点,然后,当我们创建Pod时,我们可定义此Pod能容忍的污点范围。
    假如:NodeA上有5个污点: "吃喝嫖赌抽",而此次要调度的Pod比较大度,能容忍5个污点,而NodeA的污点正好是Pod能容忍污点的范围内,于是Pod就能"嫁入"该NodeA。
    假如,Pod运行到NodeA上后,后期我们又给该NodeA打上了新污点如:"坑蒙拐骗偷",此时Pod发现NodeA的恶性原来有这么多,怎么办?通常有两种选择策略:
    1. Pod继续运行在NodeA上"艰苦度日"
    2. Pod被驱逐,或者叫Pod逃离。

默认调度器的调度规则是:根据Node最大资源空闲率来进行均衡调度。

调度器:
 预选策略
  【注意:下面这些预选策略仅是其中一部分,并且k8s在进行预选时,它会是综合评估的,即所有启用的策略规则都要做判断,所得结果越符合就越会被挑选出来,进入优选策略中】:
  CheckNodeCondition: 检查节点是否符合调度条件
  GeneralPredicates:
  HostName: 这种是判断Pod是否定义了pod.spec.hostname属性,若定义了,就在预选时,看看这些Node上是否存在相同主机名的Pod,若有,就排除该Node; 这可能是因为,某些Pod是需要有固定主机名,才会使用。
  PodFitsHostPorts: 此预选策略是判断 pods.spec.containers.ports.hostPort 属性是否定义了,若定义了就表示该Pod要绑定到Node上指定的Port上,这时在进行预选时,就要判断这个端口是否被占用了,若占用就会排除该Node。
  MatchNodeSeletctor:此预选策略会判断 pods.spec.nodeSelector 属性是否定义了,若定义了就根据Pod所定义的NodeSelector来选出匹配指定标签的Node。
  PodFitsResources: 此预选策略会判断 Node上是否符合运行Pod所需的最小空闲资源。

  NoDiskConfict: 用于判断若Pod定义了存储卷,则要检查该存储卷在该Node上是否可用,若Node能满足Pod存储卷的使用需求,则表示此Node可用。
  PodToleratesNodeTaints:检查Pod上的spec.tolerations可容忍的污点是否完全包含Node上定义的污点,若完全包含,则表示Pod能容忍该Node节点的污点,否则该Node不会通过预选。
  PodToleratesNodeNoExecuteTaints:
   #首先,污点并非单一属性,它有三种属性,NoExcute是污点的一种属性。
   #此检查是,若Pod被调度到某Node上时,最初Node上没有Pod不能容忍的污点,但后来Node的污点改变了,多了Pod不能容忍的污点,这时要怎么处理那?默认是继承既定事实,继续在该Node上运行。
   # 第二种是不接受,也就是检查此属性,此时Node会驱逐该Pod,让该Pod重新调度到新Node上。

  CheckNodeLabelPresence:这是定义在选择Node时,检查Node上是否存在指定的标签,若存在则可调度。此规则默认不启用。
  CheckServiceAffinity: 当我们需要创建一些Pod时,这些Pod都使用相同的Serivce,这时,若启用该预选策略,则将这些Pod优先调度到 已存在较多该Service下的Pod的Node上。

  #下面三个默认启用,因为这三个是最大的三个云运营商
  MaxEBSVolumeCount:此预选策略是指若Pod需要使用亚马逊的EBS(弹性块存储服务),则检查该EBS是否已经达到最大运行挂载数39个,若没有,则表示还有存储空间可用。
  MaxGCEPDVolumeCount: GCEPD:是Google的云环境中的持久存储,默认是16个
  MaxAzureDiskVolumeCount: 最大是16个

  CheckVolumeBinding:检查Node上是否存在以绑定 和 未绑定的PVC,若Node上有PVC,并且没有被绑定则能满足Pod运行的需求。
  NoVolumeZoneConflict:它是在给定了区域限制的情况下,Zone在IDC机房是有限制的,他们通常会按房间,机柜,机架来划分范围,假如机架上有20台服务器,每2台一个区域,这个就是一个逻辑区域,此配置项的含义就是检查区域中Node上是否存在逻辑卷冲突。

  CheckNodeMemoryPressure:检查Node上是否存在内存压力,即若某Node上已经出现内存紧张的情况了,那就不要在往它上面调度了。
  CheckNodePIDPressure:检查Node上是否存在PID压力过大的情况,即若某Node上运行的进程过多,导致PID可能紧张,这时在选择调度时,也不会选择它。
  CheckNodeDiskPressure:检查Node上是否存在磁盘使用压力过大的情况

  MatchInterPodAffinity:检查Node上是否满足Pod的亲和性,假如Pod是Nginx,它是要为Tomcat Pod做代理的,那么在调度tomcat Pod时,就会检查Node上是否有Nginx Pod,若有这个非常亲和的Pod则优先调度到该Node上。



优选函数:
  默认启用了7个红色的优选函数,它们的得分相加值越高,该节点越优选被选中。
  LeastRequested 【与它相反对一个优选函数:MostRequested,它是Node资源占用越高得分越高,有点像是,先将某Node的资源先全部占满的意味,然后空出部分Node.】
     计算得分的算法公式=(cpu((capacity - sum(requested))*10 / capacity) + memory((capacity - sum(requested))*10 / capacity))/2
       此公式的含义:
      假如当前有16个核心,已经用了8个核心,那么总的使用率就是 (16-8)/16 = 0.5 ,而乘以10,是为了计算得分方便,将其转化为0~10之间的数,
      而内存的计算含义一样,假如内存32G,已经用了8G,则使用率为 (32-8)/32 = 0.75 ,乘以10
      将CPU + Memory/2 就可以得到一个平均值。该平均值越高,表示该节点的资源空闲率越高。

  BalanceResourceAllocation:
    它表示CPU和内存资源占用率的相近程度,作为评估标准,越接近也优选选择,比如: Node1的CPU和内存使用率一个48%一个50%,而Node2的CPU和内存使用率都是50%,则Node2胜出。
  #它通常要和LeastRequested 结合来评估Node的资源利用率的。
  #两个组合主要是为了平衡各Node上资源的利用率。

NodePreferAvoidPods
  根据Node节点上是否定义了注解信息"scheduler.alpha.kubernetes.io/preferAvoidPods",若没有则该Node得分为10,而且权重为1万,有此注解则说明该Node是适合运行Pod的。而对于那些有Replicas Controller的控制器则,该Node的得分为0,此时则表示Pod不能运行在该Node上,但这不能决定Pod一定不能运行在该Node上,因为最终是要看总体得分的。
  注解信息:
    # kubectl describe node node1.zcf.com
      Name: node1.zcf.com
      Roles: node
      ..........
      Annotations: node.alpha.kubernetes.io/ttl: 0      #这就是所谓的node上的注解信息。
      volumes.kubernetes.io/controller-managed-attach-detach: true

TaintToleration
  基于Pod对调度Node上的污点容忍度来评估是否可调度到该Node上。
  简单说:就是取出Pod对象中定义的spec.tolerations列表,查看其中能容忍的污点,然后,一个一个去对比Node上存在的污点,若匹配的越多,则该Node的得分越低,就越不会调度到该Node上。

SelectorSpreading
  此优化函数是希望将相同标签选择器选取的Pod尽可能的散开到多个Node上。因此假如:ReplicaSet控制器,已经在B上创建了一个Pod,那么它要再创建一个Pod时,此优选函数在计算A,B,C三个Node的得分时,会根据那个Node上拥有此ReplicaSet控制器的标签选择器所匹配的Pod数量,来评分,数量越少,分数越高,反之越低。
  而使用标签选择器的资源对象有:Service,Replication Controller,ReplicaSet,StatefulSet。

InterPodAffinity
  遍历对象的亲和性条目,并将能够匹配到给定节点的条目的条目数相加结果值越大得分越高。
  简单说:NodeA上运行了3个Pod,NodeB上运行了6个Pod,NodeC上运行了5个Pod,现在要调度一个Pod到其中一个Node上,而该Pod的比较亲和某类Pod,此优选函数就会将这些Node上,所有匹配该Pod亲和条目的数量相加,值越大,则越得分越高。其中条目的理解,我还不是很懂。

NodeAffinity
  它是根据pod中定义的nodeSeletor来对Node做亲和性检查, 能成功匹配的数量越多,则得分越高。

NodeLabel:
  根据Node是否有某些标签来做评估,然后计算得分的,此优选函数只关注标签,不关注值,只有Node上有这个标签就可以得分。

ImageLocality:
  它评分的标准是,判断Node上是否有运行该Pod的镜像,若有则得分,否则不得分。
  但它评估镜像是根据镜像总的体积大小来计算得分的,例如:NodeA上有1个镜像1G,NodeB上有2镜像,NodeC上有3个镜像,而运行此Pod需要4个镜像,其中一个镜像1G,其它都是比较小,这时NodeA的得分会最高。

#实验:
  1. 定义一个Pod,并设置其nodeSelector,若指定的nodeSeletor标签没有匹配到任何Node,则Pod将处于Pinding状态,只有你给某个Node打上指定的标签和值后,该Pod才会被调度上去。

#实验2:
  kubectl explain pods.spec.affinity
    nodeAffinity: 定义Node的亲和性
    podAffinity: 定义Pod的亲和性
    podAntiAffinity:定义Pod非亲和性

  nodeAffinity:
   # 软亲和性,即若能满足则一定运行在满足条件的Node上,否则运行在其它Node上也不是不可以。
   preferredDuringSchedulingIgnoredDuringExecution

   #硬亲和性,若不能满足运行条件,则不运行Pod。
   requiredDuringSchedulingIgnoredDuringExecution
   nodeSelectorTerms:
    matchExpressions :     这是更强大的一种方式,它是配表达式的。

    matchFields:
      key: 要对那个label key做匹配操作
      operator:指定你是做什么比较操作,支持: In/NotIn(包含/不包含), Exists/DoesNotExist(存在/不存在) , Gt/Lt(大于/小于)
      values:若操作符为Exists/DoesNotExists则,values必须为空。

vim  pod-node-required-affinity.yaml
    apiVersion: v1
    kind: Pod
    metadata:
       name: pod-node-affinity-1
       labels:
             app: myapp
             tier: frontend
    spec:
      containers:
      -  name: myapp
         image: ikubernetes/myapp:v1
      affinity:
         nodeAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
             nodeSelectorTerms:
             -  matchExpressions:
                - key: zone
                  operator: In
                  values:
                  - foo
                  - bar

#默认node上没有打上zone=foo 或 zone=bar的标签,此时是硬亲和,若不能满足条件,则无法调动.


vim  pod-node-affinity.yaml
    apiVersion: v1
    kind: Pod
    metadata:
       name: pod-node-affinity-1
       labels:
             app: myapp
             tier: frontend
    spec:
      containers:
      -  name: myapp
         image: ikubernetes/myapp:v1
      affinity:
         nodeAffinity:
           preferredDuringSchedulingIgnoredDuringExecution:
           -  preference:
                matchExpressions:
                - key: zone
                  operator: In
                  values:
                  - foo
                  - bar
              weight: 60

# 此为软亲和,若node上有zone=foo 或 zone=bar,则优先调度上去,若全部都没有,勉强选一个调度上去也不是不可以

 

为啥有了节点亲和性,还要有Pod亲和性?
  假如我们有一个分布式跨地域的大集群,现在想构建一个NMT的架构,为了实现这个目的,我完全可以给Node上打标签,然后,让NMT这些Pod都匹配这些标签,然后被调度到这些Node上去运行,这不是也可以实现我们的目的吗?
  看上去没有问题,但问题是,假如你跨地域的分布式K8s集群,分别分布在北京,上海,深圳,杭州等地,你就不得不精心规划不同地域,不同IDC,不同机房,不同机柜的Node标签规划,否则你很可能在将来实现这个目的是变得困难不堪,你可以想象一下,你希望让一组NMT运行在北京某IDC的某机房中,这样一个NMT架构在工作时,才能更加高效的通信,你想实现这样的控制,你不规划Node的标签,你让预选策略如何按照你的想法去调度那?这还仅是一方面,你为了NMT架构能分离开,北京一套,上海一套.....你也要配置Pod的在选择Node标签时,设定它亲和哪些标签,所以工作量你可自己评估。

  Pod亲和性的逻辑时,我要部署NMT环境,我的MySQL Pod第一个被调用,我不管调度器会把MySQL Pod调度到哪里,反正只要调度完成,并且运行了,我后续的Nginx,Tomcat的Pod是亲和MySQL Pod的,MySQL Pod在哪个Node上,那在调度NT时,就更加优先调度到那个Node上。
  这样说看上去很简单,但仔细想想,MySQL被调度到某Node上,要是将NT也调度到那个Node上合适吗?若那个Node没有这么多资源运行它们怎么办?能不能将NT调度到M所在Node旁边的Node上,或同机房的Node中?那这算亲和吗? 其实也算,但怎么知道运行MySQL Pod的Node ,它旁边的Node是否那个主机?这其实还是需要借助于Node标签来实现,因此为集群标签的规划是在所难免的,因为你必须给预选策略一种判断标准,哪些是相同机柜,哪些是不同机房对吧,否则鬼知道集群中那么多Node那个和那个是邻居对吧,当然若你靠主机名来判断也不是不可以,你就要定义根据主机名的判断标准了。所以,比较通用的方法是,给每个Node打上不同的标签,如:北京机房1 机柜20 机架号等等来定义,这样根据这些标签值,就可以判断Node和Node之间的临近关系了。
  打个比方: MySQL Pod被调度到 rack=bjYZ1 ,那后续调度NT时,就会优选rack=bjYZ1的Node,只要有这个标签值得Node会被优选调度,这样NMT它们就都可以运行在北京亦庄的机房中了。

  kubectl pods.spec.affinity.podAffinity.
  requiredDuringSchedulingIgnoredDuringExecution : 硬限制
  labelSelector: 指定亲和Pod的标签
  namespaces: 你要指定亲和Pod,你就需要指定你亲和的Pod属于哪个名称空间,否则默认是Pod创建在那个名称空间,就匹配那个名称空间的Pod

Pod亲和性示例:
vim  pod-required-affinity.yaml
  apiVersion:  v1
  kind:  Pod
  metadata:
   name:  web1-first
   labels:
     app:  web1
     tier:  frontend
 spec:
   containers:
   -  name:  myapp
      image:  harbor.zcf.com/k8s/myapp:v1
 
 ---
 apiVersion:  v1
 kind:  Pod
 metadata:
  name:  db1-second
  labels:
     app:  db1
     tier:  db1
spec:
   containers:
   - name:  busybox
     image:  busybox:  latest
     imagePullPolicy:  IfNotPresent
     command:  ["sh", "-c", "sleep  3600"]
   affinity:
      podAffinity:     #若需要测试Pod的反亲和性,只需要修改podAffinity为 podAntiAffinity 即可测试
        requiredDuringSchedulingIgnoredDuringExecution:
        -  labelSelector:
              matchExpressions:
               -  {key: app,  operator: In,  values: ["web1","web2"]}
           topologyKey:  kubernetes.io/hostname   #通过默认Node上主机名标签来临时代表地区标签,即:只有主机名相同,就认为是在相同位置的。
    

#Pod反亲和测试:
Pod亲和性示例:
vim  pod-required-affinity.yaml
  apiVersion:  v1
  kind:  Pod
  metadata:
    name:  web1-first
    labels:
      app:  web1
      tier:  frontend
 spec:
   containers:
   - name:  myapp
     image:  harbor.zcf.com/k8s/myapp:v1
 
---
apiVersion:  v1
kind:  Pod
metadata:
   name:  db1-second
   labels:
      app:  db1
      tier:  db1
spec:
  containers:
  - name:  busybox
    image:  busybox:  latest
    imagePullPolicy:  IfNotPresent
    command:  ["sh", "-c", "sleep  3600"]
    affinity:
      podAntiAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
        -  labelSelector:
             matchExpressions:
               -   {key: app,  operator: In,  values: ["web1","web2"]}
             topologyKey:  kubernetes.io/hostname        
             #第一次测试: 使用存在的标签来测试,web1会运行在一个节点上,db1一定不会运行在web1所在的Node上。
             topologyKey:  zone     
                 #第二次测试: 使用此key,因为两个Node上都没有此标签key,因此预选策略将认为,没有找到不同区域的Node,因此db1将处于Pinding状态,无法启动。
vim  myapp-toleration.yaml
  apiVersion: apps/v1
  kind: Deployment
  metadata:
       name: myapp-toler1
  spec:
    replicas: 2
    selector:
      matchLabels:
        app: myapp
        release: canary
    template:
      metadata:
         labels:
           app: myapp
           release: canary
      spec:
        containers:
        -  name: myapp
           image: ikubernetes/myapp:v2
           ports:
           -  name: http
              containerPort: 80
        tolerations:
        -  key: "node-type"
           operator: "Equal"
           value: "production"
           effect: "NoSchedule"
  #说明: 在定义Pod的容忍度时,若指定为NoSchedule,则不能定义tolerationSeconds,即容忍不了时,可宽限多久被驱逐。
  #      若定义为NoExecute 则可以定义tolerationSeconds。
  #     Equal:做等值比较,即node-type标签 和 其值必须完全匹配。
  #     Exists :做存在性比较,即只要node-type标签存在,即可匹配,不看其标签值是否相同。

  #测试NoExecute影响
  #目前node2.zcf.com 上定义了node-type污点的影响为: NoExecute
     ............
     tolerations:
     -  key:  "node-type"
         operator:  "Exists"
         value:  ""             #设置node-type的值为空,因为Exists是做标签判断,只要node-type标签存在,即可.
         effect:  "NoSchedule"  
 #定义污点影响度为NoSchedule,即若Node上没有这个node-type污点标签,就不调度到其上.
 #若Node上污点影响(effect)为NoExecute,它是不包含NoSchedule的,即Node是不允许不能容忍NoExecute的Pod调度到自己上面的。


    #若Pod能容忍node-type标签的污点,无论它是什么值,什么影响,都可以容忍
    ...............
    tolerations:
    -   key:  "node-type"
      operator:  "Exists"
      value:  ""
      effect:   ""


 #删除节点上的污点:
     kubectl  taint  node   node01.zcf.com  node-type-   
         #注意:node-type是:node01上的标签,最后的“-”是删除该标签的意思。

 

posted @ 2019-08-02 19:17  张朝锋  阅读(2642)  评论(0编辑  收藏  举报