pod调度总结

简介: 总结pod的调度相关知识

01 概述

1.1 Pod调度控制器分类

Kubernetes平台上,我们很少会直接创建一个Pod,在大多数情况下会通过如下控制器完成对一组Pod副本的创建、调度 及全生命周期的自动控制任务

  • RC
  • Deployment
  • DaemonSet
  • Job等

1.2 RC到Deployment的发展

在最早的Kubernetes,版本里是没有这么多Pod副本控制器的,只有一个Pod副本控制器RC(Replication Controller),这个控制器是这样设计实现的:

RC独立于所控制的Pod,并通过Label标签这个松耦合关联关系控制目标Pod实例的创建和销毁

随着Kubernetes的发展,RC也出现了新的继任者-Deployment,用于更加自动地完成Pod副本的部署、版本更新、回滚等功能

严谨地说,RC的继任者其实并不是Deployment,而是ReplicaSet,因为
ReplicaSet进一步增强了RC标签选择器的灵活性(之前RC的标签选择器只能选择一个标签而ReplicaSet拥有集合式的标签选择器,可以选择多个Pod标签),如下所示:
在这里插入图片描述

1.2.1 ReplicaSet

RC不同,ReplicaSet被设计成能控制多个不同标签的Pod副本

举例: 应用MyApp目前发布了v1v2两个版本,用户希望MyAppPod副本数保持为3个,可以同时包含v1v2版本的Pod,就可以用ReplicaSet来实现这种控制,写法如下:

在这里插入图片描述
其实,Kubernetes的滚动升级就是巧妙运用ReplicaSet的这个特性来实现的, 同时,Deployment也是通过ReplicaSet来实现Pod副本自动控制功能的。

我们不应该直接使用底层的ReplicaSet来控制Pod副本,而应该通过管理ReplicaSet的Deployment对象来控制副本,这是来自官方的建议。

1.3 Pod调度

在大多数情况下,我们希望Deployment创建的Pod副本被成功调度到集群中的任何一个可用节点,而不关心具体会调度到哪个节点。

1.3.1 情景

但是,在真实的生产环境中的确也存在一种需求:希望某种Pod的副本全部在指定的一个或者一些节点上运行,比如希望将MySQL数据库调度到一个具有SSD磁盘的目标节点上

此时Pod模板中的NodeSelector属性就开始发挥作用了,上述MySQL定向调度案例的实现方式可分为以下两步:

  1. 把具有SSD磁盘的Node都打上自定义标签disk=ssd
  2. 在Pod模板中设定NodeSelector的值为“disk:ssd”

如此一来,Kubernetes在调度Pod副本的时候,就会先按照Node的标签过滤出合适的目标节点,然后选择一个最佳节点进行调度。

1.3.2 存在的问题

上述逻辑看起来既简单又完美,但在真实的生产环境中可能面临以下令人尴尬的问题:

  1. 如果NodeSelector选择的Label不存在或者不符合条件(比如:这些目标节点此时宕机或者资源不足,该怎么办?)
  2. 如果要选择多种合适的目标节点,比如SSD磁盘的节点或者超高速硬盘的节点,该怎么办?

备注:Kubernetes引入了NodeAffinity(节点亲和性设置)来解决该需求

1.3.3 解决方式

在真实的生产环境中还存在如下所述的特殊需求:

需求 举例描述 解决方式
不同Pod之间的亲和性(Affinity) 比如MySQL数据库与Redis中间件 不能被调度到同一个目标节点上,或者两种不同的Pod必须被调度到同一个Node 上,以实现本地文件共享或本地网络通信等特殊需求 PodAffinity来解决该问题
有状态集群的调度 对于ZooKeeper、Elasticsearch、MongoDB、Kafka等有状态集群,虽然集群中的每个Worker节点看起来都是相同的,但每个Worker节点都必须有明确的、不变的唯一ID(主机名或IP地址),这些节点的启动和停止次序通常有严格的顺序。此外,由于集群需要持久化保存状态数据所以集群中的Worker节点对应的Pod不管在哪个Node上恢复,都需要挂载原来的Volume,因此这些Pod还需要捆绑具体的PV 针对这种复杂的需求,Kubernetes 提供了StatefulSet这种特殊的副本控制器来解决问题,在Kubernetes1.9版本发布后,StatefulSet才可用于正式生产环境中
在每个Node上调度并且仅仅创建一个Pod副本 这种调度通常用于系统监控相关的Pod,比如主机上的日志采集、主机性能采集等进程需要被部署到集群中的每个节点,并且只能部署一个副本 DaemonSet来解决这种特殊Pod副本控制器
对于批处理作业,需要创建多个Pod副本来协同工作,当这些Pod副本都完成自己的任务时,整个批处理作业就结束了 这种Pod运行且仅运行一次的特殊调度,用常规的RC或者Deployment都无法解决 引入了新的Pod调度控制器Job来解决问题,并继续延伸了定时作业的调度控制器CronJob

与单独的Pod实例不同,由RC、ReplicaSet、Deployment、DaemonSet等控制器创建的Pod副本实例都是归属于这些控制器的,这就产生了一个问题:控制器被删除后,归属于控制器的Pod副本该何去何从?

kubernetes版本 操作
1.9之前 在RC等对象被删除后,它们所创建的Pod副本都不会被删除
1.9以后 这些Pod副本会被一并删除。如果不希望这样做,则可以通过kubectl命令的- cascade=false参数(例如:kubectl delete replicaset my-repset --cascade=false)来取消这一默认特性

02 全自动调度

2.1 功能

Deployment或RC的主要功能之一就是自动部署一个容器应用的多份副本,以及持续监控副本的数量,在集群内始终维持用户指定的副本数量,。

2.2 举例

举例:使用配置文件可以创建一个ReplicaSet,这个ReplicaSet会创建3个Nginx应用的Pod

在这里插入图片描述

使用create命令创建之后,查看Deployment的状态:

kubectl get deployments

在这里插入图片描述

该状态说明Deployment已创建好所有3个副本,并且所有副本都是最新的可用的

通过运行kubectl get rskubectl get pods可以查看已创建的ReplicaSet (RS)Pod的信息。
在这里插入图片描述

从调度策略上来说,这3个Nginx Pod由系统全自动完成调度。它们各自最终运行在哪个节点上,完全由Master的Scheduler经过一系列算法计算得出,用户无法干预调度过程和结果。

03 定向调度

3.1 Step1- 给Node打上标签

如果要实现定向调度,首先的第一步就是要为Node节点搭上标签(Label),可以使用kubectl label命令:

kubectl label nodes <node-name><label-key>=<label-value>

例如这里为k8s-node-1节点打上一个zone=north标签,表明它是“北方”的一个节点:
在这里插入图片描述

3.2 Step2- Pod指定NodeSelector

然后,在Pod的定义中加上nodeSelector的设置,以redis-master- controller.yaml为例:
在这里插入图片描述

3.3 Step3- 验证

运行kubectl create -f命令创建Podscheduler就会将该Pod调度到拥有 “zone=north” 标签的Node上。

使用kubectl get pods-o wide命令可以验证Pod所在的Node
在这里插入图片描述

需要注意的是,如果我们指定了PodnodeSelector条件,且在集群中不存在包含相应标签的Node,则即使在集群中还有其他可供使用的Node这个Pod也无法被成功调度

3.4 预定义的标签

除了用户可以自行给Node添加标签,Kubernetes也会给Node预定义一些标签,包括:

  • kubernetes.io/hostname;
  • beta.kubernetes.io/os(从1.14版本开始更新为稳定版,到1.18版本删除);
  • beta.kubernetes.io/arch(从1.14版本开始更新为稳定版,到1.18版本删除);
  • kubernetes.io/os(从1.14版本开始启用);
  • kubernetes.io/arch(从1.14版本开始启用)。

04 node亲和性调度

4.1 亲和性调度分类

目前有两种节点亲和性表达:

表达式 含义
RequiredDuringSchedulingIgnoredDuringExecution 必须满足指定的规则才可以调度Pod到Node上(功能与nodeSelector很像,但是使用的是不同的语法),相当于限制
PreferredDuringSchedulingIgnoredDuringExecution 强调优先满足指定规则,调度器会尝试调度Pod到Node上,但并不强求,相当于软限制
多个优先级规则还可以设置权重(weight)值,以定义执行的先后顺序。

IgnoredDuringExecution的意思是:如果一个Pod所在的节点在Pod运行期间 标签发生了变更,不再符合该Pod的节点亲和性需求,则系统将忽略NodeLabel 的变化,该Pod能继续在该节点上运行

4.2 举例

有如下要求:

  • requiredDuringSchedulingIgnoredDuringExecution:要求只运行在amd64的节点上(beta.kubernetes.io/arch In amd64);
  • preferredDuringSchedulingIgnoredDuringExecution:要求尽量运行在磁盘类型为ssd(disk-type In ssd)的节点上;

则资源文件的定义如下:

apiVersion:vl
kind:Pod
metadata:
    name:with-node-affinity
spec:
    affinity:
        nodeAffinity:
            requiredDuringSchedulingIgnoredDuringExecution: 
                nodeSelectorTerms
                - matchExpressions:
                    - key:beta.kubernetes.io/arch 
                      operator:In
                      values:
                      - amd64
            preferredDuringSchedulingIgnoredDuringExecution: 
            - weight:1
              preference:
              matchExpressions:
              - key:disk-type
                 operator:In
                values:
                - ssd
    containers:
    - name:with-node-affinity
      image:gcr.io/google containers/pause:2.0

从上面的配置中可以看到In操作符,NodeAffinity语法支持的操作符包括In、NotIn、Exists、DoesNotExist、Gt、Lt。虽然没有节点排斥功能,但是用NotInDoesNotExist就可以实现排斥的功能了

4.3 注意事项

  • 如果同时定义了nodeSelectornodeAffinity,那么必须两个条件都得到满足,Pod才能最终运行在指定的Node上;
  • 如果nodeAffinity指定了多个nodeSelectorTerms,那么其中一个能匹配成功即可;
  • 如果在nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod。

05 亲和性与互斥性调度

亲和性与互斥性可以理解为就是相关联的两种或多种Pod是否可以在同一个拓扑域中共存或者互斥

那么什么是拓扑域?

5.1 拓扑域

拓扑域的概念:

  • 一个拓扑域由一些Node节点组成,这些Node节点通常有相同的地理空间坐标,比如在同一个机架、机房或地区;
  • 一般用region表示机架、 机房等的拓扑区域,用Zone表示地区这样跨度更大的拓扑区域;
  • 极端情况下, 我们也可以认为一个Node就是一个拓扑区域。

k8s内置了如下一些常用的默认拓扑域,主要是为了确定各个节点所属的拓扑域

默认拓扑域 描述
kubernetes.io/hostname 在Node节点初始化时,controller--manager会为Node打上该标签
topology.kubernetes.io/region 公有云厂商提供的Kubernetes服务或者使用cloud-controller-manager创建的集群,会给Node打上该标签
topology.kubernetes.io/zone 同上

5.2 举例

Pod亲和与互斥的调度是通过在Pod的定义上增加topologyKey 属性来声明对应的目标拓扑区域内几种相关联的Pod要 “在一起或不在一起”。

与节点亲和相同,Pod亲和与互斥的条件设置也是requiredDuringSchedulingIgnoredDuringExecution
preferredDuringSchedulingIgnoredDuringExecution

  • Pod的亲和性被定义于PodSpec的affinity字段的podAffinity子字段中;
  • Pod间的互斥性则被定义于同一层次的podAntiAffinity子字段中.

下面通过实例来说明Pod间的亲和性和互斥性策略设置。

5.2.1 参照目标pod

首先,创建一个名为pod-flagPod,带有标签security=S1app=nginx,后面的例子将使用pod-flag作为Pod亲和与互斥的目标Pod

apiversion:v1
kind:Pod
metadata:
    name:pod-flag
    labels:
        security:"S1"
        app:"nginx"
spec:
    containers:
    -name:nginx
    image:nginx

5.2.2 pod的亲和性调度

下面创建第2个Pod来说明Pod的亲和性调度,这里定义的亲和标签是 “security=S1”,对应上面的Pod “pod-flag”,topologyKey的值被设置为 “kubernetes.io/hostname“:

apiVersion:vl
kind:Pod
metadata:
    name:pod-affinity
spec:
    affinity:
        podAffinity:
            requiredDuringSchedulingIgnoredDuringExecution: 
            - labelSelector:
                matchExpressions:
                - key:security
                  operator:In
                  values:
                  -S1
              topologyKey:kubernetes.io/hostname 
    containers:
    - name:with-pod-affinity
      image:gcr.io/google_containers/pause:2.0

创建Pod之后,使用kubectl get pods -o wide命令可以看到,这两个Pod在同
一个Node上运行。

在创建这个 Pod之前,删掉这个节点的 kubernetes.io/hostname 标签,重复上面的创建步骤,将会发现 Pod一直处于 Pending状态,这是因为找不到满足条件的 Node了。

5.2.3 pod的互斥性调度

创建第3个Pod,我们希望它不与目标Pod运行在同一个Node上:

apiversion:v1
kind:Pod
metadata:
    name:anti-affinity
spec:
    affinity:
        podAffinity:
            requiredDuringSchedulingIgnoredDuringExecution: 
            - labelSelector:
              matchExpressions:
              - key:security
                operator:In
                values:
                -S1
              topologyKey:topology.kubernetes.io/zone 
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution: 
          - labelSelector:
            matchExpressions:
            - key:app
              operator:In
              values:
              -nginx
            topologyKey:kubernetes.io/hostname
    containers:
    - name:anti-affinity
      image:gcr.io/google_containers/pause:2.0

这里要求这个新Podsecurity=S1的Pod为同一个zone,但是不与app=nginxPod为同一个Node

创建Pod之后,同样用kubectl get pods -o wide来查看,会看到新的Pod被调度到了同一Zone内的不同Node上。

5.4 其它

与节点亲和性类似,Pod亲和性的操作符也包括`In、NotIn、Exists、
DoesNotExist、Gt、Lt`。

原则上,topologyKey可以使用任意合法的标签Key赋值,但是出于性能和安全方面的考虑,对topologyKey有如下限制:

  • 在Pod亲和性和RequiredDuringScheduling的Pod互斥性的定义中,不允许使用空的topologyKey
  • 如果Admission controller包含了LimitPodHardAntiAffinityTopology,那么针对Required DuringScheduling的Pod互斥性定义就被限制为kubernetes.io/hostname,要使用自定义的topologyKey,就要改写或禁用该控制器。
  • 在PreferredDuringScheduling类型的Pod互斥性定义中,空的

topologyKey会被解释为kubernetes.io/hostname、failure-
domain.beta.kubernetes.io/zone 及 failure-domain.beta.kubernetes.io/region的组合。

  • 如果不是上述情况,就可以采用任意合法的topologyKey了。

PodAffinity规则设置的注意事项如下:

  • 除了设置Label Selector和topologyKey,用户还可以指定Namespace列表

进行限制。同样,使用Label Selector对Namespace进行选择,Namespace的定义 和Label Selector及topologyKey同级,省略Namespace的设置,表示使用定义了 affinity/anti-affinity的Pod所在的命名空间。如果Namespace被设置为空值 (""),则表示所有命名空间.

  • 在所有关联requiredDuringSchedulingIgnoredDuringExecution的

matchExpressions 全都满足之后 ,系统才能将Pod调度到某个Node上。

06 污点与容忍

Taint(污点) 则正好相反,它让Node拒绝Pod的运行。简单地说,被标记为Taint的节点就是存在问题的节点,比 如磁盘要满、资源不足、存在安全隐患要进行升级维护,希望新的Pod不会被调度过来。

但被标记为Taint的节点并非故障节点,仍是有效的工作节点,所以仍需将某些Pod调度到这些节点上时,可以通过使用Toleration属性来实现。

6.1.1 污点与容忍设置

在默认情况下,在Node上设置一个或多个Taint之后,除非Pod明确声明能够容忍这些污点,否则无法在这些Node上运行。

6.1.1.1 Node设置污点

可以用kubectl taint命令为Node设置Taint信息:

kubectl taint nodes node1 key=value:NoSchedule

描述:这个设置为node1加上了一个Taint,该Taint的键为key,值为valueTaint的效果是NoSchedule,这意味着除非Pod明确声明可以容忍这个Taint,否则不会被调度到node1

6.1.1.2 Pod声明容忍

Pod上声明容忍的例子如下,下面的两个Toleration都被设置为可以容忍(Tolerate)具有该TaintNode,使得Pod能够被调度到node1上:

tolerations:
- key: "key"
  operator: "Equal"
  value: "value"
  effect: "NoSchedule"

或者

tolerations:
- key: "key"
  operator: "Exists"
  effect: "NoSchedule"

6.1.1.3 小结

PodToleration声明中的keyeffect需要与Taint的设置保持一致,并且满足以下条件之一:

条件
key 空的key配合Exists操作符能够匹配所有键和值
operator 值是Exists(无须指定value), operator的值是Equal并且value相等, 如果不指定operator,则默认值为Equal
effect 空的effect匹配所有effect,在上面的例子中,effect的取值为NoSchedule,还可以取值为PreferNoSchedule,这个值的意思是优先,也可以算作NoSchedule的软限制版本 - 一个Pod如果没有声明容忍这个Taint,则系统会尽量避免把这个Pod调度到这一 节点上,但不是强制的

系统允许在同一个Node上设置多个Taint,也可以在Pod上设置多个Toleration

Kubernetes调度器处理多个Taint和Toleration的逻辑顺序为:首先列出节点中所有的Taint,然后忽略Pod的Toleration能够匹配的部分,剩下的没被忽略的Taint就是对Pod的效果了

6.1.2 特殊情况

下面是几种特殊情况:

  • 如果在剩余的Taint中存在effect=NoSchedule,则调度器不会把该Pod调度到这一节点上;
  • 如果在剩余的Taint中没有NoSchedule效果,但是有PreferNoSchedule效果,则调度器会尝试不把这个Pod指派给这个节点;
  • 如果在剩余的Taint中有NoExecute效果,并且这个Pod已经在该节点上运行,则会被驱逐;
  • 如果没有在该节点上运行,则也不会再被调度到该节点上。

例如,我们这样对一个节点进行Taint设置:

kubectl taint nodes node1 keyl=valuel:NoSchedule 
kubectl taint nodes node1 keyl=valuel:NoExecute 
kubectl taint nodes node1 key2=value2:NoSchedule

然后在Pod上设置两个Toleration:

tolerations:
- key: "key1"
  operator: "Equal"
  value: "valuel"
  effect: "NoSchedule"
- key: "key1"
  operator: "Equal"
  value: "valuel"
  effect: "NoExecute"

结果:

  • 这样的结果是该Pod无法被调度到node1上,这是因为第3个Taint没有匹配的Toleration
  • 但是如果该Pod已经在node1上运行了,那么在运行时设置第3个Taint,它还能继续在node1上运行,这是因为Pod可以容忍前两个Taint。

一般来说,如果给Node加上effect=NoExecuteTaint,那么在该Node上正在运行的所有无对应TolerationPod都会被立刻驱逐,而具有相应TolerationPod永远不会被驱逐。不过,系统允许给具有NoExecute效果的Toleration加入一 个可选tolerationSeconds“字段,这个设置表明Pod可以在Taint添加到Node之后还能在这个Node上运行多久(单位为s):

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

上述定义的意思是,如果Pod正在运行,所在节点都被加入一个匹配的Taint,则这个Pod会持续在这个节点上存活3600s后被逐出。如果在这个宽限期内Taint被移除,则不会触发驱逐事件

6.2 应用场景

TaintToleration一种处理节点并且让Pod进行规避或者驱逐Pod的弹性处理方式,下面列举一些常见的用例。

6.2.1 独占节点

如果想要拿出一部分节点专门给一些特定应用使用,则可以为节点添加这样Taint

kubectl taint nodes nodename dedicated=groupName:NoSchedule

然后给这些应用的Pod加入对应的Toleration,这样,带有合适TolerationPod就会被允许同使用其他节点一样使用有Taint的节点

通过自定义Admission Controller也可以实现这一目标。如果希望让这些应用独占一批节点,并且确保它们只能使用这些节点,则还可以给这些Taint节点加入类似的标签dedicated=groupName,然后Admission Controller需要加入节点亲和 性设置,要求Pod只会被调度到具有这一标签的节点上。

6.2.2 具有特殊硬件设备的节点

在集群里可能有一小部分节点安装了特殊的硬件设备(如GPU芯片),用户自然会希望把不需要占用这类硬件的Pod排除在外,以确保对这类硬件有需求的Pod能够被顺利调度到这些节点上

可以用下面的命令为节点设置Taint

kubectl taint nodes nodename special=true:NoSchedule 
kubectl taint nodes nodename special=true:PreferNoSchedule

然后在Pod中利用对应的Toleration来保障特定的Pod能够使用特定的硬件。

和上面独占节点的示例类似,使用Admission Controller来完成这一任务会更方便,例如:

  • Admission Controller使用Pod的一些特征来判断这些Pod,如果可以使用这些件,就添加Toleration来完成这一工作;
  • 要保障需要使用特殊硬件的Pod只被调度到安装这些硬件的节点上,则还需要一些额外的工作,比如将这些特殊资源使用opaque-int-resource的方式对自定义资源进行量化,然后在PodSpec中进行请求;
  • 也可以使用标签的方式来标注这些安装有特别硬件的节点,然后在Pod 中定义节点亲和性来实现这个目标。

6.2.3 定义Pod驱逐行为,以应对节点故障

前面提到的NoExecute这个Taint效果对节点上正在运行的Pod有以下影响:

  • 没有设置Toleration的Pod会被立刻驱逐;
  • 配置了对应Toleration的Pod,如果没有为tolerationSeconds赋值,则会一直留在这一节点中;
  • 配置了对应Toleration的Pod且指定了tolerationSeconds值,则会在指定的时间后驱逐(注意,在节点发生故障的情况下,系统将会以限速(rte- limiting)模式逐步给Node设置Taint,这样就能避免在一些特定情况下(比如

Master暂时失联)有大量的Pod被驱逐)。

注意,Kubernetes会自动给Pod添加下面几种Toleration:

  • key为node.kubernetes.io/not-ready,并配置tolerationSeconds=300;
  • key 为node.kubernetes.io/unreachable,并配置tolerationSeconds=300。

以上添加的这种自动机制保证了在某些节点发生一些临时性问题时,Pod默认能够继续停留在当前节点运行5min等待节点恢复,而不是立即被驱逐,从而避免系统的异常波动。

另外,Kubernetes从1.6版本开始引入两个与Taint相关的新特性,TaintNodesByCondition及TaintBasedEvictions用来改善异常情况下的Pod调度与驱逐问题,比如在节点内存吃紧、节点磁盘空间已满、节点失联等情况下,是 否自动驱逐某些Pod或者暂时保留这些Pod等待节点恢复正常。这个过程的完整逻 辑基本如下。

  1. 不断地检查所有Node状态,设置对应的Condition;
  2. 不断地根据Node Condition设置对应的Taint;
  3. 不断地根据Taint驱逐Node上的Pod。

其中,检查Node的状态并设置NodeTaint就是TaintNodesByCondition特性,即在Node满足某些特定的条件时,自动为Node节点添加Taint,目前主要有以下几种条件:

条件 描述
node.kubernetes.io/not-ready:节点未就绪 对应NodeCondition Ready为False的情况
node.kubernetes.io/unreachable:节点不可触达 对应NodeCondition Ready.为Unknown的情况
node.kubernetes.io/out-of-disk 节点磁盘空间已满
node.kubernetes.io/network-unavailable 节点网络不可用
node.kubernetes.io/unschedulable 节点不可调度
node.cloudprovider,kubernetes.io/uninitialized 如果kubelet是由"外部"云服务商启动的,则该污点用来标识某个节点当前为不可用状态。在云控制器 (cloud-controller-manager)初始化这个节点以后,kubelet会将此污点移除

自Kubernetes 1.13开始,上述两个特性被默认启用,TaintNodesByCondition 这个特性只会为节点添加NoSchedule效果的污点,TaintBasedEviction则为节点添加NoExecute效果的污点。

在TaintBasedEvictions特性被开启之后,kubelet会在有资源压力时对相应的Node节点自动加上对应的NoExecute效果的Taint,例如 node.kubernetes.io/memory-pressure、node.kubernetes.io/disk-pressure。

如果Pod没有设置对应的Toleration,则这部分Pod将被驱逐,以确保节点不会崩溃。

07 优先级调度

对于运行各种负载(如:ServiceJob)的中等规模或者大规模的集群来说,出于各种原因,我们需要尽可能提高集群的资源利用率

提高资源利用率的常规做法是采用优先级方案,即不同类型的负载对应不同的优先级,同时允许集群中的所有负载所需的资源总量超过集群可提供的资源,在这种情况下,当发生资源不足的情况时,系统可以选择释放一些不重要的负载(优先级最低的),保障最重要的负载能够获取足够的资源稳定运行。

7.1 案例

7.1.1 创建PriorityClass

首先,由集群管理员创建PriorityClassPriorityClass不属于任何命名空间):

apiversion:scheduling.k8s.io/vlbetal kind:Priorityclass
metadata:
    name:high-priority
va1ue:1000000
globalDefault:false
description:"This priority class should be used for XYZ service pods only."

上述YAML文件定义了一个名为high-priority的优先级类别,优先级为 100000数字越大,优先级越高,超过一亿的数字被系统保留,用于指派给系统组件。

7.1.2 Pod声明优先级类别

可以在任意Pod上引用上述Pod优先级类别:

apiVersion: v1
kind: Pod
metadata:
    name: nginx 
    labels:
        env: test
spec:
    containers:
- name: nginx
  image: nginx
  imagePullPolicy: IfNotPresent 
  priorityclassName: high-priority

如果发生了需要抢占的调度,高优先级Pod就可能抢占节点N,并将其低优先级Pod驱逐出节点N,高优先级Podstatus信息中的nominatedNodeName字段会记录目标节点的名称。

需要注意,高优先级Pod仍然无法保证最终被调度到节点N上,在节点N上低优先级Pod被驱逐的过程中,如果有新的节点满足高优先级Pod的需求,就会把它调度到新的Node

而如果在等待低优先级的Pod退出的过程中,又出现了优先级更高的Pod,调度器就会调度这个更高优先级的Pod到节点N上,并重新调度之前等待的高优先级Pod

7.1.3 注意事项

优先级抢占的调度方式可能会导致调度陷入“死循环”状态。当Kubernetes集群配置了多个调度器(Scheduler)时,这一行为可能就会发生,比如下面这个例子:

Scheduler A为了调度一个(批) Pod,特地驱逐了一些 Pod,因此在集群中有了空余的空间可以用来调度,此时 Scheduler B恰好抢在 Scheduler A之前调度了一个新的 Pod,消耗了相应的资源,因此,当 Scheduler A清理完资源后正式发起 Pod的调度时,却发现资源不足,被目标节点的 kubelet进程拒绝了调度请求! 这种情况的确无解,因此最好的做法是 让多个Scheduler相互协作来共同实现一个目标。

高优先级Pod抢占节点并驱逐低优先级的Pod,这个问题对于普通的服务型的
Pod来说问题不大,但对于执行批处理任务的Pod来说就可能是个灾难,当一个高 优先级的批处理任务的Pod创建后,正在执行批处理任务的某个低优先级的Pod可 能因为资源不足而被驱逐,从而导致对应的批处理任务被搁置。

为了避免这个问题发生,PriorityClass增加了一个新的属性一preemptionPolicy,当它的值为 preemptionLowerPriorty(默认)时,就执行抢占功能,当它的值被设置为Never 时,就默认不抢占资源,而是静静地排队,等待自己的调度机会

08 DaemonSet(每个node上只调度一个pod)

DaemonSetKubernetes1.2 版本新增的一种资源对象,用于管理在集群中的每个Node上仅运行一份Pod的副本实例,如下图所示:
在这里插入图片描述

8.1 DaemonSet

8.1.1 应用场景

下面举例DaemonSet的一些使用场景:

  • 在每个Node上都运行一个GlusterFS存储或者Ceph存储的Daemon进程;
  • 在每个Node上都运行一个日志采集程序,例如Fluentd或者Logstach
  • 在每个Node上都运行一个性能监控程序,采集该Node的运行性能数据, 例如Prometheus Node ExportercollectdNew Relic agent或者Ganglia gmond 等。

DaemonSet的Pod调度策略与RC类似,除了使用系统内置的算法在每个Node上进行调度,也可以在Pod的定义中使用NodeSelector或NodeAffinity来指定满足条件的Node范围进行调度

8.1.2 举例

下面的例子定义了为在每个Node上都启动一个fluentd容器,配置文件 fluentd-ds.yaml的内容如下 (其中挂载了物理机的两个目录"/var/log"和 "/var/lib/docker/containers"):

apiVersion: apps/v1
kind: DaemonSet
    metadata:
    name: fluentd-cloud-logging 
    namespace: kube-system 
    labels:
        k8s-app: fluentd-cloud-logging
spec:
    template:
    metadata:
        namespace: kube-system 
        labels:
            k8s-app: fluentd-cloud-logging
    spec:
    containers:
        - name: fluentd-cloud-logging
        image: gcr.io/google containers/fluentd-elasticsearch:1.17 
        resources:
            limits:
                cpu: 100m
                memory: 200Mi
        env:
        - name: FLUENTD ARGS 
          value: -q
        volumeMounts
        - name: varlog
        mountPath: /var/log
        readOnly: false
        - name: containers
        mountPath: /var/lib/docker/containers 
        readonly: false
    volumes:
    - name: containers
    hostPath:
        path: /var/lib/docker/containers 
    - name: varlog
    hostPath:
        path: /var/log

使用kubectl create命令创建该DaemonSet:

kubectl create -f fluentd-ds.yaml 
daemonset "fluentd-cloud-logging"created

查看创建好的DaemonSetPod,可以看到在每个Node上都创建了一个
Pod
在这里插入图片描述

8.1.3 注意事项

DaemonSet调度不同于普通的Pod调度,所以没有用默认的`Kubernetes
Scheduler进行调度,而是通过专有的DaemonSet Controller进行调度。但是随着Kubernetes`版本的改进和调度特性不断丰富,产生了一些难以解决的矛盾,最主要的两个矛盾如下:

  • 普通的Pod是在Pending状态触发调度并被实例化的,DaemonSet Controller并不是在这个状态调度Pod的,这种不一致容易误导和迷惑用户
  • **Pod优先级调度是被Kubernetes Scheduler执行的,而DaemonSet

Controller并没有考虑到Pod优先级调度的问题,也产生了不一致的结果**。

Kubernetes 1.8开始,DaemonSet的调度默认切换到Kubernetes Scheduler进行,从而一劳永逸地解决了以上问题及未来可能的新问题,因为默认切换到了Kubernetes Scheduler统一调度Pod,因此DaemonSet也能正确处理TaintsTolerations的问题。

09 批处理调度

Kubernetes从1.2版本开始支持批处理类型的应用,我们可以通过`Kubernetes
Job`资源对象来定义并启动一个批处理任务。

9.1 批处理调度

批处理任务通常并行(或者串行) 启动多个计算进程去处理一批工作项(Work item),处理完成后,整个批处理任务结束。

9.1.1 任务模式分类

9.1.1.1 按实现方式分类

按照批处理任务实现方式的不同,批处理任务可以分为如图所示的几种模式:
在这里插入图片描述
模式分类:

模式名称 描述
Job Template Expansion 一个Job对象对应一个待处理的Work item,有几个Work item就产生几个独立的Job,通常适合Work item数量少、每 个Work item要处理的数据量比较大的场景,比如有一个100GB的文件作为一个 Work item,总共有10个文件需要处理。
Queue with Pod Per Work Item 采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,在这种模式下,Job会启动NPod,每个Pod都对应一个Work item
Queue with Variable Pod Count 也是采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,但与上面的模式不同,Job启动的Pod数量是可变的
Single Job with Static Work Assignment 也是一个Job产生多个Pod,但它采用程序静态方式分配任务项,而不是采用队列模式进行动态分配

模式对比:

模式名称 是否是一个job pod的数量少于work item 用户程序是否要做相应的修改 kubernetes是否支持
Job Template Expansion / /
Queue with Pod Per Work Item / 有时候需要
Queue with Variable Pod Count / /
Single Job with Static Work Assignment / /

9.1.1.2 按批处理并行分类

考虑到批处理的并行问题,KubernetesJob分以下三种类型:

类型 描述
Non-parallel Jobs 通常一个Job只启动一个Pod,除非Pod异常,才会重启该Pod,一旦此Pod正常结束,Job将结束
Parallel Jobs with a fixed completion count 并行Job会启动多个Pod,此时需要设定Job的.spec.completions参数为一个正数,当正常结束的Pod 数量达至此参数设定的值后,Job结束。此外,Job的.spec.parallelism参数用来控制并行度,即同时启动几个Job来处理Work item
Parallel Jobs with a work queue 任务队列方式的并行Job需要一个独 立的Queue,Work item都在一个Queue中存放,不能设置Job 的.spec.completions参数,此时Job有以下特性:① 每个Pod都能独立判断和决定是否还有任务项需要处理,如果某个Pod正常结束,则Job不会再启动新的Pod 。 ②如果一个Pod成功结束,则此时应该不存在其他Pod还在工作的情况,它们应该都处于即将结束、退出的状态。③如果所有Pod都结束了,且至少有一个Pod成功结束,则整个Job成功结束

9.1.2 案例

9.1.2.1 Job Template Expansion案例

首先是Job Template Expansion模式,由于在这种模式下每个Work item都对应一个Job实例,所以这种模式首先定义一个Job模板,模板里的主要参数是Work item的标识,因为每个Job都处理不同的Work item

如下所示的Job模板(文件名为job.yaml.txt)中的 $ITEM 可以作为任务项的标识:

apiVersion: batch/v1
kind: Job
metadata:
    name: process-item-$ITEM 
    labels:
        jobgroup: jobexample
spec:
    template:
        metadata:
            name: jobexample
            labels:
                jobgroup: jobexample
        spec:
            containers:
            - name: c
              image: busybox
              command: ["sh","-c","echo Processing item $ITEM &sleep 5"] 
            restartPolicy: Never

通过下面的操作,生成了3个对应的Job定义文件并创建Job

> for i in apple banana cherry 
> do
>  cat job.yaml.txt | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml 
> done 
# ls jobs
job-apple.yaml job-banana.yaml job-cherry.yaml 
# kubectl create -f jobs
job "process-item-apple"created 
job "process-item-banana"created 
job "process-item-cherry"created

观察Job的运行情况:

$ kubect1 get jobs -l jobgroup=jobexample

NAME                DESIRED        SUCCESSFUL        AGE
process-item-apple        1            1            4m
process-item-banana        1            1            4m
process-item-cherry        1            1            4m

9.1.2.2 Queue with Pod Per Work Item案例

在这种模式下需要一个任务队列存放Work item,比如RabbitMQ客户端程序先把要处理的任务变成Work item放入任务队列,然后编写Worker程序、打包镜像并定义成为Job中的Work Pod

Worker程序的实现逻辑是从任务队列中拉取一个Work item并处理, 在处理完成后结束进程。并行度为2的Demo如下图所示:
在这里插入图片描述

9.1.2.3 Queue with Variable Pod Count案例

由于这种模式下,Worker程序需要知道队列中是否还有等待处理的Work item,如果有就取出来处理,否则就认为所有工作完成并结束进程,所以任务队列通常要采用Redis或者数据库来实现:
在这里插入图片描述

10 定时任务

Kubernetes从1.5版本开始增加了一种新类型的Job,即类似Linux Cron的定时任务Cron Job,下面看看如何定义和使用这种类型的Job

10.1 基本语法

首先,确保 Kubernetes的版本为 1.8及以上。

Cron Job的定时表达式基本上照搬了Linux Cron的表达式,格式如下:

Minutes Hours DayofMonth Month DayofWeek

其中每个域都可出现的字符如下。

描述
Minutes 可出现“,” “-” “*” “/” 这4个字符,有效范围为0~59的整数
Hours 可出现“,” “-” “%” “/” 这4个字符,有效范围为0~23的整数
DayofMonth 可出现“,” “- “*” “/“ “?” “L” “W“ “C”这8个字符,有效范围 为1~31的整数
Month 可出现“,” “-” “*” “/” 这4个字符,有效范围为1~12的整数或JAN~DEC
DayofWeek 可出现“,” “*” “/” “?” “L” “C” “#” 这8个字符,有效范围为1~7的整数或SUN~SAT1表示星期天,2表示星期一,以此类推

表达式中的特殊字符“*”“/”的含义如下:

  • *表示匹配该域的任意值,假如在Minutes域使用“*”,则表示每分钟都会触发事件
  • /表示从起始时间开始触发,然后每隔固定时间触发一次,例如在

Minutes域设置为5/20,则意味着第1次触发在第5min时,接下来每20min触发一 次,将在第25min、第45min等时刻分别触发。

10.2 案例

比如,我们要每隔1min执行一次任务,则Cron表达式如下:

 */1 * * * *

编写一个Cron Job的配置文件(cron.yaml):

apiversion: batch/vl beta 
kind: CronJob
metadata:
    name: hello
spec:
    schedule: "*/1 * * * *"
    jobTemplate:
        spec:
            template:
                spec:
                    containers:
                    - name:hello
                      image:busybox
                      args:
                      - /bin/sh
                      - -C
                      - date;echo Hello from the Kubernetes cluster restartPolicy:OnFailure

该例子定义了一个名为helloCron Job,任务每隔1min执行一次,运行的镜像是busybox,运行的命令是Shell脚本,脚本运行时会在控制台输出当前时间和字符串"Hello from the Kubernetes cluster".

接下来运行kubectl create命令完成创建:

$ kubectl create -f cron.yaml 

cronjob "hello"created

然后每隔1min运行kubectl get cronjob hello查看任务状态,发现的确每分钟调度了一次:
在这里插入图片描述
还可以通过查找Cron Job对应的容器,验证每隔1min产生一个容器的事实:

在这里插入图片描述
查看任意一个容器的日志,结果如下:
在这里插入图片描述
运行下面的命令,可以更直观地了解Cron Job定期触发任务执行的历史和现状:
在这里插入图片描述


Kubernetes1.9版本后,kubectl命令增加了别名cj来表示cronjob,同时 kubectl set image/env命令也可以作用在CronJob对象上。

11 容灾调度

我们可以将Pod各种常规调度策略认为是将整个集群视为一个整体,然后进行 “打散或聚合” 的调度。

当我们的集群是为了容灾而建设的跨区域的多中心(多个Zone)集群,即集群中的节点位于不同区域的机房时,比如:

北京、上海、广 州、武汉, 要求每个中心的应用相互容灾备份,又能同时提供服务,此时最好的调度策略就是将需要容灾的应用均匀调度到各个中心,当某个中心出现问题时, 又自动调度到其他中心均匀分布,

Pod的多中心均匀分布调度效果图如下所示(不管每个中心的Nod节点数量如何):
在这里插入图片描述

11.1 如何实现?

用普通的基于Node标签选择的调度方式也可以实现上述效果,比如为每个
Zone都建立一个Deployment,Pod的副本总数除以Zone的数量就是每个分区的
Pod副本数量。但这样做有个问题:如果某个Zone失效,那么这个Zone的Pod就无法迁移到其他Zone

另外,topology.kubernetes.io/zone就是Kubernetes默认支持的重要拓扑域之
一,那是否可以用Pod的亲和性调度来解决这个问题呢?不能因为Pod的亲和性 调度用于解决相关联的Pod的调度问题,不能保证被依赖的Pod被均匀调度到多个Zone

为了满足这种容灾场景下的特殊调度需求,在Kubernetes1.16版本中首次引入Even Pod Spreading特性用于通过topologyKey属性识别Zone,并通过设置新 的参数topologySpreadConstraints来将Pod均匀调度到不同的Zone

11.2 举例

举个例子, 假如我们的集群被划分为多个Zone,我们有一个应用(对应的Pod标签为 app=foo)需要在每个Zone均匀调度以实现容灾,则可以定义YAML文件如下:

spec: 
topologySpreadConstraints:  
- maxSkew: 1
  whenUnsatisfiable: DoNotSchedule
  topologyKey: topology.kubernetes.io/zone
  selector:
      matchLabels:
          app: foo

在以上YAML定义中,关键的参数是maxSkew,用于指定Pod在各个Zone上调度时能容忍的最大不均衡数

  • 值越大,表示能接受的不均衡调度越大;
  • 值越小,表示各个Zone的Pod数量分布越均匀。

为了理解maxSkew,我们需要先理解skew参数的计算公式:

 skew[topo]=count[topo]-min(count[topo])

即每个拓扑区域的skew值都为该区域包括的目标Pod数量与整个拓扑区域最少Pod数量的差,而naxSkew就是最大的skew值

假如在上面的例子中有3个拓扑区域,分别为Zone A、Zone B及Zone C,有3个目标Pod需要调度到这些拓扑区域,那么前两个毫无疑问会被调度到Zone A和Zone B,Even Pod Spreading调度效果如图所示:
在这里插入图片描述
那么,第3个Pod会被调度到哪里呢?我们可以手动计算每个Zone的skew

  • 首先计算出min(count[topo])是0,对应Zone C;
  • 于是Zone Askew=1-0=1Zone Bskew=1-0=0Zone Cskew=0-0=0,于是第3个Pod应该被放在Zone C,此时min(count[topo])的值就变成了1,而实际的maxSkew的值为0,符合预期设置;
  • 如果我们把maxSkew设置为2,则在这种情况下,第3个Pod被放在

Zone AZone B都是符合要求的。

有了新的Even Pod Spreading调度特性的加持,再加上之前就已成熟的Pod亲和性调度,Kubernetes就可以完美实现特定应用的容灾部署目标了。

具体做法也很简单:将一个应用中需要部署在一起的几个Pod用亲和性调度声明捆绑,然后选 择其中一个Pod,加持Even Pod Spreading调度规则即可。最终的部署效果图如下:
在这里插入图片描述

相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
目录
相关文章
|
存储 Kubernetes Cloud Native
云原生|kubernetes |一文带你搞懂pod调度策略,驱逐策略,污点、容忍调度(一)
云原生|kubernetes |一文带你搞懂pod调度策略,驱逐策略,污点、容忍调度
565 0
|
3天前
|
Kubernetes 应用服务中间件 调度
k8s-高级调度-污点容忍、亲和性调度
k8s-高级调度-污点容忍、亲和性调度
|
5天前
|
Kubernetes 应用服务中间件 调度
kubernetes最小调度单元pod详解(一)
kubernetes最小调度单元pod详解(一)
|
5天前
|
Kubernetes 网络协议 调度
kubernetes最小调度单元pod详解(二)
kubernetes最小调度单元pod详解(二)
|
5天前
|
监控 Kubernetes Java
查看pod资源使用情况
使用`kubectl top pod -n $namespace|grep online`检查Kubernetes pod资源使用,例如CPU和内存。同样,通过`kubectl top nodes`查看节点资源。关注NMC的堆内存使用,防止溢出。监控实例请求和线程状态以确保正常运行。当遇到问题时,收集日志如jstack、jmap输出、操作系统日志和NMC线程监控数据。对于效率问题,依赖NMC线程监控和客户端SPR记录。错误日志可通过控制台获取。
|
5天前
|
Kubernetes 固态存储 调度
Kubernetes节点亲和性分配Pod
Kubernetes节点亲和性分配Pod
36 0
Kubernetes节点亲和性分配Pod
|
10月前
|
Kubernetes 算法 调度
13-Kubernetes-Pod详解-调度
13-Kubernetes-Pod详解-调度
|
5月前
|
Kubernetes 安全 调度
k8s教程(pod篇)-亲和性与互斥性调度
k8s教程(pod篇)-亲和性与互斥性调度
127 0
|
9月前
|
Kubernetes 算法 调度
k8s--pod 调度、定向调度、亲和性调度、污点和容忍 (一)
k8s--pod 调度、定向调度、亲和性调度、污点和容忍
|
9月前
|
Kubernetes 调度 Perl
k8s--pod 调度、定向调度、亲和性调度、污点和容忍 (二)
k8s--pod 调度、定向调度、亲和性调度、污点和容忍