大家好,我是腾意。

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

1.1 Kubernetes调度器概述

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

img

图12-1 Kubernetes调度器

Kubernetes内建了适合绝大多数场景中Pod资源调度需求的默认调度器,它支持同时使用算法基于原生及可定制的工具来选出集群中最适合运行当前Pod资源的一个节点,其核心目标是基于资源可用性将各Pod资源公平地分布于集群节点之上。目前,平台提供的默认调度器也称为“通用调度器”,它通过三个步骤完成调度操作:节点预选(Predicate)、节点优先级排序(Priority)及节点择优(Select),如图12-2所示。

img

图12-2 节点预选、优选及选定示意图

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

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

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

偶尔,有些特殊的Pod资源需要运行在特定的节点之上,或者说对某类节点有着特殊偏好(如那些有着SSD、GPU等特殊硬件的节点),以便更好地匹配容器应用的运行需求。另外,有的Pod资源与其他Pod资源存在着特定的关联性,它们运行于同一节点以便能够实现更高效的协同效果等。此种场景可通过组合节点标签,以及Pod标签或标签选择器等来激活特定的预选策略以完成高级调度,如MatchInterPodAffinity、MatchNodeSelector和PodToleratesNodeTaints等预选策略,它们用于为用户提供自定义Pod亲和性或反亲和性、节点亲和性以及基于污点及容忍度的调度机制。

不过,未激活特定的预选策略时,Pod资源对节点便没有特殊偏好,相关的预选策略无法在节点预选过程中真正发挥作用。

1.1.1 常用的预选策略

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

img

图12-3 Kubernetes的预选策略

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

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

3)PodFitsHostPorts:若Pod容器定义了ports.hostPort属性,则检查其值指定的端口是否已被节点上的其他容器或服务占用。在Kubernetes 1.0版本之前此预选策略名称为PodFitsPorts。

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

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

6)PodFitsResources:检查节点是否有足够的资源(如CPU、内存和GPU等)满足Pod对象的运行需求。节点声明其资源可用容量,而Pod则定义其资源需求,于是,调度器会判断节点是否有足够的可用资源运行Pod对象,若无法满足则返回失败原因(例如,CPU或内存资源不足等)。调度器评判资源消耗的标准是节点已分配的资源量(各容器的requests值之和),而非其上各Pod对象已用的资源量。注意,那些在注解中标记为关键性(critical)的Pod资源不受此预选策略控制。

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

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

9)CheckNodeLabelPresence:仅检查节点上指定的所有标签的存在性,要检查的标签以及其可否存在取决于用户的定义。当集群中部署的节点以regions/zones/racks的拓扑方式放置且基于此类标签对其进行位置标识时,预选策略可以根据此类标识将Pod资源调度至此类节点之上。

10)CheckServiceAffinity:根据当前Pod对象所属的Service已有的其他Pod对象所运行的节点进行调度,其目的在于将相同Service的Pod对象放置在同一个或同一类节点上以提高效率。此预选策略试图将那些在其节点选择器中带有特定标签的Pod资源调度至拥有同样标签的节点之上,具体的标签则取决于用户的定义。例如,若某Service的第一个Pod有一个节点选择器rack,而且它被调度到了标签为region=rack的节点,则属于此Service的所有其他后续的Pod对象都将被调度至具有相同标签(region=rack)的节点上。若该Pod未在其节点选择器中指定标签,则第一个Pod将根据可用性放置在任一节点之上,而后该Service的所有后续Pod对象都将调度至与该节点拥有相同标签值的节点上。

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

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

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

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

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

16)CheckNodeMemoryPressure:若给定的节点已经报告了存在内存资源压力过大的状态,则检查当前Pod对象是否可调度至此节点之上。目前,最低优先级的BestEffort QoS类型的Pod资源也不可调度至此类节点之上,因为它们随时可能因OOM而终止。

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

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

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

如上给定的各预选策略中,CheckNodeLabelPresence和CheckServiceAffinity可以接受特定的配置参数以便在预选过程中融合用户自定义的调度逻辑,这类策略也可称为可配置策略,而余下那些不可接受配置参数的策略也统一称为静态策略。另外,NoDiskConflict、PodToleratesNodeNoExecuteTaints、CheckNodeLabelPresence和CheckServiceAffinity没有包含在默认的预选策略中。

1.1.2 常用的优选函数

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

img

图12-4 Kubernetes支持的优选函数

另外,调度器还支持为每个优选函数指定一个简单的由正数值表示的权重,进行节点优先级分值的计算时,它首先将每个优选函数的计算得分乘以其权重(大多数优先级的默认权重为1),然后将所有优选函数的得分相加从而得出节点的最终优先级分值。权重属性赋予了管理员定义优选函数倾向性的能力。下面是每个节点的最终优先级得分的计算公式:

finalScoreNode=(weight1priorityFunc1)+(weight2priorityFunc2)+…

下面是各优选函数的相关说明。

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

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

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

4)NodeAffinityPriority:基于节点亲和性调度偏好进行优先级评估,它将根据Pod资源中的nodeSelector对给定节点进行匹配度检查,成功匹配到的条目越多则节点得分越高。不过,其评估过程使用首选而非强制型的“PreferredDuringSchedulingIgnoredDuringExecution”标签选择器。

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

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

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

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

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

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

Kubernetes的默认调度器以预选、优选、选定机制完成将每个新的Pod资源绑定至为其选出的目标节点上,不过,它只是Pod对象的默认调度器,使用中,用户还可以自定义调度器插件,并在定义Pod资源配置清单时通过spec.schedulerName指定即可使用。

1.2 节点亲和调度

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

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

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

img

图12-5 节点亲和性

1.2.1 节点硬亲和性

为Pod对象使用nodeSelector属性可以基于节点标签匹配的方式将Pod对象强制调度至某一类特定的节点之上,这一点在第4章中曾有介绍,不过它仅能基于简单的等值关系定义标签选择器,而nodeAffinity中支持使用matchExpressions属性构建更为复杂的标签选择机制。例如,下面的配置清单示例(required-nodeAffinity-pod.yaml)中定义的Pod对象,其使用节点硬亲和规则定义可将当前Pod对象调度至拥有zone标签且其值为foo的节点之上:


apiVersion: v1
kind: Pod
metadata:
  name: with-required-nodeaffinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - {key: zone, operator: In, values: ["foo"]}
  containers:
  - name: myapp
image: ikubernetes/myapp:v1

将上面配置清单中定义的资源创建于集群之中,由其状态信息可知它处于Pending阶段,这是由于强制型的节点亲和限制场景中不存在能够满足匹配条件的节点所致:


~]$ kubectl apply -f required-nodeAffinity-pod.yaml
pod "with-required-nodeaffinity" created
~]$ kubectl get pods with-required-nodeaffinity
NAME                         READY     STATUS    RESTARTS   AGE
with-required-nodeaffinity   0/1       Pending   0          53s

“kubectl describe”命令显示的资源详细信息Events字段中也给出了具体的原因“0/4nodesare available:4node(s)didn't match node selector”,命令及结果如下所示:


~]$ kubectl describe pods with-required-nodeaffinity
……
Events:
  Type     Reason            Age               From               Message
  ----     ------            ----              ----               -------
  Warning  FailedScheduling  6s (x6 over 21s)  default-scheduler  0/4 nodes are 
      available: 4 node(s) didn't match node selector.

接下来按图12-5中的规划为各节点设置节点标签,这也是设置节点亲和性的前提之一:


~]$ kubectl label node node02.ilinux.io zone=foo
node "node02.ilinux.io" labeled
~]$ kubectl label node node02.ilinux.io zone=foo
node "node02.ilinux.io" labeled
~]$ kubectl label node node03.ilinux.io zone=bar
node "node03.ilinux.io" labeled
~]$ kubectl label node node01.ilinux.io ssd=true
node "node01.ilinux.io" labeled
~]$ kubectl label node node03.ilinux.io ssd=true
node "node03.ilinux.io" labeled

设置完成后,Pod对象with-required-nodeaffinity的详细信息事件中已然出现成功调度至node01.ilinux.io节点的信息,具体如下所示:


Events:
  Type     Reason             Age              From                  Message
  ----     ------             ----             ----                  -------
  Warning  FailedScheduling    1m (x15 over 4m)  default-scheduler      0/4 nodes 
      are available: 4 node(s) didn't match node selector.
  Normal   Scheduled          14s      default-scheduler           Successfully 
      assigned with-required-nodeaffinity to node01.ilinux.io

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

下面的资源配置清单示例(required-nodeAffinity-pod2.yaml)中定义了调度拥有两个标签选择器的节点挑选条目,两个标签选择器彼此之间为“逻辑与”的关系,因此,满足其条件的节点为node01和node03,如图12-5右侧的Pod对象的指向所示:


apiVersion: v1
kind: Pod
metadata:
  name: with-required-nodeaffinity-2
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - {key: zone, operator: In, values: ["foo", "bar"]}
          - {key: ssd, operator: Exists, values: []}
  containers:
  - name: myapp
    image: ikubernetes/myapp:v1

构建标签选择器表达式中支持使用操作符有In、NotIn、Exists、DoesNotExist、Lt和Gt等,具体的用法请参考第4章中关于标签选择器的介绍。

另外,调度器在调度Pod资源时,节点亲和性(MatchNodeSelector)仅是其节点预选策略中遵循的预选机制之一,其他配置使用的预选策略依然正常参与节点预选过程。例如将上面资源配置清单示例中定义的Pod对象容器修改为如下内容并进行测试:


containers:
- name: myapp
  image: ikubernetes/myapp:v1
  resources:
    requests:
      cpu: 6
      memory: 20Gi

在预选策略PodFitsResources根据节点资源可用性进行节点预选的过程中,它会获取给定节点的可分配资源量(资源问题减去已被运行于其上的各Pod对象的requests属性之和),去除那些无法容纳新Pod对象请求的资源量的节点。本书试验环境中使用的四个节点(其中一个为Master)配置相同,均为8核心CPU和16GB内存,它们都无法满足容器myapp的需求,因此调度失败,Pod资源会被置于Pending状态。下面是将资源创建于集群中,而后通过其详细信息获取到的事件,它表明集群中仅有两个节点符合节点选择器,但4个节点都不具有充足的内存资源从而导致调度失败:


Events:
  Type     Reason            Age              From               Message
  ----     ------            ----             ----               -------
  Warning  FailedScheduling  4s (x2 over 4s)  default-scheduler  0/4 nodes are 
      available: 2 node(s) didn't match node selector, 4Insufficient memory.

由上述操作过程可知,节点硬亲和性实现的功能与节点选择器(nodeSelector)相似,但亲和性支持使用匹配表达式来挑选节点,这一点提供了灵活且强大的选择机制,因此可被理解为新一代的节点选择器。

1.2.2 节点软亲和性

节点软亲和性为节点选择机制提供了一种柔性控制逻辑,被调度的Pod对象不再是“必须”而是“应该”放置于某些特定节点之上,当条件不满足时,它也能够接受被编排于其他不符合条件的节点之上。另外,它还为每种倾向性提供了weight属性以便用户定义其优先级,取值范围是1~100,数字越大优先级越高。下面一个Deployment资源配置清单示例(deploy-with-preferred-nodeAffinity.yaml):


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

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

img

图12-6 节点软亲和性

以本书所用的测试环境为例,它共有三个节点(图12-6虚线内的节点),相对于myapp-deploy-with-node-affinity中定义的节点亲和性规则来说,它们所拥有的倾向性权重分别如图12-6中标识的信息所示。在创建需要3个Pod对象的副本时,它们会不会被创建于同一节点node01之上?下面来验证其运行效果:


~]$ kubectl create -f deploy-with-preferred-nodeAffinity.yaml
deployment.apps "myapp-deploy-with-node-affinity" created
~]$ kubectl get pods -l app=myapp -o wide
NAME                  READY  STATUS   RESTARTS  AGE   IP      NODE
myapp-deploy--8qrv7  1/1   Running  0   12s   10.244.2.131  node02.ilinux.io
myapp-deploy--j5d7j  1/1   Running  0   12s   10.244.1.20   node01.ilinux.io
myapp-deploy--twdsr  1/1   Running  0   12s   10.244.3.149  node03.ilinux.io

结果显示,三个Pod对象被分散运行于集群中的三个节点之上,而非集中运行于node01节点。之所以如此,是因为使用了节点软亲和性的预选方式,所有节点均能够通过调度器上MatchNodeSelector预选策略的筛选,因此,可用节点取决于其他预选策略的筛选结果。在第二阶段的优选过程中,除了NodeAffinityPriority优选函数之外,还有其他几个优选函数参与优先级评估,尤其是SelectorSpreadPriority,它会将同一个ReplicaSet控制器管控的所有Pod对象分散到不同的节点上运行以抵御节点故障带来的风险。不过,这种节点亲和性的权重依然在发挥作用,如果把副本数量扩展至越过节点数很多(如15个),那么它们将被调度器以接近节点亲和性权重比值(90:60:30)的方式分置于相关的节点之上,读者可自行验证其效果。

1.3 Pod资源亲和调度

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

当然,也可以通过节点亲和性来定义Pod对象间的亲和或反亲和特性,但用户必须为此明确指定Pod可运行的节点标签,显然这并非较优的选择。较理想的实现方式是,允许调度器把第一个Pod放置于任何位置,而后与其有亲和或反亲和关系的Pod据此动态完成位置编排,这就是Pod亲和性调度和反亲和性调度的功用。Pod的亲和性定义也存在“硬”(required)亲和性和“软”(preferred)亲和性的区别,它们表示的约束意义同节点亲和性相似。

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

1.3.1 位置拓扑

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

如果以基于各节点的kubernetes.io/hostname标签作为评判标准,那么很显然,“同一位置”意味着同一个节点,不同节点即不同的位置,如图12-8所示。

img

图12-7 Pod资源与位置拓扑

img

图12-8 基于节点的位置拓扑

而如果是基于如图12-9所划分的故障转移域来进行评判,那么server1和server4属于同一位置,而server2和server3属于另一个意义上的同一位置。

img

图12-9 基于故障转移域的位置拓扑

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

1.3.2 Pod硬亲和调度

Pod强制约束的亲和性调度也使用requiredDuringSchedulingIgnoredDuringExecution属性进行定义。Pod亲和性用于描述一个Pod对象与具有某特征的现存Pod对象运行位置的依赖关系,因此,测试使用Pod亲和性约束,需要事先存在被依赖的Pod对象,它们具有特别的识别标签。下面创建一个有着标签“app=tomcat”的Deployment资源部署一个Pod对象:


~]$kubectl run tomcat-l app=tomcat--image tomcat:alpine

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


apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity-1
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - {key: app, operator: In, values: ["tomcat"]}
        topologyKey: kubernetes.io/hostname
  containers:
  - name: myapp
    image: ikubernetes/myapp:v1

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


~]$ kubectl apply -f required-podAffinity-pod1.yaml
~]$ kubectl get pods -o wide
NAME                     READY  STATUS   RESTARTS  AGE IP            NODE
tomcat-69f99cdf9d-6hvb5  1/1    Running  0         6m  10.244.3.152  node03.ilinux.io
with-pod-affinity-1      1/1    Running  0         4s  10.244.3.154  node03.ilinux.io

基于单一节点的Pod亲和性只在极个别的情况下才有可能会用到,较为常用的通常是基于同一地区(region)、区域(zone)或机架(rack)的拓扑位置约束。例如部署应用程序服务(myapp)与数据库(db)服务相关的Pod时,db Pod可能会部署于如图12-10所示的foo或bar这两个区域中的某节点之上,依赖于数据服务的myapp Pod对象可部署于db Pod所在区域内的节点上。当然,如果db Pod在两个区域foo和bar中各有副本运行,那么myapp Pod将可以运行于这两个区域的任何节点之上。

例如,创建具有两个拥有标签为“app=db”的副本Pod作为被依赖的资源,它们可能运行于类似图12-5所示的三个节点中的任何一个或两个节点之上,本示例中它们凑巧运行于两个zone标签值不同的节点之上:


~]$ kubectl run db -l app=db --image=redis:alpine --replicas=2
~]$ kubectl get pods -l app=db -o wide -w
NAME                  READY   STATUS    RESTARTS  AGE  IP   NODE
db-6b97b54df7-gbsl6   1/1     Running   ……                 node03.ilinux.io
db-6b97b54df7-rhzrp   1/1     Running   ……                 node02.ilinux.io

img

图12-10 Pod硬亲和性调度

于是,依赖于亲和于这两个Pod的其他Pod对象可运行于zone标签值为foo和bar的区域内的所有节点之上。下面的资源配置清单(deploy-with-required-podAffinity.yaml)中正是定义了这样一些Pod资源,它们由Deployment控制器所创建:


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:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - {key: app, operator: In, values: ["db"]}
            topologyKey: zone
      containers:
      - name: myapp
        image: ikubernetes/myapp:v1

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

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

1.3.3 Pod软亲和调度

类似于节点亲和性机制,Pod也支持使用preferredDuringSchedulingIgnoredDuringExecution属性定义柔性亲和机制,调度器会尽力确保满足亲和约束的调度逻辑,然而在约束条件不能得到满足时,它也允许将Pod对象调度至其他节点运行。下面是一个使用了Pod软亲和性调度机制的资源配置清单示例(deploy-with-preferred-podAffinity.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:
      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
      containers:
      - name: myapp
        image: ikubernetes/myapp:v1

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

1.3.4 Pod反亲和调度

podAffinity用于定义Pod对象的亲和约束,对应地,将其替换为podAntiAffinty即可用于定义Pod对象的反亲和约束。不过,反亲和性调度一般用于分散同一类应用的Pod对象等,也包括将不同安全级别的Pod对象调度至不同的区域、机架或节点等。下面的资源配置清单(deploy-with-required-podAntiAffinity.yaml)中定义了由同一Deployment创建但彼此基于节点位置互斥的Pod对象:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-with-pod-anti-affinity
spec:
  replicas: 4
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      name: myapp
      labels:
        app: myapp
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - {key: app, operator: In, values: ["myapp"]}
            topologyKey: kubernetes.io/hostname
      containers:
      - name: myapp
        image: ikubernetes/myapp:v1

由于定义的强制性反亲和约束,因此,创建的4个Pod副本必须运行于不同的节点中。不过,本集群中一共只存在3个节点,因此,必然地会有一个Pod对象处于Pending状态,如下所示:


~]$ kubectl get pods -o wide -l app=myapp
NAME            READY     STATUS    RESTARTS   AGE   IP       NODE
myapp--4gcvv   0/1      Pending   0          2s    <none>   <none>
myapp--788k6   1/1      Running   0          ……   node01.ilinux.io
myapp--cxqv5   1/1      Running   0          ……   node03.ilinux.io
myapp--mk28s   1/1      Running   0          ……   node02.ilinux.io

类似地,Pod反亲和性调度也支持使用柔性约束机制,在调度时,它将尽量满足不把位置相斥的Pod对象调度于同一位置,但是,当约束关系无法得到满足时,也可以违反约束而调度。读者可参考podAffinity的柔性约束示例将上面的Deployment资源myapp-with-pod-anti-affinity修改为柔性约束并进行调度测试。

1.4 污点和容忍度

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

img

图12-11 污点与容忍之间的关系示意图

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

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

1.4.1 定义污点和容忍度

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

·NoSchedule:不能容忍此污点的新Pod对象不可调度至当前节点,属于强制型约束关系,节点上现存的Pod对象不受影响。

·PreferNoSchedule:NoSchedule的柔性约束版本,即不能容忍此污点的新Pod对象尽量不要调度至当前节点,不过无其他节点可供调度时也允许接受相应的Pod对象。节点上现存的Pod对象不受影响。

·NoExecute:不能容忍此污点的新Pod对象不可调度至当前节点,属于强制型约束关系,而且节点上现存的Pod对象因节点污点变动或Pod容忍度变动而不再满足匹配规则时,Pod对象将被驱逐。

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

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

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

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

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

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

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


~]$ kubectl describe node master.ilinux.io
Name:               master.ilinux.io
Roles:              master
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/hostname=master.ilinux.io
                    node-role.kubernetes.io/master=
……
Taints:             node-role.kubernetes.io/master:NoSchedule
Unschedulable:      false

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


~]$ kubectl describe pods kube-flannel-ds-lsgqr -n kube-system
……
Node-Selectors:  beta.kubernetes.io/arch=amd64
Tolerations:     node-role.kubernetes.io/master:NoSchedule
                 node.kubernetes.io/disk-pressure:NoSchedule
                 node.kubernetes.io/memory-pressure:NoSchedule
                 node.kubernetes.io/not-ready:NoExecute
                 node.kubernetes.io/unreachable:NoExecute

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

1.4.2 管理节点的污点

任何符合其键值规范要求的字符串均可用于定义污点信息:仅可使用字母、数字、连接符、点号和下划线,且仅能以字母或数字开头,其中键名的长度上限为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”定义节点node01.ilinux.io:


~]$ kubectl taint nodes node01.ilinux.io node-type=production:NoSchedule
node "node01.ilinux.io" tainted

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


~]$ kubectl get nodes node01.ilinux.io -o go-template={{.spec.taints}}
[map[value:production effect:NoSchedule key:node-type]]

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


~]$ kubectl taint nodes node01.ilinux.io node-type=production:PreferNoSchedule
node "node01.ilinux.io" tainted

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


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

例如,删除node01上node-type键的效用标识为“NoSchedule”的污点信息:


~]$ kubectl taint nodes node01.ilinux.io node-type:NoSchedule-
node "node01.ilinux.io" untainted

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


~]$ kubectl taint nodes node01.ilinux.io node-type-
node "node01.ilinux.io" untainted

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


~]$ kubectl patch nodes node01.ilinux.io -p '{"spec":{"taints":[]}}'
node "node01.ilinux.io" patched

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

1.4.3 Pod对象的容忍度

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


tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
  tolerationSeconds: 3600

使用存在性判断机制的容忍度示例如下所示:


tolerations:
- key: "key1"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 3600

实践中,若集群中的一组机器专用于为运行非生产型的容器应用而备置,而且它们可能随时按需上下线,那么就应该为其添加污点信息,以确保仅那些能容忍此污点的非生产型Pod对象可以调度其上。另外,某些有着特殊硬件的节点需要专用于运行一类有着此类硬件资源需求的Pod对象时,例如,那些有着SSD或GPU的设备,也应该为其添加污点信息以排除其他的Pod对象。

1.4.4 问题节点标识

Kubernetes自1.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/uninitialized:kubelet由外部的云环境程序启动时,它将自动为节点添加此污点,待到云控制器管理器中的控制器初始化此节点时再将其删除。

不过,Kubernetes的核心组件通常都要容忍此类的污点,以确保其相应的DaemonSet控制器能够无视此类污点,于节点上部署相应的关键性Pod对象,例如kube-proxy或kube-flannel等。

1.5 Pod优选级和抢占式调度

Kubernetes自1.8版本起开始支持Pod资源的优选级机制,它用于表现一个Pod对象相对于其他Pod对象的重要程度。一个Pod对象无法被调度时,调度器会尝试抢占(驱逐)较低优先级的Pod对象,以便可以调度当前Pod。另外,在Kubernetes 1.9和更高版本中,优先级还会影响节点上Pod的调度顺序和驱逐次序。

不过,Pod优选级和抢占机制默认处于禁用状态,如需启用,则需要同时为kube-apiserver、kube-scheduler和kubelet程序的“--feature-gates”选项添加“PodPriority=true”条目。添加完成后,事先创建好优先级类别,并在创建Pod资源时通过priorityClassName属性指定其所属的优选级类别即可。

此种特性目前仍处于Alpha阶段,因此,本篇不再对其进行过多的描述,感兴趣的读者可参考文档进行测试。

1.6 小结

本篇讲解了Kubernetes默认调度器的分步调度机制,描述了各预选策略和优选函数的功能,并对节点亲和性调度、Pod亲和性调度以及基于污点和容忍的高级调度方式进行了重点说明。

版权声明:如无特殊说明,文章均为本站原创,版权所有,转载需注明本文链接

本文链接:http://www.bianchengvip.com/article/Kubernetes-Pod-Resource-Scheduling/