TOC

调度器

    在Kubernetes集群系统之上的masteri节点有一个非常重要的组件,kube-scheduler,它内部有一个调度器程序,kube-scheduler是kube-apiserver的客户端,其主要作用在于注册并监听kube-apiserver上每一个Pod资源,并关注这个Pod资源的调度结果当中的属性字段是不是为空,Pod其中有一个字段会指明Pod被调度到哪一个目标节点上去了,那个字段如果为空,那么就说明还没调度完成,因此一旦通过kube-apiserver注册监听Pod,如果发现其指定的目标Pod的nodename字段为空的话,就开始视图由自己为此Pod从当前集群的众多节点当中选择一个最佳的节点,来运行此Pod,选定完以后,它就把这个选出的节点名放在,这个Pod.spec当中的nodename字段,并回存kube-apiserver,一旦存储到kube-apiserver之后,kube-apiserver会看到这个Pod被调度到某一个节点,接下来kube-apiserver会通知给这个节点的kubelet程序由kubelet负责去读取这个Pod的所有定义,而后调用本地的Docker引擎,将其运行起来;
    那么这众多节点当中,kube-scheduler从中挑选节点的策略是什么呢,它一定有一个评判标准,就像一些常见的负载均衡调度算法一样,根据不同的调度算法,可能每次挑选的后端节点是不同的,kube-scheduler也是这样的, 因此,在Kubernetes内部,有一个调度器,这个调度器可以受影响于很多来计算调度目标结果排序的方式;
    对于Kubernetes来讲,每次当一个新Pod来创建的时候,kube-scheduler一旦发现调度结果为空,也就是发现出一个未经调度的Pod资源时,此时,kube-scheduler就会基于自己的调度算法,将所有节点拿来做一个评估,评估完以后,从中选择一个最优的,而评估过程大体分为三个阶段;

    第一阶段(Predicate),预选阶段,先去评估哪些至少在基本条件上能满足此Pod的运行节点,什么是基本条件呢,比如说如果我们节点有污点的话,这Pod不能容忍污点,那么这些所有的不能容忍的污点就要被排除在外了,只能留下没有污点的或者此Pod能容忍的污点的节点,这叫预选,但预选中的节点应该有很多个,这很多个应该有一个是最佳的;
    第二阶段(Priority),优选阶段,通过优选函数拿到优选结果,如果依然有可能存在有多个,所谓优选就是评估运行此Pod,满足它运行需要的优先级,而这个优先级通畅是一个评分,是一个分值,这个分值如果正好有一个是最高的,那么这个结果就是已既定了,如果有多个最高分的节点,那么这个还需要随机挑选一个,进入第三阶段;
    第三阶段(Select), 随机挑选;
调度器基本工作流程
    对于预选,Kubernetes有很多预选器来帮忙完成这个节点是否符合Pod的基本要求,对于Predicate来讲,它是一个或者一组程序,会将Kubernetes之上的所有节点拿来,进行一次计算,如果其中有一组程序声明这个节点不符合运行此Pod的条件,那么这就被预选排除了,以此类推,这些左右节点都经过这一组Predicate进行评估之后,留下的都是至少能符合运行此Pod的基本条件;
    于是这些节点谁是最好的,因此就需要进入第二步,Priority也是有一组优先函数组成的,这个函数不像Predicate它合不合理,而评估的是这个节点和运行此Pod的评估分数,比如node1进来,进入第一个Priority函数计算得两分,进入第二个Priority函数计算得三分,最终所有的Priority函数的分值的和就是这个node节点的整体得分分值,而后基于得分进行排序,逆序排序;
    排完序之后,就看得分最高的有几个,然后会将得分最高的挑选出来,进入Select流程,进行随机挑选;

调度器的实现

    其实Kubernetes的调度器也是插件实现的,我们可以使用内建调度器叫default-scheduler来调度,用户也可以自定义调度器,并且在创建Pod时还可以指定使用什么调度器来调度Pod;
指定调度器:kubectl explain pods.spec.schedulerName
查看默认使用的调度器
    其实我们在创建Pod的时候,是没有指定调度器的,准入控制器会为我们补齐该字段,当我们未定义的时候,直接使用默认值;
# 挑选一个已创建的Pod查看调度器默认值
[root@node1 ~]# kubectl get pods -n dev  dev-pod -o yaml|grep schedulerName
  schedulerName: default-scheduler
节点过滤
    节点过滤其实有很多评估几个简单的示例解说如下;
Volumes Filters:如果某一个Pod要求要使用本地的临时存储,而且需要至少有20G的存储空间,正好所有节点当中有的节点有20G有的节点没有20G,那么没有20G的就会被排除掉;
Resources Filters:如果某一节点要求resource,CPU至少有四核,那么CPU也需要经过一次筛选;
Topology Filters:
预选策略
    自上而下,依次进行,源码地址:https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/algorithm/predicates/predicates.go;

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不能容忍的污点,但后来N,第二种是不接受,也就是检查此属性,此时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上; 
优选策略
    自上而下,依次优选,源码地址:https://github.com/kubernetes/kubernetes/tree/master/pkg/scheduler/algorithm/priorities;
LeastRequested:CPU和memory最少请求的资源,它就是由节点空闲资源,与节点容量比值计算得来,如果这个节点有8颗核心,现在已经分配了1个,那么已分配比例是八分之一,也就是7比1,每一个节点都依次这样进行评估,哪个节点的比值最小,那么就优先哪个节点(cpu(capacity-sum(requested))*10/capacity)+(memory(capacity-sum(requested))*10/capacity)/2;
BalancedResourceAllocation:CPU和内存资源的被占用率相近的胜出;目的是平衡节点资源的使用率;
NodePreferAvoidPods:节点注解信息"scheduler.alpha.kubernetes.io/preferAvoidPods";
TaintToleration:将Pod对象的spec.tolerations与节点的taints列表项进行匹配度检查,污点分值,匹配的条目越多得分越低;
SelectorSpreading:调度器将pod分散调度,尽可能将Pod分散到多个节点运行,如果此节点被之前同一个调度器已经调度到已选出的节点,那么尽量不在此节点再创建Pod;
InterPodAffinity:根据Pod间的亲和性;
NodeAffinity:根据节点亲和性;
MostRequested:根据最多被请求的节点,和LeastRequested是相反的,所以这个默认没有启用;
NodeLabel:根据节点标签;
ImageLocality:根据满足当前Pod对象需求的已有镜像的体积大小之和;
随机挑选
    当一个Pod被调度的时候,所有通过预选策略的node都会进行一些硬性条件检查,而后经过优选策略进行软性分值计算,而后通过分值进行排序,接下来进行选择阶段了,最高分胜出;

Kuberntes调度器

    任何时候我们所创建一个Pod时,都不可避免要被我们的kube-scheduler当中的default-scheduler进行调度,当然如果不想使用默认的的default-scheduler,那么Kubernetes也允许用户自定义kube-scheduler,并在创建Pod的时候指明调度器,而不使用default-scheduler,目前来讲,Kubernetes内部只有一个调度器,但是它支持插件式调度器,允许用户自定义调度器对接到已有的调度程序上,可被用户选择使用其他调度器;
    即便Pod的调度,是由default-scheduler来负责完成的,但我们还是可以在外部施加各种条件,来影响调度结果,比如我们的定义Pod时,在Pod的spec字段加上一个nodeName来影响它的调度结果,也可以使用nodeSeletor来指定只将此Pod运行在匹配到的node节点上,至少一个节点符合,如果没有任何节点被匹配,那么就会Pending,被挂起了,这都是一种硬性调度机制;
    也可以有一些柔性调度机制,比如Affinity,亲和性也分硬亲和和软亲和,硬亲和指的是,如果满足它的亲和性,它就可以把node作为调度节点,如果不满足,就直接排除,而软亲和指的是如果我们经过亲和性检测,发现没有一个节点符合这个亲和性,那么就随机亲和;
    当然我们也可以使用污点, 从上面描述的来看可以看出node都是等待着被选择,那么我们也可以赋予node的主动权,给node加上一个污点,如果节点不能容忍这个污点,那么就不会调度上面来,这就给了node主动控制权;
高级调度机制
    可以看出,用户可以自己在必要时,去施加在Pod上一些条件,或者施加在node上的一些控制机制,来确定Pod只能够或者更愿意调度于哪些节点,以及节点是否要挑选哪些Pod能够运行于当前节点,这些我们就称之为高级调度机制,如果我们把nodeName和nodeSelector排除在外的话,我们还有NodeAffinity,叫做node的亲和性,意思是Pod更愿意运行在哪些节点上;
    PodAffinity表示两个Pod能不能运行在同一个节点上,Pod和Pod之间可以不可以在一起,有亲和性表示应该在一起,反亲和那就是不能在一起,比如两个Pod不能运行在同一个节点上,但是这个一起可以是一个机房或者是一个机架,也可以是一个节点;
    还有一种高级调度机制,也就是我们可以允许节点以获取调度控制的主动权,让节点通过赋予自身污点,并通过Pod是否能容忍这些污点,来定义出它到底是允许该Pod运行在此节点上,还是驱离此Pod,那么这个时候,需要在两个维度上协同定义,首先Pod得有容忍度的定义,节点要有污点的定义,必要时Kubernetes也允许用户给节点打上污点,以避免节点多次被调度Pod运行;
nodeAffinity的生效方式
1、同事指定了nodeAffinity和nodeSeletor那么他们是“与”关系,即符合条件的Node需要同时满足两个条件;
2、为nodeAffinity同时指定多个nodeSelectorTerms时,各条目间取逻辑“或”关系,即满足一个条件即可;
3、同一个nodeSeletorTerms中的多个matchExpression存在逻辑“与”关系,即Node需要同时满足多个条件;
4、如果调度以及完成了,后来将节点的label删除了,导致这个节点不能再满足这个亲和性了,那么已调度的Pod不会被remove,这个亲和性仅仅发生在调度的过程当中,调度完成后label的变动将不再有任何影响;
5、对于软亲和来讲,可以指定weight权重,如果有多个preferredDuringSchedulingIgnoredDuringExecution,多个preferredDuringSchedulingIgnoredDuringExecution的权限可以不一样;
nodeAffinity
    节点亲和性,主要是定义Pod运行于哪个节点的亲和性,Pod更期望运行与哪种Pod;
硬限制:kubectl explain pods.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution  # 只能运行与满足这个亲和性的节点
软限制:kubectl explain pods.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution # 即使不满足不会不调度Pod,能满足亲和性就以亲和性为准,不能满足的时候以其他优选函数为准;
评估亲和程度语法:In、NotIn、Exists、DoesNotExist、Gt、Lt
示例
spec:
  affinity:
    nodeAffinity:  # 节点亲和性
      requiredDuringSchedulingIgnoredDuringExecution: # 硬亲和,必须满足
        nodeSelectorTerms: # 节点选择
        - matchExpressions: # 选择表达式
          - key: kubernetes.io/e2e-az-name
            operator: In # 基于集合的标签选择器
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution: # 软亲和,尽量满足
      - weight: 1  # 这个判断权重为
        preference: # 定义亲和度
          matchExpressions: # 选择表达式
          - key: another-node-label-key
            operator: In # 基于集合的标签选择器
            values:
            - another-node-label-key

# 存在性判断
spec:
  containers:
  - name: dev-container
    image: ikubernetes/myapp:v1
  affinity:
    nodeAffinity:  # 节点亲和性
      requiredDuringSchedulingIgnoredDuringExecution: # 硬亲和,必须满足
        nodeSelectorTerms: # 节点选择
        - matchExpressions: # 选择表达式
          - key: node-core-number # 定义核心数
            operator: Exists # 存在性判断
实战示例
    分别演示硬亲和性,和软亲和性;
# 使用硬限制定义Pod的affinity,只在CPU核心数大于2的节点上运行;
[root@node1 ~]# cat pod.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: dev-pod
spec:
  containers:
  - name: dev-container
    image: ikubernetes/myapp:v1
  affinity:
    nodeAffinity:  # 节点亲和性
      requiredDuringSchedulingIgnoredDuringExecution: # 硬亲和,必须满足
        nodeSelectorTerms: # 节点选择
        - matchExpressions: # 选择表达式
          - key: node-core-number # 定义核心数
            operator: Gt # 大于
            values: 
            - "2" # 2
[root@node1 ~]# kubectl apply -f pod.yaml 
pod/dev-pod created
# 可以看到没有一个node的labels符合我们定义的硬限制
[root@node1 ~]# kubectl get nodes --show-labels 
NAME            STATUS   ROLES    AGE   VERSION   LABELS
node1.cce.com   Ready    master   39d   v1.16.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node1.cce.com,kubernetes.io/os=linux,node-role.kubernetes.io/master=
node2.cce.com   Ready    <none>   39d   v1.16.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2.cce.com,kubernetes.io/os=linux
node3.cce.com   Ready    <none>   39d   v1.16.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node3.cce.com,kubernetes.io/os=linux
# 查看此时Pod的状态
[root@node1 ~]# kubectl get pods 
NAME      READY   STATUS    RESTARTS   AGE
dev-pod   0/1     Pending   0          74s  # 等待状态
# 手动给node添加labels
[root@node1 ~]# kubectl label nodes node3.cce.com node-core-number=4
node/node3.cce.com labeled
# 当手动添加labels之后会立马进行调度
[root@node1 ~]# kubectl get pods --show-labels -o wide              
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE            NOMINATED NODE   READINESS GATES   LABELS
dev-pod   1/1     Running   0          18s   10.244.2.11   node3.cce.com   <none>           <none>            <none>

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注