五分钟了解k8s调度器kube-scheduler

五分钟了解k8s调度器kube-scheduler

转自大数据开放实验室

kube-scheduler简介


调度是容器编排的重要环节,需要经过严格的监控和控制,现实生产通常对调度有各类限制,譬如某些服务必须在业务独享的机器上运行,或者从灾备的角度考虑尽量把服务调度到不同机器,这些需求在Kubernetes集群依靠调度组件kube-scheduler满足。

kube-scheduler是Kubernetes中的关键模块,扮演管家的角色遵从一套机制为Pod提供调度服务,例如基于资源的公平调度、调度Pod到指定节点、或者通信频繁的Pod调度到同一节点等。容器调度本身是一件比较复杂的事,因为要确保以下几个目标:

  1. 公平性:在调度Pod时需要公平的进行决策,每个节点都有被分配资源的机会,调度器需要对不同节点的使用作出平衡决策。
  2. 资源高效利用:最大化群集所有资源的利用率,使有限的CPU、内存等资源服务尽可能更多的Pod。
  3. 效率问题:能快速的完成对大批量Pod的调度工作,在集群规模扩增的情况下,依然保证调度过程的性能。
  4. 灵活性:在实际运作中,用户往往希望Pod的调度策略是可控的,从而处理大量复杂的实际问题。因此平台要允许多个调度器并行工作,同时支持自定义调度器。

为达到上述目标,kube-scheduler通过结合Node资源、负载情况、数据位置等各种因素进行调度判断,确保在满足场景需求的同时将Pod分配到最优节点。显然,kube-scheduler影响着Kubernetes集群的可用性与性能,Pod数量越多集群的调度能力越重要,尤其达到了数千级节点数时,优秀的调度能力将显著提升容器平台性能。

调度流程


kube-scheduler的根本工作任务是根据各种调度算法将Pod绑定(bind)到最合适的工作节点,整个调度流程分为两个阶段:预选策略(Predicates)和优选策略(Priorities)。

  1. 预选(Predicates):输入是所有节点,输出是满足预选条件的节点。kube-scheduler根据预选策略过滤掉不满足策略的Nodes。例如,如果某节点的资源不足或者不满足预选策略的条件如“Node的label必须与Pod的Selector一致”时则无法通过预选。
  2. 优选(Priorities):输入是预选阶段筛选出的节点,优选会根据优先策略为通过预选的Nodes进行打分排名,选择得分最高的Node。例如,资源越富裕、负载越小的Node可能具有越高的排名。

通俗点说,调度的过程就是在回答两个问题:1. 候选有哪些?2. 其中最适合的是哪个?

值得一提的是,如果在预选阶段没有节点满足条件,Pod会一直处在Pending状态直到出现满足的节点,在此期间调度器会不断的进行重试。

到这里我们可以对Pod的整个启动流程进行总结:

  1. 资源管控中心Controller Manager创建新的Pod,将该Pod加入待调度的Pod列表。
  2. kube-scheduler通过API Server提供的接口监听Pods,获取待调度pod,经过预选和优选两个阶段对各个Node节点打分排序,为待调度Pod列表中每个对象选择一个最优的Node。
  3. kube-scheduler将Pod与Node的绑定写入etcd(元数据管理服务)。
  4. 节点代理服务kubelet通过API Server监听到kube-scheduler产生的绑定信息,获得Pod列表,下载Image并启动容器,然后由kubelet负责拉起Pod。

到此为止就完成了Pod的调度与启动。



预选和优选算法介绍

kube-scheduler支持多种预选和优选算法,开发者可以根据当前场景下的重要要素进行选择。

预选策略(Predicates)

  1. 基于存储卷数量的判断
  • MaxEBSVolumeCount:确保已挂载的EBS存储卷数量不超过设置的最大值(默认39),调度器会检查直接或及间接使用这种类型存储的PVC,累加总数,如果卷数目超过设最大值限制,则不能调度新Pod到这个节点上。
  • MaxGCEPDVolumeCount:同上,确保已挂载的GCE存储卷数量不超过预设的最大值(默认16)。
  • MaxAzureDiskVolumeCount:同上,确保已挂载的Azure存储卷不超过设置的最大值(默认16)。
  • 基于资源压力状态的判断
    • CheckNodeMemoryPressure:判断节点是否已经进入到内存压力状态,如果是则只允许调度内存为0标记的Pod。
    • CheckNodeDiskPressure:判断节点是否已经进入到磁盘压力状态,如果是,则不能调度新的Pod。
  • 基于卷冲突的判断
    • NoDiskConflict:卷冲突判断,即如果该节点已经挂载了某个卷,其它同样使用相同卷的Pod将不能再调度到该节点。
    • NoVolumeZoneConflict:对于给定的某块区域,判断如果在此区域的节点上部署Pod是否存在卷冲突。
    • NoVolumeNodeConflict:对于某个指定节点,检查如果在此节点上部署Pod是否存在卷冲突。
  • 基于约束关系的判断
    • MatchNodeSelector:检查节点标签(label)是否匹配Pod指定的nodeSelector,是则通过预选。
    • MatchInterPodAffinity:根据Pod之间的亲和性做判断。
    • PodToleratesNodeTaints:排斥性关系,即判断Pod不允许被调度到哪些节点。这里涉及到两个概念Taints(污点)和Toleration(容忍)。Node可以定义一或多个Taint,Pod可以定义一或多个Toleration,对于具有某个Taint的节点,只有遇到能容忍它的(即带有对应Toleration的)Pod,才允许Pod被调度到此节点,从而避免Pod被分配到不合适的节点。
  • 基于适合性的判断
    • PodFitsResources:检查节点是否有足够资源(如CPU、内存、GPU等)满足Pod的运行需求。
    • PodFitsHostPorts:检查Pod容器所需的HostPort是否已被节点上其它容器或服务占用。如果已被占用,则禁止Pod调度到该节点。
    • PodFitsHost:检查Pod指定的NodeName是否匹配当前节点。

    优选策略(Priorities)

    优选过程会根据优选策略对每个候选节点进行打分,最终把Pod调度到分值最高的节点。kube-scheduler用一组优先级函数处理每个通过预选的节点,每个函数返回0-10的分数,各个函数有不同权重,最终得分是所有优先级函数的加权和,即节点得分的计算公式为:

    优选的优先级函数包括:

    • LeastRequestedPriority(默认权重1):尽量将Pod调度到计算资源占用比较小的Node上,这里涉及两种计算资源:内存和CPU。计算公式如下:其中,capacity表示该节点的现有容量,requested表示Pod所请求的容量。


    • BalancedResourceAllocation(默认权重1):CPU和内存使用率越接近的节点权重越高。该策略均衡了节点CPU和内存的配比,尽量选择在部署Pod后各项资源更均衡的机器。该函数不能单独使用,必须和LeastRequestedPriority同时使用,因为如果请求的资源(CPU或者内存)大于节点的capacity,那么该节点永远不会被调度到。计算公式如下:
    • SelectorSpreadPriority(默认权重1):把属于同一个Service或者ReplicationController的Pod,尽量分散在不同的节点上,如果指定了区域,则尽量把Pod分散在该区域的不同节点。通常来说节点上已运行的Pod越少,节点分数越高。计算公式如下,是基于节点的计算和基于区域的计算的加权和,其中,maxPriority代表系数,默认为10,maxCount为节点最多允许运行的Pod数量,nodeCount为该节点已经存在的Pod数量,maxCountByZone为该区域最多允许的Pod数量,zoneCount为区域内已经运行的Pod数量。



    • NodeAffinityPriority(默认权重1):尽量调度到标签匹配Pod属性要求的节点,判断行为与预选中的MatchNodeSelector相似,未来可能会完全将其取代。
      该函数提供两种选择器:requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution。前者是强要求,指定将Pod调度到节点上必须满足的所有规则;后者是弱要求,即尽可能调度到满足特定限制的节点,但允许不满足。计算公式如下,在节点满足requiredDuringSchedulingIgnoredDuringExecution后,该节点会累加preferredDuringSchedulingIgnoredDuringExecution中所有满足条件的规则的权值weight_i,CountWeight为preferredDuringSchedulingIgnoredDuringExecution中所有规则的权值总和:
    • InterPodAffinityPriority(默认权重1):叠加该节点已调度的每个Pod的权重,权重可配置文件里设定,通常亲和性越强的Pod权重越高,如果Pod满足要求则将加到权重和,具有最高权重和的节点是最优的。提供两种选择器:requiredDuringSchedulingIgnoredDuringExecution(保证所选的主机必须满足所有Pod对主机的规则要求)、preferredDuringSchedulingIgnoredDuringExecution(调度器会尽量但不保证满足NodeSelector的所有要求)。
      计算公式如下,其中,weight_i为节点上符合亲和性的每个Pod的权重,sumCount为节点上符合亲和性的Pod权重和,maxCount为节点上所有Pod的权重和,minCount为节点上最小的Pod权重,maxPriority是系数,默认为10。



    • NodePreferAvoidPodsPriority(默认权重10000):避免将ReplicationController或ReplicaSet调度到节点。如果Pod由RC或者RS的Controller调度,则得分为0,不对最终加权得分产生影响;如果不是,则总分为100000,意味着覆盖其他策略,直接决定最优节点。
    • TaintTolerationPriority(默认权重1):Pod与Node的排斥性判断。通过Pod的tolerationList与节点Taint进行匹配,配对失败的项越少得分越高,类似于Predicates策略中的PodToleratesNodeTaints。计算公式如下:其中,totalTaints表示Taint总个数,intolerableTaints表示配对不成功的个数。


    • ImageLocalityPriority(默认权重1):尽量调度到Pod所需镜像的节点。检查Node是否存在Pod所需镜像:如果不存在,返回0分;如果存在,则镜像越大得分越高。计算公式如下:其中,sumSize表示该节点上存在的Pod所需镜像大小总和,maxImgSize表示Pod所需镜像总大小,minImgSize表示Pod所需最小镜像的尺寸。
    • EqualPriority(默认权重1):给予所有节点相等权重,一般仅用于测试。
    • MostRequestedPriority(默认权重1):适用于动态伸缩集群环境,会优先调度Pod到使用率最高的节点,方便在伸缩集群时,先腾出空闲机器,从而进行停机处理。

    案例演示

    下面将通过四个案例来介绍如何在实战中对以上的调度策略进行选择。

    • 场景一:调度到基于SSD的节点
      某架构使用分布式缓存Redis,对磁盘读写敏感,开发者希望调度到配置SSD的节点,可用预选策略MatchNodeSelector解决,对目标Node设置label,Redis的spec添加nodeSelector。

    假设Redis的YAML文件如下:

    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: redis-master
      labels:
        name: redis
      namespace: default
    spec:
      replicas: 4
      template:
        metadata:
          labels:
          name: redis
      spec:
        containers:
        - name: master
          image: 172.16.1.41:5000/redis:3.0.5
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
    

    节点transwarp配置了SSD,因此添加“disk=ssd”标签。

    kubectl label node transwarp disk=ssd
    

    通过更新Redis服务的YAML文件字段添加nodeSelector标签“disk=ssd”来匹配节点transwarp。

    kubectl patch deploy redis-master -p  '{"spec":{"template":{"spec":{"nodeSelector:"{"disk":"ssd"}}}}}'
    
    • 场景二:只允许调度到CentOS节点
      某系统中有各种异构系统机器,比如SLES、RHEL、CentOS7.2、CentOS7.3等,现在有一个线上任务只能运行在CentOS系统,但是不区分CentOS系统版本。当然我们可以使用MatchNodeSelector来实现匹配,然而如果以这种方式实现需要备注多个标签,因此建议用NodeAffinityPriority解决。

    我们在该服务的YAML文件中将NodeAffinityPriority选择器的指定为requiredDuringSchedulingIgnoredDuringExecution,表示强要求,指定Pod必须被调度到key为“operation system”,值为“centos 7.2”或“centos 7.3”的节点。

    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: operation system
          operator: In
          values:
          -  centos 7.2
          -  centos 7.3
    

    如果需要优先调度到CentOS 7.2的节点,还可以增加一个由preferredDuringSchedulingIgnoredDuringExecution决定的调度策略,优先调度到具有“another-node-label-key-system”标签,且值为“another-node-label-value-centos7.2”的节点。

    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 1
      preference:
       matchExpressions:
       - key: another-node-label-key-system
         operator: In
         values:
         - another-node-label-value-centos7.2
    

    该线上服务的YAML文件具体内容如下:

    apiVersion: v1
    kind: Pod
    metadata:
      name: with-node-affinity
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: operation system
                operator: In
                values:
                -  centos 7.2
                -  centos 7.3
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
              - key: another-node-label-key-system
                operator: In
                values:
                - another-node-label-value-centos7.2
      containers:
      - name: with-node-affinity
        image: gcr.io/google_containers/pause:2.0

    注意:如果同时指定了nodeSelector和nodeAffinity,目标节点必须同时满足这两个规则;nodeSelectorTerms可以指定多个,满足任意一个即可满足;matchExpressions也可以指定多个,要求必须都满足。

    • 场景三:调度到同一分区
      现有公有云组件API服务,需要与认证服务器频繁通信,因此要尽可能的协同部署在云提供商的同一个分区中。
      该场景的需求是使API服务和认证服务运行在同一分区,转化到更通用的视角就是把Pod优先调度到更具Pod亲和性的节点,故可用优选策略InterPodAffinityPriority解决。

    首先我们对API服务添加标签“security=cloud”,它的其中一段配置如下:

    apiVersion: v1
    kind: Pod
    metadata:
      name: pod-flag
      labels:
        security: "cloud"
    spec:
      containers:
      - name: nginx
        image: nginx
    

    为了使认证服务分配到相同分区,需要在其YAML文件中添加requiredDuringSchedulingIgnoredDuringExecution字段去亲和具有“security=cloud”标签的Pod,同时添加topologyKey: failure-domain.beta.kubernetes.io字段来限制拓扑域范围,表示把部署限制在同一个分区。

    apiVersion: v1
    kind: Pod
    metadata:
      name: with-pod-affinity
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: security
                operator: In
                values:
                - cloud
            topologyKey: failure-domain.beta.kubernetes.io/zone
    
    • 场景四:
      私有云服务中,某业务使用GPU进行大规模并行计算。为保证性能,希望确保该业务对服务器的专属性,避免将普通业务调度到部署GPU的服务器。
      该场景要求Pod避免被调度到指定类型的节点,符合排斥关系,故可用策略PodToleratesNodeTaints解决。

    首先先在配有GPU的node1中添加Taints污点(gpu = true),NoSchedule表示只要没有对应Toleration的Pod就不允许调度到该节点。

    kubectl taint nodes nodes1 gpu=true:NoSchedule
    

    同时,通过更新服务的YAML文件,为需要GPU资源业务的Pods添加Toleration(key: "gpu",value: "true"),表示容忍GPU服务器的Taints,这样就可以把指定业务调度到GPU服务器,同时把普通服务调度到其他节点。

    apiVersion: v1
    kind: Pod
    metadata:
      generateName: redis-
      labels:
        app: redis
      namespace: default
    spec:
      containers:
      - image: 172.16.1.41:5000/redis
        imagePullPolicy: Always
        name: redis
      schedulerName: default-scheduler
      tolerations:
      - effect:  NoSchedule
        key: gpu
        operator:  Equal
        value: true
    

    这里需要提一下,tolerations里的effect字段表示处理排斥的策略,支持三种调度规则:NoSchedule表示不允许调度,已调度的不影响;PreferNoSchedule表示尽量不调度;NoExecute表示不允许调度,已运行的Pod将在之后删除。

    总结与展望

    目前kube-scheduler已经提供了丰富的调度策略可供使用,一般情况下,使用kube-scheduler的默认调度策略就能满足大部分需求,并且其插件化的形式也方便于用户进行定制与二次开发。未来,我们会在此基础上对其进一步优化:包括增加cache以减少predict和prioritize阶段的重复计算,已在TOS 1.9中实现;通过scheduler-extender扩展来针对local Volume、GPU等资源实现更合理调度;适配custom-metrics-server的API来实现实时调度,使得每个节点的资源能更好的被利用。

    发布于 2019-01-31