Loading

(十四)Kubernetes Pod调度器

Pod调度器

API Server 接收客户端提交Pod对象创建请求后的操作过程中,有一个重要的步骤是由调度程序(kube-scheduler)从当前集群中选择一个可用的最佳节点来接收并运行它,通常是默认的调度器(default-scheduler)负责执行此类任务。对于每个待创建的Pod对象来说,调度过程通常分为三个阶段——预选、优选和选定三个步骤,以筛选执行任务的最佳节点。

Kubernetes 调度器概述

Kubernetes系统的核心任务在于创建客户端请求创建的Pod对象并确保其以期望的状态运行。创建Pod对象时,调度器scheduler负责为每一个未经调度的Pod资源、基于一系列的规则集从集群中挑选一个合适的节点来运行它,因此也称为Pod调度器。调度过程中,调度器不会修改Pod资源,而是从中读取数据,并根据配置的策略挑选出最适合的节点,而后通过API调用将Pod绑定至挑选出的节点之上以完成调度过程。

Pod调度器其核心目标是基于资源可用性将各Pod资源公平地分布于集群节点之上。通过三个步骤完成调度操作:节点预选(Predicate)、节点优先级排序(Priority)及节点择优(Select);如下图所示

📌 节点预选

基于一系列预选规则(如PodFitsResources和MatchNode-Selector等)对每个节点进行检查,将那些不符合条件的节点过滤掉从而完成节点预选。

📌 节点优选

对预选出的节点进行优先级排序,以便选出最适合运行Pod对象的一个节点。

📌 节点择优

从优选级排序结果中挑选出优先级最高的节点运行Pod对象,当此类节点多于一个时,则从中随机选择一个。

常用的预选策略

简单来说,预选策略就是节点过滤器,列如节点标签必须能够匹配到Pod资源的标签选择器(由MatchNodeSelector实现的规则),以及Pod容器的资源请求量不能大于节点上剩余的可分配资源(由PodFitsResources实现的规则)等。执行预选操作时,调度器将每个节点基于配置使用的预选策略以特定次序逐一筛选,并根据一票否决制进行节点淘汰。若预选后不存在任何一个满足条件的节点,则Pod处于Pending状态,直到至少有一个节点可用为止。

常用的预选策略如下:

✏️ CheckNodeCondition:检查是否可以在节点报告磁盘、网络不可用或未准备好的情况下将Pod对象调度于其上。

✏️ HostName:若Pod对象拥有spec.hostname属性,则检查节点名称字符串与此属性值是否匹配。

✏️ PodFitsHostPorts:若Pod容器定义了ports.hostPort属性,则检查其值指定的端口是否已被节点上的其他容器或服务占用。

✏️ MatchNodeSelector:若Pod对象定义了sepc.nodeSelector属性,则检查节点标签是否能匹配此属性值。

✏️ NoDiskConflict:检查Pod对象请求的存储卷在此节点是否可用,若不存在冲突则通过检查。

✏️ PodFitsResources:检查节点是否有足够的资源(如CPU、内存和GPU等)满足Pod对象的运行需求。

✏️ PodToleratesNodeTaints:若Pod对象定义了spec.tolerations属性,则检查其值是否能够接纳节点定义的污点(taints),不过,它仅关注具有NoScheduleNoExecute两个效用标识的污点。

✏️ PodToleratesNodeNoExecuteTaints:若Pod对象定义了spec.tolerations属性,则检查其值是否能够接纳节点定义的NoExecute类型的污点。

✏️ CheckNodeLabelPresence:仅检查节点上指定的所有标签的存在性,要检查的标签以及其可否存在取决于用户的定义。

✏️ CheckServiceAffinity:根据当前Pod对象所属的Service已有的其他Pod对象所运行的节点进行调度,其目的在于将相同的ServicePod对象放置在同一个或同一类节点上以提高效率。

✏️ MaxEBSVolumeCount:检查节点上已挂载的EBS存储卷数量是否超过了设置的最大值,默认值为39。

✏️ MaxGCEPDVolumeCount:检查节点上已挂载的GCE PD存储卷数量是否超过了设置的最大值,默认值为16。

✏️ MaxAzureDiskVolumeCount:检查节点上已挂载的Azure Disk存储卷数量是否超过了设置的最大值,默认值为16。

✏️ CheckVolumeBinding:检查节点上已绑定和未绑定的PVC是否能够满足Pod对象的存储卷需求,对于已绑定的PVC,此预选策略将检查给定的节点是否能够兼容相应的PV,而对于未绑定的PVC,预选策略将搜索那些可满足PVC申请的可用的PV,并确保它可与给定的节点兼容。

✏️ NoVolumeZoneConflict:在给定了区域(zone)限制的前提下,检查在此节点上部署Pod对象是否存在存储卷冲突。某些存储卷存在区域调度约束,于是,此类存储卷的区域标签(zone-labels)必须与节点上的区域标签完全匹配方可满足绑定条件。

✏️ CheckNodeMemoryPressure:若给定的节点已经报告了存在内存资源压力过大的状态,则检查当前Pod对象是否可调度至此类节点之上。

✏️ CheckNodePIDPressure:若给定的节点已经报告了存在PID资源压力过大的状态,则检查当前Pod对象是否可调度至此节点之上。

✏️ CheckNodeDiskPressure:若给定的节点已经报告了存在磁盘资源压力过大的状态,则检查当前Pod对象是否可调度至此节点之上。

✏️ MatchInterPodAffinity:检查给定节点是否能够满足Pod对象的亲和性或反亲和性条件,以用于实现Pod亲和性调度或反亲和性调度。

常用的优选函数

预选策略筛选并生成一个节点列表后即进入第二阶段的优选过程。在这个过程中,调度器向每个通过预选的节点传递一系列的优选函数来计算其优先级分值,优先级分值介于0到10之间,其中0表示不适合,10表示最适合托管该Pod对象。

常见的优选函数相关说明如下:

✏️ LeastRequestedPriority:由节点空闲资源与节点总容量的比值计算而来,即由CPU或内存资源的总容量减去节点上已有Pod对象需求的容量总和,再减去当前要创建的Pod对象的需求容量得到的结果除以总容量。CPU和内存具有相同的权重,资源空闲比例越高的节点得分就越高,其计算公式为:(cpu((capacity - sum(requested)) * 10/capacity) + memory((capacity - sum(requested)) * 10/capacity))/2 。

✏️ BalancedResourceAllocation:以CPU和内存资源占用率的相近程序作为评估标准,二者越接近的节点权重越高。该优选函数不能单独使用,需要和LeastRequestedPriority组合使用来平衡优化节点资源的使用情况,以选择那些在部署当前Pod资源后系统资源更为均衡的节点。

✏️ NodePreferAvoidPodsPriority:此优选级函数权限默认为10000,根据节点是否设置了注解信息"scheduler.alpha.kubernetes.io/preferAvoidPods"来计算其优选级。计算方式是:给定的节点无此注解信息时,其得分为10乘以权重10000;存在此注解信息时,对于那些由ReplicationControllerReplicaSet控制器管控的Pod对象的得分为0,其他Pod对象会被忽略(得最高分)。

✏️ NodeAffinityPriority:基于节点亲和性调度偏好进行优先级评估,它将根据Pod资源中的nodeSelector对给定节点进行匹配度检查,成功匹配到的条目越多则节点的分越高。

✏️ TaintTolerationPriority:基于Pod资源对节点的污点容忍调度偏好进行其优先级评估,它将Pod对象的tolerations列表与节点的污点进行匹配度检查,成功匹配的条目越多,则节点得分越低。

✏️ SelectorSpreadPriority:首先查找与当前Pod对相匹配的Service、ReplicationController、ReplicaSet(RS)和StatefulSet,而后查找与这些选择器匹配的现存Pod对象及其所在的节点,则运行此类Pod对象越少的节点得分将越高。简单来说,会尽量将同一标签选择器匹配到的Pod资源分散到不同的节点之上来运行。

✏️ InterPodAffinityPriority:遍历Pod对象的亲和性条目,并将那些能够匹配给定节点的条目的权重相加,结果值越大的节点得分越高。

✏️ MostRequestedPriority:与优选函数LeastRequestedPriority的评估节点得分的方法相似,不同的是,资源站用比例越大的节点,其得分越高。

✏️ NodeLabelPriority:根据节点是否拥有特定的标签来评估其得分,而无论其值为何。需要其存在时,拥有相应标签的节点将获得优先级,否则,不具有相应标签的节点将获得优先级。

✏️ ImageLocalItyPriority:基于给定节点上拥有的运行当前Pod对象中的容器所依赖到的镜像文件来计算节点得分,不具有Pod依赖到的任何镜像文件的节点其得分为0,而拥有相应镜像文件的各节点中,所拥有的被依赖到的镜像文件其体积之和越大则节点得分越高。

节点亲和调度

节点亲和性是调度程序用来确定Pod对象调度位置的一组规则,这些规则基于节点上的自定义标签和Pod对象上指定的标签选择器进行定义。节点亲和性允许Pod对象定义针对一组可以调度于其上的节点的亲和性或反亲和性,不过,它无法具体到某个特定的节点。如将Pod调度至有着特殊CPU的节点或一个可用区域内的节点之上。

节点亲和性规则有两种类型:硬亲和性(required)和软亲和性(preferred)。硬亲和性实现的是强制性规则,Pod调度时必须要满足的规则,而在不存在满足规则的节点时,Pod对象会被置为Pending状态。而软亲和性规则实现的是一种柔和性调度限制,倾向于将Pod对象运行于某类特定的节点之上,而调度器也将尽量满足此需求,但在无法满足调度需求时它将退而求其次地选择一个不匹配规则的节点。

定义节点亲和性规则的关键点有两个,一是为节点配置合乎需求的标签,另一个是为Pod对象定义合理的标签选择器,从而能够基于标签选择出符合期望的目标节点。不过preferredDuringSchedulingIgnoredDuringExecutionrequiredDuringSchedulingIgnoredDuringExecution名字中的后半段字符串IgnoredDuringExecution隐含的意义所指,在Pod资源基于节点亲和性规则调度至某节点之后,节点标签发生了改变而不再符合此节点亲和性规则时,调度器不会将Pod对象从此节点上移除,因为它仅对新建的Pod对象生效。节点亲和性模型如下:

节点硬亲和性

📝 示例一

下面的配置清单示例中定义的Pod对象,其使用节点硬亲和性规则定义可将当前Pod对象调度至拥有zone标签其值为foo的节点之上:

# vim required-nodeaffinity-demo-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: with-required-nodeaffinity
  namespace: default
  labels:
    app: myapp
spec:
  containers:
  - name: myapp
    image: blwy/myapp:v1
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - {key: zone, operator: In, values: ["foo"]}

接下来创建pod,并查看其状态

# kubectl apply -f required-nodeaffinity-demo-pod.yaml
pod/with-required-nodeaffinity created

# kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
with-required-nodeaffinity   0/1     Pending   0          4s

# kubectl describe pods with-required-nodeaffinity
......
Events:
  Type     Reason            Age        From               Message
  ----     ------            ----       ----               -------
  Warning  FailedScheduling  <unknown>  default-scheduler  0/3 nodes are available: 3 node(s) didn't match node selector.
  Warning  FailedScheduling  <unknown>  default-scheduler  0/3 nodes are available: 3 node(s) didn't match node selector.

通过kubectl get podskubectl describe pods查看pod,可以发现pod一直处于Pending状态,调度失败,这是由于强制型的节点亲和性和限制场景中不存在能够满足匹配条件的节点所致。

接下来给node节点设置节点标签,这也是设置节点亲和性的前提之一。

# kubectl label node k8s-node1 zone=foo
node/k8s-node1 labeled

# kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
with-required-nodeaffinity   1/1     Running   0          1m

# kubectl describe pods with-required-nodeaffinity
.....
Events:
  Type     Reason            Age        From                Message
  ----     ------            ----       ----                -------
  Warning  FailedScheduling  <unknown>  default-scheduler   0/3 nodes are available: 3 node(s) didn't match node selector.
  Warning  FailedScheduling  <unknown>  default-scheduler   0/3 nodes are available: 3 node(s) didn't match node selector.
  Normal   Scheduled         <unknown>  default-scheduler   Successfully assigned default/with-required-nodeaffinity to k8s-node1
  Normal   Pulled            9s         kubelet, k8s-node1  Container image "blwy/myapp:v1" already present on machine
  Normal   Created           9s         kubelet, k8s-node1  Created container myapp
  Normal   Started           9s         kubelet, k8s-node1  Started container myapp

通过给其中一个node节点打上标签后,可以看出pod状态一下就变成了Running状态,这是因为节点硬亲和性匹配到了规则。

在定义节点亲和性时,requiredDuringSchedulingIgnoredDuringExecution字段的值是一个对象列表,用于定义节点硬亲和性,可以定义多个nodeSelectorTerm对象,彼此之间为“逻辑或“的关系,进行匹配度检查时,在多个nodeSelectorTerm之间只要满足其中之一即可。nodeSelectorTerm用于定义节点选择器条目,其值为对象列表,由一个或多个matchExpressions对象定义的匹配规则组成,多个规则彼此之间为”逻辑与“的关系,意味着某节点的标签需要完全匹配同一个nodeSelectorTerm下所有的matchExpression对象定义的规则才算成功通过节点选择器条目的检查。而matchExpressions又可由一到多个标签选择器组成,多个标签选择器彼此间为”逻辑与“的关系。

📝 示例二

下面的资源配置清单示例中定义了调度拥有两个标签选择器的节点选择头目。两个标签选择器彼此间为“逻辑与”的关系。

为了测试,这里先给节点多打几个标签

# kubectl label node k8s-node1 ssd=true
node/k8s-node1 labeled
# kubectl label node k8s-node2 ssd=true
node/k8s-node2 labeled
# kubectl label node k8s-node2 zone=bar
node/k8s-node2 labeled

查看节点标签
# kubectl get nodes --show-labels
NAME          STATUS   ROLES    AGE   VERSION   LABELS
k8s-master1   Ready    master   47d   v1.16.6   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master1,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-node1     Ready    <none>   47d   v1.16.6   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node1,kubernetes.io/os=linux,ssd=true,zone=foo
k8s-node2     Ready    <none>   47d   v1.16.6   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node2,kubernetes.io/os=linux,ssd=true,zone=bar

编写yaml文件

# vim required-nodeaffinity-demo-pod2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: with-required-nodeaffinity-2
  namespace: default
  labels:
    app: myapp
spec:
  containers:
  - name: myapp
    image: blwy/myapp:v1
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - {key: zone, operator: In, values: ["foo"]}
          - {key: ssd, operator: Exists, values: []}

接下来创建pode2,可以明确知道满足条件的只有node2节点,因为同时满足了zone=foossd=true

# kubectl apply -f required-nodeaffinity-demo-pod2.yaml
pod/with-required-nodeaffinity-2 created

# kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
with-required-nodeaffinity-2   1/1     Running   0          4s

# kubectl describe pods with-required-nodeaffinity-2
Events:
  Type    Reason     Age        From                Message
  ----    ------     ----       ----                -------
  Normal  Scheduled  <unknown>  default-scheduler   Successfully assigned default/with-required-nodeaffinity-2 to k8s-node1
  Normal  Pulled     15s        kubelet, k8s-node1  Container image "blwy/myapp:v1" already present on machine
  Normal  Created    15s        kubelet, k8s-node1  Created container myapp
  Normal  Started    15s        kubelet, k8s-node1  Started container myapp

注意:调度器在调度Pod资源时,节点亲和性仅是其节点预选策略中遵循的预选机制之一,其他配置使用的预选策略依然正常参与节点预选过程。例如下面的测试,将上面的资源配置清单修改如下

# vim required-nodeaffinity-demo-pod3.yaml
apiVersion: v1
kind: Pod
metadata:
  name: with-required-nodeaffinity-3
  namespace: default
  labels:
    app: myapp
spec:
  containers:
  - name: myapp
    image: blwy/myapp:v1
    resources:
      requests:
        cpu: 6
        memory: 20Gi
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - {key: zone, operator: In, values: ["foo"]}
          - {key: ssd, operator: Exists, values: []}


# kubectl apply -f required-nodeaffinity-demo-pod3.yaml
pod/with-required-nodeaffinity-3 created

# kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
with-required-nodeaffinity-3   0/1     Pending   0          4s

# kubectl describe pods with-required-nodeaffinity-3
....
Events:
  Type     Reason            Age        From               Message
  ----     ------            ----       ----               -------
  Warning  FailedScheduling  <unknown>  default-scheduler  0/3 nodes are available: 2 node(s) didn't match node selector, 3 Insufficient cpu, 3 Insufficient memory.
  Warning  FailedScheduling  <unknown>  default-scheduler  0/3 nodes are available: 2 node(s) didn't match node selector, 3 Insufficient cpu, 3 Insufficient memory.

上面的with-required-nodeaffinity-3 pod资源一直处于Pending状态,这是因为这里所有节点都无法满足需求,这里所有节点配置在2核心CPU2GB内存,无法满足容器myapp的需求,所以调度失败。

节点软亲和性

节点软亲和性为节点选择机制提供了一种柔性控制逻辑,被调度的Pod对象不再是"必须"而是"应该"放置于某些特定的节点之上,当条件不满足时,也能够接受编排于其他不符合条件的节点之上。还提供了weight属性以便用户自定义其优先级,取值范围是1~100,数字越大优先级越高。

📝 示例

# vim deploy-with-preferred-nodeaffinity-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deploy-with-node-affinity
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      name: myapp-pod
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: blwy/myapp:v1
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 60
            preference:
              matchExpressions:
              - {key: zone, operator: In, values: ["foo"]}
          - weight: 30
            preference:
              matchExpressions:
              - {key: ssd, operator: Exists, values: []}

上面资源配置清单中定义了节点软亲和性以选择运行在拥有zone=foossd标签(无论其值为何)的节点之上,其中zone=foo是更为重要的倾向性规则,权重为60,相比ssd标签就没有那么关键,权重为30。这样一来,如果集群中拥有足够多的节点,那么它将被此规则分为四类:同时满足拥有zone=foossd标签、仅具有zone=foo标签、仅具有ssd标签,以及不具备此两个标签,如下图所示:

# kubectl apply -f deploy-with-preferred-nodeaffinity-demo.yaml
deployment.apps/myapp-deploy-with-node-affinity created

# kubectl get pods -o wide
NAME                                               READY   STATUS    RESTARTS   AGE    IP            NODE        NOMINATED NODE   READINESS GATES
myapp-deploy-with-node-affinity-6d7884b4c8-kdg5n   1/1     Running   0          116s   10.244.1.11   k8s-node1   <none>           <none>
myapp-deploy-with-node-affinity-6d7884b4c8-rjn99   1/1     Running   0          116s   10.244.1.10   k8s-node1   <none>           <none>
myapp-deploy-with-node-affinity-6d7884b4c8-rnd4r   1/1     Running   0          116s   10.244.2.10   k8s-node2   <none>           <none>

这里只有两个工作节点,可以从上面的结果看出,node1节点运行了两个pod,这是因为上面定义的规则中,node1节点同时具有zone=foo标签和ssd标签,且权重为60。

Pod 资源亲和调度

出于高效通信的需求,偶尔需要把一些Pod对象组织在相近的位置(同一节点、机架、区域或地区等),如一个业务的前端Pod和后端Pod等,此时可以将这些Pod对象间的关系称为亲和性。偶尔,出于安全或分布式等原因也有可能需要将一些Pod对象在其运行的位置上隔离开来,如在每个区域运行一个应用代理Pod对象等,此时可以把这些Pod对象间的关系称为反亲和性(anti-affinity)

Kubernetes调度器通过内建的MatchInterPodAffinity预选策略为这种调度方式完成节点预选,并给予InterPodAffinityPriority优选函数进行各节点的优选级评估。

位置拓扑

Pod亲和性调度需要各相关的Pod对象运行于"同一位置",而反亲和性调度则需要它们不能运行于"同一位置"。何谓同一位置?事实上,取决于节点的位置拓扑,拓扑的方式不同,如下图"Pod资源与位置拓扑"中的Pod-APod-B是否在同一位置的判定结果也可能有所不同

如果以基于各节点的kubernetes.io/hostname标签作为评判标准,那么"同一位置"意味着同一节点,不同节点即不同的位置,如"基于节点的位置拓扑"图所示

如果是基于"故障转移域"来进行评判,那么server1server4属于同一位置,而server2server3属于另一个意义上的同一位置。

故此,在定义Pod对象的亲和性与反亲和性时,需要借助于标签选择器来选择被依赖的Pod对象 ,并根据选出的Pod对象所在的节点的标签来判定"同一位置"的具体意义。

Pod 硬亲和调度

Pod硬亲和性调度也是用requiredDuringSchedulingIgnoredDuringExecution属性进行定义。Pod亲和性用于描述一个Pod对象与具有某特征的现存Pod对象运行位置的依赖关系,因此,测试使用Pod亲和性约束,需要事先存在被依赖的Pod对象,具有特别的识别标签。

📝 示例1

下面创建一个有着标签app=tomcat的deployment资源部署一个Pod对象:

# kubectl run tomcat -l app=tomcat --image tomcat:alpine

下面资源配置清单中定义了一个Pod对象,通过labelsSelector定义的标签选择器挑选感兴趣的现存Pod对象,然后根据挑选出的Pod对象所在节点的标签kubenetes.io/hostname 来判断同一位置的具体含义,并将当前Pod对象调度至这一位置的某节点之上:

# vim required-podAffinity-pod1.yaml
apiVersion: v1
kind: Pod
metadata:
  name: with-pod-paffinity-1
  namespace: default
spec:
  containers:
  - name: myapp
    image: blwy/myapp:v1
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - {key: app, operator: In, values: ["tomcat"]}
        topologyKey: kubernetes.io/hostname

事实上,kubernetes.io/hostname标签是kubernetes集群节点的内建标签,它的值为当前节点的节点主机名称标识,各个节点均不相同。因此,新建的Pod对象将部署至被依赖的Pod对象的同一节点之上。requiredDuringSchedulingIgnoredDuringExecution表示这种亲和性为强制约束。

# kubectl apply -f required-podAffinity-pod1.yaml
pod/with-pod-paffinity-1 created

# kubectl get pods  -o wide
NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
tomcat-6c999f4c7d-x5x95   1/1     Running   0          12m   10.244.1.41   k8s-node1   <none>           <none>
with-pod-paffinity-1      1/1     Running   0          25s   10.244.2.42   k8s-node2   <none>           <none>

基于单一节点的Pod亲和性只在极个别的情况下才可能会用到,较为常用的是基于同一地区、区域或几家的拓扑位置约束。例如部署应用程序myapp与数据库db服务相关的Pod时,db pod可能会部署于如下图"pod硬亲和性"所示的foobar这两个区域中的某个节点之上,依赖与数据服务的myapp pod对象可部署于 db pod所在区域的节点上。如果db pod在两个区域foobar中各有副本运行,那么myapp将可以运行于这两个区域的任何节点之上。

📝 示例2

创建具有两个拥有标签为"app=db"的副本Pod作为被依赖的资源,这里随机分配给了两个节点。

# kubectl run db -l app=db --image=redis:alpine --replicas=2

# kubectl get pods -l app=db -o wide
NAME                  READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
db-86f4768855-cdtq7   1/1     Running   0          32s   10.244.1.43   k8s-node1   <none>           <none>
db-86f4768855-pftfv   1/1     Running   0          32s   10.244.2.32   k8s-node2   <none>           <none>

于是,依赖于亲和于这两个Pod的其他pod对象可运行于zone标签值为foobar的区域内的所有节点之上。下面进行定义一个Deployment控制器管理的Pod资源示例

# cat deploy-with-required-podAffinity-pod2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-with-pod-affinity
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      name: myapp
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: blwy/myapp:v1

在调度器示例中的Deployment控制器创建的Pod资源时,调度器首先会基于标签选择器查询拥有标签"app=db"的所有Pod资源,接着获取它们分别所属的节点的zone标签值,接下来再查询拥有匹配这些标签值的所有节点,从而完成节点预选。根据优选函数计算这些节点的优先级,从而挑选出运行新建Pod对象的节点。

# kubectl apply -f deploy-with-required-podAffinity-pod2.yaml
deployment.apps/myapp-with-pod-affinity created

# kubectl get pods -o wide
NAME                                      READY   STATUS    RESTARTS   AGE    IP           NODE           NOMINATED NODE   READINESS GATES
db-86f4768855-cdtq7                       1/1     Running   0          132m   10.244.1.43   k8s-node1   <none>           <none>
db-86f4768855-pftfv                       1/1     Running   0          132m   10.244.2.32   k8s-node2   <none>           <none>
myapp-with-pod-affinity-c775766d5-86c48   1/1     Running   0          5s     10.244.1.44   k8s-node1   <none>           <none>
myapp-with-pod-affinity-c775766d5-gdfxp   1/1     Running   0          5s     10.244.2.39   k8s-node2   <none>           <none>
myapp-with-pod-affinity-c775766d5-ptcfd   1/1     Running   0          5s     10.244.2.40   k8s-node2   <none>           <none>

注意:如果节点上的标签在运行时发生了变更,以致不再满足Pod上的亲和性规则,但该Pod还将继续在该节点上运行,因此它仅会影响新建的Pod资源;另外、labelSelector属性仅匹配与被调度器的Pod在同一名称空间中的Pod资源,不过也可以通过为其天剑namespace字段以指定其他名称空间。

Pod 软亲和调度

类似于节点亲和性机制,Pod也支持使用preferredDuringSchedulingIgnoredDuringExecution属性定义柔性亲和性机制,调度器会尽力确保满足亲和性约束的调度逻辑,然而在约束条件不能得到满足时,它也允许将Pod对象调度至其他节点运行。

📝 示例

# vim deploy-with-perferred-podAffinity-pod.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-with-preferred-pod-affinity
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      name: myapp
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: blwy/myapp:v1
      affinity:
        podAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 80
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - {key: app, operator: In, values: ["cache"]}
              topologyKey: zone
          - weight: 20
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - {key: app, operator: In, values: ["db"]}
              topologyKey: zone

上面资源配置清单中定义了两组亲和性判定机制,一个是选出cache Pod所在节点的zone标签,并赋予了较高的权重80,另一个是选择db Pod所在节点的zone标签,有着略低的权重20。于是,调度器会将目标节点分为四类:cache Poddb Pod同时所属的zonecache Pod单独所属的zonedb Pod单独所属的zone,以及其他所有的zone

# kubectl apply -f deploy-with-perferred-podAffinity-pod.yaml
deployment.apps/myapp-with-preferred-pod-affinity created

# kubectl get pods  -o wide
NAME                                                 READY   STATUS    RESTARTS   AGE    IP           NODE           NOMINATED NODE   READINESS GATES
db-86f4768855-cdtq7                                  1/1     Running   0          160m   10.244.1.43   k8s-node1   <none>           <none>
db-86f4768855-pftfv                                  1/1     Running   0          160m   10.244.2.32   k8s-node2   <none>           <none>
myapp-with-preferred-pod-affinity-79b96db78b-24mnl   1/1     Running   0          7s     10.244.1.45   k8s-node1   <none>           <none>
myapp-with-preferred-pod-affinity-79b96db78b-l9rt9   1/1     Running   0          7s     10.244.2.41   k8s-node2   <none>           <none>
myapp-with-preferred-pod-affinity-79b96db78b-vwfbw   1/1     Running   0          7s     10.244.2.42   k8s-node2   <none> 

Pod 反亲和调度

podAffinity 用于定义Pod对象的亲和约束,对应地,将其替换为podAntiAffinity即可用于定义Pod对象的反亲和约束。反亲和性调度一般用于分散同一类应用的Pod对象等,也包括将不同安全级别的Pod对象调度至不同的区域、机架或节点等。

📝 示例

这里将定义由同一Deployment创建但彼此基于节点位置互斥的Pod对象:

# vim deploy-with-required-podAntiAffinity-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-with-pod-anti-affinity
  namespace: default
spec:
  replicas: 4
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      name: myapp
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: blwy/myapp:v1
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - {key: app, operator: In, values: ["myapp"]}
            topologyKey: kubernetes.io/hostname

由于定义的强制性反亲和约束,因此,创建的4个Pod副本必须运行于不同的节点中,不过,由于这里实验只有两台工作节点,因此,必然会有两个Pod对象处于Pending状态。

# kubectl apply -f deploy-with-required-podAntiAffinity-demo.yaml
deployment.apps/myapp-with-pod-anti-affinity created

# kubectl get pods -o wide
NAME                                            READY   STATUS    RESTARTS   AGE   IP            NODE        NOMINATED NODE   READINESS GATES
myapp-with-pod-anti-affinity-5d7ddf7766-8rql7   0/1     Pending   0          5s    <none>        <none>      <none>           <none>
myapp-with-pod-anti-affinity-5d7ddf7766-b85c6   1/1     Running   0          5s    10.244.2.25   k8s-node2   <none>           <none>
myapp-with-pod-anti-affinity-5d7ddf7766-t25xt   0/1     Pending   0          5s    <none>        <none>      <none>           <none>
myapp-with-pod-anti-affinity-5d7ddf7766-tbb94   1/1     Running   0          5s    10.244.1.15   k8s-node1   <none>           <none>

Pod反亲和性调度也支持使用柔性约束机制,在调度时,将尽量满足不把位置相斥的Pod对象调度于同一位置,但是,当约束条件无法得到满足时,也可以违反约束而调度。

污点和容忍度

污点(taints)是定义节点之上的键值型属性数据,用于让节点拒绝将Pod调度运行于其上,除非该Pod对象具有接纳节点污点的容忍度。而容忍度(tolerations)是定义在Pod对象上的键值型属性数据,用于配置其可容忍的节点污点,而且调度器仅能将Pod对象调度至其能够容忍该节点污点的节点之上,如下图所示:

上面的节点选择器(nodeSelector)和节点亲和性(nodeAffinity)两种调度方式都是通过在Pod对象上添加标签选择器来完成对特定类型节点标签的匹配,它们实现的是由Pod选择节点的机制。而污点和容忍度则是通过向节点添加污点信息来控制Pod对象的调度结果,从而赋予了节点控制何种Pod对象能够调度于其上的主控权。简单来说,节点亲和性使得Pod对象被吸引到一类特定的节点,而污点则相反,提供了让节点排斥特定Pod对象的能量。

Kubernetes使用PodToleratesNodeTaints预选策略和TaintTolerationPriority优选函数来完成此种类型的高级调度机制。

定义污点和容忍度

污点定义在节点的nodeSpec中,容忍度定义在PodpodSpec中,都是键值型数据,都额外支持(effect)标记,语法格式为"key=value:effect",其中keyvalue的用法及格式与资源注解信息相似,而effect则用于定义对Pod对象的排斥等级,主要包含以下三种类型:

  • NoSchedule:不能容忍此污点的新Pod对象不可调度至当前节点,属于强制型约束关系,节点上现存的Pod对象不受影响。
  • PreferNoScheduleNoSchedule的柔性约束版本,即不能容忍此污点的新Pod对象尽量不要调度至当前节点,不过无其它节点可供调度时也允许接受相应的Pod对象。节点上现存的Pod对象不受影响。
  • NoExecute:不能容忍此污点的新Pod对象不可调度至当前节点,属于强制型约束关系,而且节点上现存的Pod对象因节点污点变动或Pod容忍度变动而不再满足匹配规则时,Pod对象将被驱逐。

Pod对象上定义容忍度时,支持两种操作符:一种是等值比较(Equal),表示容忍度与污点必须在keyvalueeffect三者之上完全匹配;另一种是存在性判断(Exists),表示二者的keyeffect必须完全匹配,而容忍度中的value字段要使用空值。

注意:一个节点可以配置使用多个污点,一个Pod对象也可以有多个容忍度,不过二者在进行匹配检查时遵循如下逻辑:

  1. 首先处理每个有着与之匹配的容忍度的污点。

  2. 不能匹配到的污点上,如果存在了一个使用NoSchedule效用标识,则拒绝调度Pod对象至此节点。

  3. 不能匹配到的污点上,若没有任何一个使用了NoSchedule效用标识,但至少有一个使用了PreferNoScheduler,则应尽量避免将Pod对象调度至此节点。

  4. 如果至少有一个不匹配的污点使用了NoExecute效用标识,则节点将立即驱逐Pod对象,或者不予调度至给定节点;另外,即便容忍度可以匹配到使用了NoExecute效用标识的污点,若在定义容忍度时还同时使用tolerationSeconds属性定义了容忍时限,则超出时限后期也将被节点驱逐。

使用kubeadm部署的Kubernetes集群,其Master节点将自动添加污点信息以阻止不能容忍此污点的Pod对象调度至此节点,因此,用户手动创建的未特意添加容忍此污点容忍度的Pod对象将不会被调度至此节点:

# kubectl describe node k8s-master
Name:               k8s-master
Roles:              master
......
Taints:             node-role.kubernetes.io/master:NoSchedule

不过,有些系统级应用,如kube-proxy或者kube-flannel等,都在资源创建时就添加上了相应的容忍度以确保它们被DaemonSet控制器创建时能够调度至Master节点运行一个实例:

# kubectl describe pods kube-flannel-ds-amd64-bg7gc -n kube-system
......
Node-Selectors:  <none>
Tolerations:     :NoSchedule
                 node.kubernetes.io/disk-pressure:NoSchedule
                 node.kubernetes.io/memory-pressure:NoSchedule
                 node.kubernetes.io/network-unavailable:NoSchedule
                 node.kubernetes.io/not-ready:NoExecute
                 node.kubernetes.io/pid-pressure:NoSchedule
                 node.kubernetes.io/unreachable:NoExecute
                 node.kubernetes.io/unschedulable:NoSchedule

另外,这类Pod是构成Kubernetes系统的基础且关键性的组件,它们甚至还定义了更大的容忍度。通过上面的kube-flannel实例的容忍度定义来看,它还能容忍那些报告了磁盘压力或内存压力的节点,以及未就绪的节点和不可达的节点,以确保它们能在任何状态下正常调度至集群节点上运行。

管理节点的污点

任何符合其键值规范要求的字符串均可用于定义污点信息:仅可使用字母、数字、连接符、点号和下划线,且仅能以字母或数字开头,其中键名长度上限为253个字符,值最长为63个字符。可以用污点用来描述具体的部署规划,键名比如node-type、node-role、node-project或node-geo等,还可以在必要时戴上域名以描述其额外的信息,如node-type.ilinux.io等。

📌 添加污点

kubectl taint命令即可向节点添加污点,命令格式如下:

kubectl taint nodes <node-name> <key>=<value>:<effect> ...

例如,使用"node-type=production:NoSchedule"定义节点k8s-node1

# kubectl taint nodes k8s-node1 node-type=production:NoSchedule
node/k8s-node1 tainted

此时,k8s-node1上已有的Pod对象不受影响,但新建的Pod若不能容忍此污点将不能再被调度至此节点。通过下面的命令可以查看节点的污点信息:

# kubectl get nodes k8s-node1 -o go-template={{.spec.taints}}
[map[effect:NoSchedule key:node-type value:production]]

需要注意的是,即便是同一个键值数据,若其效用标识不同,则其也分属于不同的污点信息,例如,将上面命令中的效用标识定义为PreferNoSchedule再添加一次:

# kubectl taint nodes k8s-node1 node-type=production:PreferNoSchedule
node/k8s-node1 tainted

📌 删除污点

删除污点,仍通过kubectl taint命令进行,但是要使用如下的命令格式,省略效用标识则表示删除使用指定键名的所有污点,否则就只是删除指定健名上对应效用标识的污点:

kubectl taint nodes <node-name> <key>[:<effect>]-

例如,删除k8s-node1node-type键的效用标识为NoSchedule的污点信息:

# kubectl taint nodes k8s-node1 node-type:NoSchedule-
node/k8s-node1 untainted

若要删除使用指定健名的所有污点,则在删除命令中省略效用标识即能实现,例如:

# kubectl taint nodes k8s-node1 node-type-
node/k8s-node1 untainted

删除节点上的全部污点信息,通过kubectp patch命令将节点属性spec.taints的值直接置空,例如:

# kubectl patch nodes k8s-node1 -p '{"spce":{"taints":[]}}'

节点污点的变动会影响到新建Pod对象的调度结果,而且使用NoExecute进行标识时,还会影响到节点上现有的Pod对象。

Pod 对象的容忍度

Pod对象的容忍度可通过其spec.tolerations字段进行添加,根据使用的操作符不同,主要有两种可用的形式:一种是与污点信息完全匹配的等值关系;另一种是判断污点信息存在性的匹配方式。使用Equal操作符的示例如下所示,其中tolerationSeconds用于定义延迟驱逐当前Pod对象的时长:

📝 示例

首先给两个工作节点添加不同的污点

# kubectl taint node k8s-node1 node-type=production:NoSchedule
# kubectl taint node k8s-node2 node-type=dev:NoExecute

编写示例yaml文件

# vim deploy-taint-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-taint-demo-pod
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: blwy/myapp:v1
        ports:
        - name:
          containerPort: 80
      tolerations:
      - key: "node-type"
        operator: "Equal"
        value: "production"
        effect: "NoExecute"
        tolerationSeconds: 30

创建资源,可以发现目前pod一直处于Pending状态,这是因为两个节点都不能容忍。

# kubectl apply -f deploy-taint-demo.yaml
deployment.apps/deploy-taint-demo-pod created
# kubectl get pods -o wide
NAME                                     READY   STATUS        RESTARTS   AGE     IP            NODE        NOMINATED NODE   READINESS GATES
deploy-taint-demo-pod-7459f65d64-426vg   0/1     Pending       0          4s      <none>        <none>      <none>           <none>
deploy-taint-demo-pod-7459f65d64-h6fbp   0/1     Pending       0          4s      <none>        <none>      <none>           <none>
deploy-taint-demo-pod-7459f65d64-k4xjw   0/1     Pending       0          4s      <none>        <none>      <none>           <none>

修改资源配置清单,将容忍度改为NoSchedule, 这个示例中表示容忍key为node-type值为production的污点

# vim deploy-taint-demo.yaml
      tolerations:
      - key: "node-type"
        operator: "Equal"
        value: "production"
        effect: "NoSchedule"

重新创建,可以发现已经调度到k8s-node1节点上

# kubectl apply -f deploy-taint-demo.yaml
deployment.apps/deploy-taint-demo-pod configured
[root@k8s-master1 schedule]# kubectl get pods -o wide
NAME                                    READY   STATUS    RESTARTS   AGE   IP            NODE        NOMINATED NODE   READINESS GATES
deploy-taint-demo-pod-87cffffdd-9tm29   1/1     Running   0          3s    10.244.1.24   k8s-node1   <none>           <none>
deploy-taint-demo-pod-87cffffdd-qt8wf   1/1     Running   0          2s    10.244.1.25   k8s-node1   <none>           <none>
deploy-taint-demo-pod-87cffffdd-zgmrl   1/1     Running   0          4s    10.244.1.23   k8s-node1   <none>           <none>

还可以通过Exists进行判断,修改资源配置清单如下,下面这段表示,只要有污点key为node-type,不论值为什么且是什么类型的污点,都能容忍

# vim deploy-taint-demo.yaml
      tolerations:
      - key: "node-type"
        operator: "Exists"
        value: ""
        effect: ""

查看结果可以看出,两个节点都已经进行调度了。

# kubectl get pods -o wide
NAME                                     READY   STATUS    RESTARTS   AGE   IP            NODE        NOMINATED NODE   READINESS GATES
deploy-taint-demo-pod-7c87f4957f-4njh7   1/1     Running   0          16s   10.244.2.27   k8s-node2   <none>           <none>
deploy-taint-demo-pod-7c87f4957f-mj46d   1/1     Running   0          14s   10.244.1.26   k8s-node1   <none>           <none>
deploy-taint-demo-pod-7c87f4957f-vp9t2   1/1     Running   0          13s   10.244.2.28   k8s-node2   <none>           <none>

问题节点标识

Kubernetes1.6版本起支持使用污点自动标识问题节点,通过节点控制器在特定条件下自动为节点添加污点信息实现。它们都是用NoExecute效用标识,因此不能容忍此类污点的现有Pod对象也会遭到驱逐。目前,内建使用的此类污点包含如下几个:

✏️ node.kubernetes.io/not-ready:节点进入“NotReady”状态时被自动添加的污点。

✏️ node.alpha.kubernetes.io/unreachable:节点进入“NotReachable”状态时被自动添加的污点。

✏️ node.kubernetes.io/out-of-disk:节点进入“OutOfDisk”状态时被自动添加的污点。

✏️ node.kubernetes.io/memory-pressure:节点内存资源面临压力。

✏️ node.kubernetes.io/disk-pressure:节点磁盘资源面临压力。

✏️ node.kubernetes.io/network-unavailable:节点网络不可用。

✏️ ​node.cloudprovider.kubernetes.io/uninitializedkubelet由外部的云环境程序启动时,它将自动为节点添加此污点,待到云控制器管理器中的控制器初始化此节点时再将其删除。

posted @ 2020-06-18 14:18  别来无恙-  阅读(1102)  评论(0编辑  收藏  举报