k8s中的调度和驱逐

k8s中的调度和驱逐

k8s官方文档调度和驱逐

调度是指将 Pod 放置到合适的 Node 上,然后对应 Node 上的 Kubelet 才能够运行这些 pod。

听起来非常简单,但是有非常多需要考虑的问题:

  • 公平:如果保证每个节点都能被分配资源。
  • 资源高效利用:集群所有资源最大化被利用。
  • 效率:调度的性能要好,能够尽快完成对大批量的Pod的调度工作
  • 灵活:允许用户根据自己的需求控制调度的逻辑。

驱逐是指当Node节点发生变化,不再适合一些Pod的运行,会将Node上运行的Pod进行驱逐。


1. 概览

在k8s中有几大核心组件,Scheduler调度器是其中的一个。调度器通过 kubernetes 的 watch 机制来发现集群中新创建且尚未被调度到 Node 上的 Pod。调度器会将发现的每一个未调度的 Pod 调度到一个合适的 Node 上来运行。

对每一个新创建的 Pod 或者是未被调度的 Pod,kube-scheduler 会选择一个最优的 Node 去运行这个 Pod。然而,Pod 内的每一个容器对资源都有不同的需求,而且 Pod 本身也有不同的资源需求。因此,Pod 在被调度到 Node 上之前,根据这些特定的资源调度需求,需要对集群中的 Node 进行一次过滤。

在一个集群中,满足一个 Pod 调度请求的所有 Node 称之为可调度节点。如果没有任何一个 Node 能满足 Pod 的资源请求,那么这个 Pod 将一直停留在未调度状态(Pending)直到调度器能够找到合适的 Node。

调度器先在集群中找到一个 Pod 的所有可调度节点,然后根据一系列函数对这些可调度节点打分,然后选出其中得分最高的 Node 来运行 Pod。之后,调度器将这个调度决定通知给 kube-apiserver,这个过程叫做 绑定。

在做调度决定时需要考虑的因素包括:单独和整体的资源请求、硬件/软件/策略限制、亲和以及反亲和要求、数据局域性、负载间的干扰等等。


2. 调度流程

kube-scheduler 给一个 pod 做调度选择包含两个步骤:

  • 过滤(断言) predicate
  • 打分 (优先级)priority

过滤阶段会将所有满足 Pod 调度需求的 Node 选出来。例如,PodFitsResources 过滤函数会检查候选 Node 的可用资源能否满足 Pod 的资源请求。在过滤之后,得出一个 Node 列表,里面包含了所有可调度节点;通常情况下,这个 Node 列表包含不止一个 Node。如果这个列表是空的,代表这个 Pod 不可调度。

在打分阶段,调度器会为 Pod 从所有可调度节点中选取一个最合适的 Node。根据当前启用的打分规则,调度器会给每一个可调度节点进行打分。

最后,kube-scheduler 会将 Pod 调度到得分最高的 Node 上。如果存在多个得分最高的 Node,kube-scheduler 会从中随机选取一个。
实现过滤和打分的手段主要有固定节点调度和节点标签调度亲和和反亲和污点和容忍度。如果容器对资源进行了请求(例如CPU、内存),那么节点当前可用内存和CPU也是调度需要考虑的。


3. 固定节点调度和节点标签调度

3.1 固定节点调度 NodeName

nodeName 是节点选择约束的最简单方法,但是由于其自身限制,通常不使用它。nodeName 是 PodSpec 的一个字段。如果它不为空,调度器将忽略 pod,并且运行在它指定节点上的 kubelet 进程尝试运行该 pod。因此,如果 nodeName 在 PodSpec 中指定了,则它的优先级最高。

使用 nodeName 的方式选择节点存在一些限制:

  • 如果指定的节点不存在,pod将会一直处于Pending状态。
  • 如果指定的节点没有资源来容纳 pod,pod 将会调度失败并且其原因将显示为,比如 OutOfmemory 或 OutOfcpu。
  • 云环境中的节点名称并非总是可预测或稳定的。

下面使用NodeName进行案例演示

#定义yaml文件
[root@k8s-master01 scheduler]# cat > hostname.yaml << EOF
apiVersion: v1
kind: Pod
metadata:
  name: test-nodename-pod
spec:
  # 直接调度在k8s-node02上
  nodeName: k8s-node02
  containers:
  - name: nginx
    image: wangyanglinux/myapp:v1
    ports:
    - containerPort: 80
      name: http
EOF
#创建Pod
[root@k8s-master01 scheduler]# kubectl create -f hostname.yaml
[root@k8s-master01 scheduler]# pod/test-nodename-pod created
#查看Pod被调度到的节点
[root@k8s-master01 scheduler]# kubectl get pod -o wide
NAME                                                READY   STATUS             RESTARTS   AGE     IP            NODE         
test-nodename-pod                                   1/1     Running            0          22s     10.244.2.82   k8s-node02   <none>           <none>

可以发现Pod被调度到了k8s-node02上。


3.2 节点标签调度 NodeSelector

nodeSelector 是 PodSpec 的一个字段。 它包含键值对的映射。为了使 pod 可以在某个节点上运行,该节点的标签中必须包含这里的每个键值对。最常见的用法的是一对键值对。

下面进行案例演示:

  1. 添加标签到节点
# 查看当前的node节点的标签
[root@k8s-master01 scheduler]# kubectl get node --show-labels
NAME           STATUS   ROLES    AGE   VERSION   LABELS
k8s-master01   Ready    master   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master01,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-node01     Ready    <none>   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node01,kubernetes.io/os=linux
k8s-node02     Ready    <none>   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node02,kubernetes.io/os=linux

选择一个你要增加标签的节点,然后执行 kubectl label nodes <node-name> <label-key>=<label-value> 命令将标签添加到你所选择的节点上。

#例如对k8s-node02打上 gpu=true 标签
[root@k8s-master01 scheduler]# kubectl label node k8s-node02 gpu=true
node/k8s-node02 labeled
#重新查看,发现k8s-node02已经打上了gpu=true的标签
[root@k8s-master01 scheduler]# kubectl get node --show-labels 
NAME           STATUS   ROLES    AGE   VERSION   LABELS
k8s-master01   Ready    master   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master01,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-node01     Ready    <none>   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node01,kubernetes.io/os=linux
k8s-node02     Ready    <none>   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,gpu=true,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node02,kubernetes.io/os=linux

如果想要对已有的标签进行删除或者是修改呢?

修改对应的标签 kubectl label node <node-name> <label-key>=<label-value> --overwrite

[root@k8s-master01 scheduler]# kubectl label node k8s-node02 gpu=false --overwrite 
node/k8s-node02 labeled
[root@k8s-master01 scheduler]# kubectl get nodes k8s-node02 --show-labels
NAME         STATUS   ROLES    AGE   VERSION   LABELS
k8s-node02   Ready    <none>   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,gpu=false,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node02,kubernetes.io/os=linux

删除对应的标签 kubectl label node <node-name> <label-key>- 注意最后面有一个 “-” 减号

[root@k8s-master01 scheduler]# kubectl label node k8s-node02 gpu-
node/k8s-node02 labeled
[root@k8s-master01 scheduler]# kubectl get nodes k8s-node02 --show-labels
NAME         STATUS   ROLES    AGE   VERSION   LABELS
k8s-node02   Ready    <none>   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node02,kubernetes.io/os=linux
  1. 添加 nodeSelector 字段到 Pod 配置中
    由于之前删除了标签,重新对k8s-node02打上标签 kubectl label node k8s-node02 gpu=false
    先创建一个yaml文件
[root@k8s-master01 scheduler]# cat > nodeSelector.yaml << EOF 
apiVersion: v1
kind: Pod
metadata:
  name: test-node-selector
spec:
  containers:
  - name: test-node-selector
    image: wangyanglinux/myapp:v1
    ports:
    imagePullPolicy: IfNotPresent
  nodeSelector:
    gpu: "false"
EOF

这里需要注意 false 一定要加上“”双引号,不然会报错。

根据yaml文件创建Pod kubectl create -f nodeSelector.yaml

查看Pod被调度到的节点

[root@k8s-master01 scheduler]# kubectl get pod test-node-selector -o wide
NAME                 READY   STATUS    RESTARTS   AGE     IP            NODE         NOMINATED NODE   READINESS GATES
test-node-selector   1/1     Running   0          2m32s   10.244.2.84   k8s-node02   <none>           <none>
[root@k8s-master01 scheduler]# kubectl get node k8s-node02 --show-labels 
NAME         STATUS   ROLES    AGE   VERSION   LABELS
k8s-node02   Ready    <none>   47h   v1.16.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,gpu=false,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node02,kubernetes.io/os=linux

这里需要注意的是,如果没有匹配标签的Node节点,Pod的状态会一直处于Pending状态。


4. 亲和和反亲和

NodeSelector 提供了一种非常简单的方法来将 pod 约束到具有特定标签的节点上,但是其功能还不够强大和灵活,亲和/反亲和功能极大地扩展了可以表达约束的类型。

关键的增强点是:

  • 语言更具表现力(不仅仅是“完全匹配的 AND”)。nodeSelector要求节点的标签必须同时满足一些key=value值,不够灵活。
  • 可以制定规则是“软”/“偏好”,而不是硬性要求,因此,如果调度器无法满足该要求,仍然调度该 pod。
  • 可以使用节点上(或其他拓扑域中)的 pod 的标签来约束,而不是使用节点本身的标签,来允许哪些 pod 可以或者不可以被放置在一起。

亲和功能包含两种类型的亲和,即“节点亲和”“pod 间亲和/反亲和”


4.1 节点亲和

节点亲和概念上类似于 nodeSelector,它使你可以根据节点上的标签来约束 pod 可以调度到哪些节点。节点亲和的功能更加强大,具有上述增强点中前两个优势。
我们用explain查看一下nodeAffinity

[root@k8s-master01 schedule]# kubectl explain pod.spec.affinity.nodeAffinity
KIND:     Pod
VERSION:  v1

RESOURCE: nodeAffinity <Object>

DESCRIPTION:
     Describes node affinity scheduling rules for the pod.

     Node affinity is a group of node affinity scheduling rules.

FIELDS:
   preferredDuringSchedulingIgnoredDuringExecution	<[]Object>
     The scheduler will prefer to schedule pods to nodes that satisfy the
     affinity expressions specified by this field, but it may choose a node that
     violates one or more of the expressions. The node that is most preferred is
     the one with the greatest sum of weights, i.e. for each node that meets all
     of the scheduling requirements (resource request, requiredDuringScheduling
     affinity expressions, etc.), compute a sum by iterating through the
     elements of this field and adding "weight" to the sum if the node matches
     the corresponding matchExpressions; the node(s) with the highest sum are
     the most preferred.

   requiredDuringSchedulingIgnoredDuringExecution	<Object>
     If the affinity requirements specified by this field are not met at
     scheduling time, the pod will not be scheduled onto the node. If the
     affinity requirements specified by this field cease to be met at some point
     during pod execution (e.g. due to an update), the system may or may not try
     to eventually evict the pod from its node.

目前有两种类型的节点亲和,分别为 requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution。你可以视它们为“硬”和“软”,意思是,前者指定了将 pod 调度到一个节点上必须满足的规则(就像 nodeSelector 但使用更具表现力的语法),后者指定调度器将尝试执行但不能保证的偏好。

名称的“IgnoredDuringExecution”部分意味着,类似于 nodeSelector 的工作原理,如果节点的标签在运行时发生变更,从而不再满足 pod 上的亲和规则,但是 pod 将仍然继续在该节点上运行。

将来我们计划提供 requiredDuringSchedulingRequiredDuringExecution,它将类似于 requiredDuringSchedulingIgnoredDuringExecution,只是它会将 pod 从不再满足 pod 的节点亲和要求的节点上驱逐。

怎么理解软策略和硬策略?
硬策略实现了predicate(过滤、断言),软策略类实现了priority(打分、优先级)。
requiredDuringSchedulingIgnoredDuringExecution 类似于原来的nodeSelector,只是语法更具有表现力,功能更加强大,节点必须满足一定的规则。preferredDuringSchedulingIgnoredDuringExecution 如果节点满足了一些特定规则,那么该节点具有更高的优先级,通过打分来实现。

节点亲和通过 PodSpec 的 affinity 字段下的 nodeAffinity 字段进行指定。

下面是一个使用节点亲和的 pod 的实例:

[root@k8s-master01 scheduler]# cat > pod-with-node-affinity.yaml << EOF
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      #硬亲和性,类似于nodeSelector,但是功能更加强大和灵活
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/hostname
            operator: NotIn
            values:
            - k8s-node02
      #软亲和,满足特定条件的Node在调度时具有更高的优先级
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: source
            operator: In
            values:
            - qikqiak
  containers:
  - name: with-node-affinity
    image: wangyanglinux/myapp:v1
EOF

创建Pod和查看Pod

[root@k8s-master01 ~]# kubectl create -f pod-with-node-affinity.yaml
pod/with-node-affinity created
[root@k8s-master01 ~]# kubectl get pod  with-node-affinity -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
with-node-affinity   1/1     Running   0          10m   10.244.1.95   k8s-node01   <none>           <none>

可以看到Pod被调度到了k8s-node01节点上。此节点亲和规则表示,pod 只能放置在具有标签键为 kubernetes.io/hostname 且标签值不为k8s-node02的节点上。另外,在满足这些标准的节点中,具有标签键为 source 且标签值为 qikqiak 的节点应该优先使用。

新的节点亲和语法支持下面的操作符: InNotInExistsDoesNotExistGtLt。 你可以使用 NotInDoesNotExist 来实现节点反亲和行为,或者使用节点污点将 pod 从特定节点中驱逐。

如果你同时指定了 nodeSelectornodeAffinity,两者必须都要满足,才能将 pod 调度到候选节点上。

如果你指定了多个与 nodeAffinity 类型关联的 nodeSelectorTerms,则如果其中一个 nodeSelectorTerms 满足的话,pod将可以调度到节点上。

如果你指定了多个与 nodeSelectorTerms 关联的 matchExpressions,则只有当所有 matchExpressions 满足的话,pod 才会可以调度到节点上。

如果你修改或删除了 pod 所调度到的节点的标签,pod 不会被删除。换句话说,节点亲和选择只在 pod 调度期间有效。

preferredDuringSchedulingIgnoredDuringExecution 中的 weight 字段值的范围是 1-100。对于每个符合所有调度要求(资源请求,RequiredDuringScheduling 亲和表达式等)的节点,调度器将遍历该字段的元素来计算总和,并且如果节点匹配对应的MatchExpressions,则添加“权重”到总和。然后将这个评分与该节点的其他优先级函数的评分进行组合。总分最高的节点是最优选的。

键值运算关系

  • In:label的值在某个列表中。
  • NotIn:label的值不在某个列表中。
  • Gt:label的值大于某个值。
  • Lt:label的值小于某个值。
  • Exists:某个label存在。
  • DoesNotExist:某个label不存在。

4.2 pod 间亲和与反亲和

pod 间亲和与反亲和使你可以基于已经在节点上运行的 pod 的标签来约束 pod 是否可以调度到的节点,而不是基于节点上的标签。使用场景:使用pod间亲和,可以将一些pod调度到相同的节点上一起运行,web服务通常和缓存运行在同一个node上;使用pod间反亲和,可以让多个pod的副本运行在不同的node节点中。

规则的格式为“如果 X 节点上已经运行了一个或多个 满足规则 Y 的pod,则这个 pod 应该(或者在非亲和的情况下不应该)运行在 X 节点上”。Y 表示一个具有可选的关联命令空间列表的 LabelSelector;与节点不同,因为 pod 是命名空间限定的(因此 pod 上的标签也是命名空间限定的),因此作用于 pod 标签的标签选择器必须指定选择器应用在哪个命名空间。

从概念上讲,X 是一个拓扑域,如节点,机架,云供应商地区,云供应商区域等。你可以使用 topologyKey 来表示它,topologyKey 是节点标签的键以便系统用来表示这样的拓扑域。可以简单的理解为topologyKey就是节点的标签的key值。节点的标签中必须存在对应的key,value不关心,才可以将pod调度到节点上。

除了对node节点附加的标签的key以外,节点还预先填充了一组标准标签。这些标签是

  • kubernetes.io/hostname
  • failure-domain.beta.kubernetes.io/zone
  • failure-domain.beta.kubernetes.io/region
  • topology.kubernetes.io/zone
  • topology.kubernetes.io/region
  • beta.kubernetes.io/instance-type
  • node.kubernetes.io/instance-type
  • kubernetes.io/os
  • kubernetes.io/arch

kubernetes.io/hostname外,这些标签中的大多数已经废弃,不再推荐使用。

接下来对pod间亲和和反亲和进行案例演示

[root@k8s-master01 scheduler]# cat > pod-with-pod-affinity.yaml << EOF
apiVersion: v1
kind: Pod
metadata:
  name: pod-3
  labels:
    app: pod-3
spec:
  containers:
  - name: pod-3
    image: wangyanglinux/myapp:v1
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - pod-1
        topologyKey: kubernetes.io/hostname
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values:
              - pod-2
          topologyKey: kubernetes.io/hostname
EOF

pod间亲和反亲和用pod.spec.affinity中的podAffinitypodAntiAffinity表示这两个同样存在硬亲和和软亲和。
在这个 pod 的 affinity 配置定义了一条 pod 亲和规则和一条 pod 反亲和规则。在此示例中,podAffinity 配置为 requiredDuringSchedulingIgnoredDuringExecution,然而 podAntiAffinity 配置为 preferredDuringSchedulingIgnoredDuringExecution。pod 亲和规则表示,仅当节点和至少一个已运行且有键为“app”且值为“pod-1”的标签的 pod 处于同一区域时,才可以将该 pod 调度到节点上。(更确切的说,如果节点 N 具有带有键kubernetes.io/hostname,则 pod 有资格在节点 N 上运行,以便集群中至少有一个节点具有键kubernetes.io/hostname正在运行具有键“app”和值“pod-1”的标签的 pod。)

pod 反亲和规则表示,如果节点已经运行了一个具有键“app”和值“pod-2”的标签的 pod,则该 pod 不希望将其调度到该节点上。(如果 topologyKeykubernetes.io/hostname,则意味着当节点和具有键“app”和值“pod-2”的标签的 pod 处于相同的区域,pod 不能被调度到该节点上。)

Pod 亲和与反亲和的合法操作符有 InNotInExistsDoesNotExist

除了 labelSelectortopologyKey,你也可以指定表示命名空间的 namespaces 队列,labelSelector 也应该匹配它(这个与 labelSelectortopologyKey 的定义位于相同的级别)。如果忽略或者为空,则默认为 pod 亲和/反亲和的定义所在的命名空间。

所有与 requiredDuringSchedulingIgnoredDuringExecution 亲和与反亲和关联的 matchExpressions 必须满足,才能将 pod 调度到节点上。

创建pod,并查看pod

[root@k8s-master01 schedule]# kubectl apply -f pod-with-pod-affinity.yaml 
pod/pod-3 created
[root@k8s-master01 schedule]# kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
pod-3   0/1     Pending   0          7s
#查看一下pod的描述信息
[root@k8s-master01 schedule]# kubectl describe pod pod-3 
Name:         pod-3
Namespace:    default
Priority:     0
Node:         <none>
Labels:       app=pod-3
.......
Events:
  Type     Reason            Age        From               Message
  ----     ------            ----       ----               -------
  Warning  FailedScheduling  <unknown>  default-scheduler  0/3 nodes are available: 1 node(s) had taints that the pod didn't tolerate, 2 node(s) didn't match pod affinity rules.

我们可以看到Pod一直处于Pending状态,这是因为没有一个运行的Pod的标签是app=pod-1,因此不能调度到对应的节点上,也就处于Pending状态。

接下来我们监视pod-3的状态。

[root@k8s-master01 ~]# kubectl get pod -o wide -w
NAME    READY   STATUS    RESTARTS   AGE     IP       NODE     NOMINATED NODE   READINESS GATES
pod-3   0/1     Pending   0          3m48s   <none>   <none>   <none>           <none>

同时创建一个pod,并且打上app=pod-1的标签,查看pod-3的调度情况。

[root@k8s-master01 schedule]# kubectl run nginx-myapp --image=wangyanglinux/myapp:v1 -l app=pod-1
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/nginx-myapp created

[root@k8s-master01 ~]# kubectl get pod -o wide -w
NAME    READY   STATUS    RESTARTS   AGE     IP       NODE     NOMINATED NODE   READINESS GATES
pod-3   0/1     Pending   0          3m48s   <none>   <none>   <none>           <none>
nginx-myapp-748fb4d5d5-kj7h7   0/1     Pending   0          0s      <none>   <none>   <none>           <none>
nginx-myapp-748fb4d5d5-kj7h7   0/1     Pending   0          0s      <none>   k8s-node01   <none>           <none>
pod-3                          0/1     Pending   0          5m11s   <none>   k8s-node01   <none>           <none>
nginx-myapp-748fb4d5d5-kj7h7   0/1     ContainerCreating   0          0s      <none>   k8s-node01   <none>           <none>
pod-3                          0/1     ContainerCreating   0          5m11s   <none>   k8s-node01   <none>           <none>
nginx-myapp-748fb4d5d5-kj7h7   1/1     Running             0          1s      10.244.1.98   k8s-node01   <none>           <none>
pod-3                          1/1     Running             0          5m13s   10.244.1.97   k8s-node01   <none>           <none>

我们可以看到当nginx-myapp这个deploy被创建的时候,pod-3这个pod被成功调度,然后运行。


4.3 具体案例演示:redis和web服务器

Pod 间亲和与反亲和在与更高级别的集合(例如 ReplicaSets,StatefulSets,Deployments 等)一起使用时,它们可能更加有用。可以轻松配置一组应位于相同定义拓扑(例如,节点)中的工作负载。

在三节点集群中,一个 web 应用程序具有内存缓存,例如 redis。我们希望 web 服务器尽可能与缓存放置在同一位置,同时在同一个node节点上,应该只有一对web服务器和redis服务器。

下面是一个简单 redis deployment 的 yaml 代码段,它有两个副本和选择器标签 app=store。Deployment 配置了 PodAntiAffinity,用来确保调度器不会将副本调度到单个节点上。

[root@k8s-master01 scheduler]# cat > redis-deployment.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 2
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine
EOF

这里使用了podAntiAffinity,强制要求如果Node已经运行了标签label为app=store的Pod不能再运行该Pod。也就是说,由于在同一个node上不能运行多个该Pod的副本,只能运行一个Pod的副本。这也符合常识,因为redis多个实例最好不要放在同一个node上,可以由podAntiAffinity做到。

创建Pod,并且查看Pod

[root@k8s-master01 schedule]# kubectl apply -f redis-deployment.yaml 
deployment.apps/redis-cache created
[root@k8s-master01 schedule]# kubectl get pod --show-labels -o wide
NAME                           READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES   LABELS
redis-cache-6bc7d5b59d-9n9dk   1/1     Running   0          27s   10.244.1.102   k8s-node01   <none>           <none>            app=store,pod-template-hash=6bc7d5b59d
redis-cache-6bc7d5b59d-s7rtm   1/1     Running   0          27s   10.244.2.119   k8s-node02   <none>           <none>            app=store,pod-template-hash=6bc7d5b59d

我们可以看到redis的两个Pod并没部署到同一个Node上,这是由podAntiAffinity做到的。

下面nginx deployment 的 yaml 代码段中配置了 podAntiAffinitypodAffinity。这将通知调度器将它的所有副本与具有 app=store 选择器标签的 pod 放置在一起。这还确保每个 web 服务器副本不会调度到单个节点上。

[root@k8s-master01 schedule]# cat > nginx-deployment.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
spec:
  selector:
    matchLabels:
      app: web-store
  replicas: 2
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: wangyanglinux/myapp:v1
EOF

创建web服务器,并且且查看Pod

[root@k8s-master01 schedule]# kubectl apply -f nginx-deployment.yaml 
deployment.apps/web-server created
[root@k8s-master01 schedule]# kubectl get pod -o wide --show-labels 
NAME                           READY   STATUS    RESTARTS   AGE     IP             NODE         NOMINATED NODE   READINESS GATES   LABELS
redis-cache-6bc7d5b59d-9n9dk   1/1     Running   0          3m50s   10.244.1.102   k8s-node01   <none>           <none>            app=store,pod-template-hash=6bc7d5b59d
redis-cache-6bc7d5b59d-s7rtm   1/1     Running   0          3m50s   10.244.2.119   k8s-node02   <none>           <none>            app=store,pod-template-hash=6bc7d5b59d
web-server-78cf75c769-99txk    1/1     Running   0          17s     10.244.1.103   k8s-node01   <none>           <none>            app=web-store,pod-template-hash=78cf75c769
web-server-78cf75c769-hsgkj    1/1     Running   0          17s     10.244.2.120   k8s-node02   <none>           <none>            app=web-store,pod-template-hash=78cf75c769

web 服务器副本不会调度到单个节点上,同时web服务器和缓存服务器被调度到同一个node节点上

我们创建了上面的两个 deployment,我们的二节点集群将如下表所示。

k8s-node01k8s-node02
nginx-1nginx-2
cache-1cache-2

查看一下Pod的调度情况

[root@k8s-master01 schedule]# kubectl get pod -o wide --show-labels 
NAME                           READY   STATUS    RESTARTS   AGE     IP             NODE         NOMINATED NODE   READINESS GATES   LABELS
redis-cache-6bc7d5b59d-9n9dk   1/1     Running   0          3m50s   10.244.1.102   k8s-node01   <none>           <none>            app=store,pod-template-hash=6bc7d5b59d
redis-cache-6bc7d5b59d-s7rtm   1/1     Running   0          3m50s   10.244.2.119   k8s-node02   <none>           <none>            app=store,pod-template-hash=6bc7d5b59d
web-server-78cf75c769-99txk    1/1     Running   0          17s     10.244.1.103   k8s-node01   <none>           <none>            app=web-store,pod-template-hash=78cf75c769
web-server-78cf75c769-hsgkj    1/1     Running   0          17s     10.244.2.120   k8s-node02   <none>           <none>            app=web-store,pod-template-hash=78cf75c769

参阅 ZooKeeper 教程, 以获取配置反亲和来达到高可用性的 StatefulSet 的样例(使用了相同的技巧)。


5. 污点和容忍度

k8s官方文档污点和容忍度
节点亲和性是 Pod 的一种属性,它使 Pod 被吸引到一类特定的节点。 这可能出于一种偏好,也可能是硬性要求。 Taint(污点)则相反,它使节点能够排斥一类特定的 Pod。

容忍度(Tolerations)是应用于 Pod 上的,允许(但并不要求)Pod 调度到带有与之匹配的污点的节点上。

污点和容忍度(Toleration)相互配合,可以用来避免 Pod 被分配到不合适的节点上。 每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的 Pod,是不会被该节点接受的。

污点和容忍度可以认为是过滤的实现,当Pod不能容忍节点上的污点时,节点将被过滤。


5.1 污点(Taint)的组成

污点(Taint)是相对于节点来说,使用kubectl taint命令可以给某个Node节点设置污点,Node被设置上污点之后就和Pod之间存在一种互斥的关系,可以让Node拒绝Pod的调度执行,甚至将已经存在的Pod的驱逐出去。

每个污点的组成:

key=value:effect

每个污点都有对应的key和value,其中value可以为空。effect用于描述污点的作用。当前taint的effect支持如下三个选项:

  • NoSchedule:表示k8s将不会将Pod调度到具有该污点的Node上。
  • PreferNoSchedule:表示k8s将尽量避免将Pod调度到具有该污点的节点上。
  • NoExecute:表示k8s将不会将Pod调度到具有该污点的节点上,同时会将Node上已存在且不能容忍该污点的Pod驱逐出去。

一个Node可以有多个污点,一个Pod也可以设置多个容忍度和容忍时间,那么k8s将如何进行调度呢?后面会详细说明。


5.2 污点的设置、查看和去除

设置污点

kubectl taint node node1 key1=value1:NoSchedule

上面给node1这个节点设置一个key为key1,value为value1的污点,同时效果为NoSchedule,表示当Pod容忍这个污点时,才可以将Pod调度node1上,否则不能进行调度。

查看污点

kubectl describe node node1

去除污点

kubectl taint nodes node1 key:NoSchedule-

案例演示

我们先将k8s-node01打上一个污点

kubectl taint node k8s-node01 gpu=false:NoSchedule

该命令表示k8s-node01这个节点没有gpu,不要将包含gpu计算任务的Pod调度到该节点。
我们创建一个名为nginx的Pod,然后查看一下这个Pod被调度到了哪个节点上。

[root@k8s-master01 schedule]# kubectl run nginx --image=wangyanglinux/myapp:v1 --image-pull-policy=IfNotPresent
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/nginx created
[root@k8s-master01 schedule]# 
[root@k8s-master01 schedule]# kubectl get pod nginx-5445474cc8-8rxqg -o wide
NAME                     READY   STATUS              RESTARTS   AGE   IP       NODE         NOMINATED NODE   READINESS GATES
nginx-5445474cc8-8rxqg   0/1     ContainerCreating   0          57s   <none>   k8s-node02   <none>           <none>

我们发现Pod被调度到了k8s-node02这个节点上,这并不是因为随机调度到了k8s-node02节点,而是因为k8s-node01上存在着 gpu=false:NoSchedule 这个污点,Pod没有容忍这个污点,且调度策略为NoSchedule,即不能调度,因此Pod不会被调度到k8s-node01上。


5.3 容忍度(Toleration)

设置了污点的Node将根据taint的effect:NoSchedulePreNoScheduleNoExecute和Pod之间产生互斥的关系,Pod在一定程度上不会调度到Node上。但是我们可以在Pod中设置容忍(Toleration),意思是Pod可以容忍该污点的存在,能够被调度到有污点的Node上。

Toleration在pod.spec中进行定义,例如:

[root@k8s-master01 schedule]# cat > pod-toleration.yaml << EOF
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: wangyanglinux/myapp:v1
    imagePullPolicy: IfNotPresent
  tolerations:
  - key: "gpu"
    operator: "Exists"
    effect: "NoSchedule"
EOF

默认的operatorEqual。当然了和污点一样,容忍度可以有多个。
一个容忍度和一个污点相“匹配”是指它们有一样的键名和效果。并且对于value只而言,需要满足如下规则:

  • 如果 operatorExists (此时容忍度不能指定 value)。
  • 如果 operatorEqual ,则它们的 value 应该相等。

说明, 存在两种特殊情况:

  • 如果一个容忍度的 key 为空且 operator 为 Exists, 表示这个容忍度与任意的 key 、value 和 effect 都匹配,即这个容忍度能容忍任意taint。
  • 如果 effect 为空,则可以与所有键名 key 的效果相匹配。

我们先创建Pod,然后查看nginx这个Pod的调度情况

[root@k8s-master01 schedule]# kubectl create -f pod-toleration.yaml 
pod/nginx created
[root@k8s-master01 schedule]# kubectl get pod -o wide
NAME    READY   STATUS              RESTARTS   AGE   IP       NODE         NOMINATED NODE   READINESS GATES
nginx   0/1     ContainerCreating   0          1s    <none>   k8s-node01   <none>           <none>

我们可以看到Pod被调度到了k8s-node01节点上(也可能被调度到k8s-node02上),这是由于在Pod的spec中定义了toleration,容忍度和污点相匹配。

前面讲述了,可以节点Node设置多个污点,那么当Pod也设置了多个容忍度,那么k8s的Scheduler该如何处理这种情况呢?

事实上Kubernetes 处理多个污点和容忍度的过程就像一个过滤器:从一个节点的所有污点开始遍历, 过滤掉那些 Pod 中存在与之相匹配的容忍度的污点。余下未被过滤的污点的 effect 值决定了 Pod 是否会被分配到该节点。

简而言之,存在着两个for循环,外循环是节点的污点,内循环是Pod声明的容忍度。从节点的污点开始遍历,依次遍历Pod声明的容忍度,查看容忍度是否和污点匹配。如果内循环结束仍然无法匹配,则表明Pod无法容忍该污点,根据污点的effect值,调度Pod。

特别是以下情况:

  • 如果未被过滤的污点中存在至少一个 effect 值为 NoSchedule 的污点, 则 Kubernetes 不会将 Pod 分配到该节点。
  • 如果未被过滤的污点中不存在 effect 值为 NoSchedule 的污点, 但是存在 effect 值为 PreferNoSchedule 的污点, 则 Kubernetes 会 尝试 将 Pod 分配到该节点。
  • 如果未被过滤的污点中存在至少一个 effect 值为 NoExecute 的污点, 则 Kubernetes 不会将 Pod 分配到该节点(如果 Pod 还未在节点上运行), 或者将 Pod 从该节点驱逐(如果 Pod 已经在节点上运行)。

案例演示
例如,假设给k8s-node01节点添加了如下污点

kubectl taint nodes k8s-node01 key1=value1:NoSchedule
kubectl taint nodes k8s-node01 key1=value1:NoExecute
kubectl taint nodes k8s-node01 key2=value2:NoSchedule

假定有一个 Pod,它有两个容忍度:

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

在这种情况下,上述 Pod 不会被分配到上述节点,因为其没有容忍度和第三个污点相匹配。 但是如果在给节点添加上述污点之前,该 Pod 已经在上述节点运行, 那么它还可以继续运行在该节点上,因为第三个污点是三个污点中唯一不能被这个 Pod 容忍的。

通常情况下,如果给一个节点添加了一个 effect 值为 NoExecute 的污点, 则任何不能忍受这个污点的 Pod 都会马上被驱逐, 任何可以忍受这个污点的 Pod 都不会被驱逐。 但是,如果 Pod 存在一个 effect 值为 NoExecute 的容忍度指定了可选属性 tolerationSeconds 的值,则表示在给节点添加了上述污点之后, Pod 还能继续在节点上运行的时间。例如,

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

这表示如果这个 Pod 正在运行,同时一个匹配的污点被添加到其所在的节点, 那么 Pod 还将继续在节点上运行 3600 秒,然后被驱逐。 如果在此之前上述污点被删除了,则 Pod 不会被驱逐。


5.4 使用示例

通过污点和容忍度,可以灵活地让 Pod 避开 某些节点或者将 Pod 从某些节点驱逐。下面是几个使用例子:

  • 专用节点:如果您想将某些节点专门分配给特定的一组用户使用,您可以给这些节点添加一个污点(即, kubectl taint nodes nodename dedicated=groupName:NoSchedule), 然后给这组用户的 Pod 添加一个相对应的 toleration(通过编写一个自定义的 准入控制器,很容易就能做到)。 拥有上述容忍度的 Pod 就能够被分配到上述专用节点,同时也能够被分配到集群中的其它节点。 如果您希望这些 Pod 只能被分配到上述专用节点,那么您还需要给这些专用节点另外添加一个和上述 污点类似的 label (例如:dedicated=groupName),同时 还要在上述准入控制器中给 Pod 增加节点亲和性要求上述 Pod 只能被分配到添加了 dedicated=groupName 标签的节点上。

  • 配备了特殊硬件的节点:在部分节点配备了特殊硬件(比如 GPU)的集群中, 我们希望不需要这类硬件的 Pod 不要被分配到这些特殊节点,以便为后继需要这类硬件的 Pod 保留资源。 要达到这个目的,可以先给配备了特殊硬件的节点添加 taint (例如 kubectl taint nodes nodename special=true:NoSchedulekubectl taint nodes nodename special=true:PreferNoSchedule), 然后给使用了这类特殊硬件的 Pod 添加一个相匹配的 toleration。 和专用节点的例子类似,添加这个容忍度的最简单的方法是使用自定义 准入控制器。 比如,我们推荐使用扩展资源 来表示特殊硬件,给配置了特殊硬件的节点添加污点时包含扩展资源名称, 然后运行一个 ExtendedResourceToleration 准入控制器。此时,因为节点已经被设置污点了,没有对应容忍度的 Pod 会被调度到这些节点。但当你创建一个使用了扩展资源的 Pod 时, ExtendedResourceToleration 准入控制器会自动给 Pod 加上正确的容忍度, 这样 Pod 就会被自动调度到这些配置了特殊硬件件的节点上。 这样就能够确保这些配置了特殊硬件的节点专门用于运行需要使用这些硬件的 Pod, 并且您无需手动给这些 Pod 添加容忍度。

  • 基于污点的驱逐: 这是在每个 Pod 中配置的在节点出现问题时的驱逐行为。


6. 基于污点的驱逐

6.1 Pod被驱逐的情况

前文提到过污点的 effect 值 NoExecute会影响已经在节点上运行的 Pod

  • 如果 Pod 不能忍受 effect 值为 NoExecute 的污点,那么 Pod 将马上被驱逐。
  • 如果 Pod 能够忍受 effect 值为 NoExecute 的污点,但是在容忍度定义中没有指定 tolerationSeconds,则 Pod 还会一直在这个节点上运行。
  • 如果 Pod 能够忍受 effect 值为 NoExecute 的污点,而且指定了 tolerationSeconds, 则 Pod 还能在这个节点上继续运行这个指定的时间长度。

6.2 Node节点发生异常状态时的情况

当节点控制器检测到某个节点Node发生异常情况时,节点控制器会自动给节点添加一个污点。当前内置的污点包括:

  • node.kubernetes.io/not-ready:节点未准备好。这相当于节点状态 Ready 的值为 “False”。
  • node.kubernetes.io/unreachable:节点控制器访问不到节点. 这相当于节点状态 Ready 的值为 “Unknown”。
  • node.kubernetes.io/out-of-disk:节点磁盘耗尽。
  • node.kubernetes.io/memory-pressure:节点存在内存压力。
  • node.kubernetes.io/disk-pressure:节点存在磁盘压力。
  • node.kubernetes.io/network-unavailable:节点网络不可用。
  • node.kubernetes.io/unschedulable: 节点不可调度。

说明: 为了保证由于节点问题引起的 Pod 驱逐 速率限制行为正常, 系统实际上会以限定速率的方式添加污点。在像主控节点与工作节点间通信中断等场景下, 这样做可以避免 Pod 被大量驱逐。
使用这个功能特性,结合 tolerationSeconds,Pod 就可以指定当节点出现一个 或全部上述问题时还将在这个节点上运行多长的时间。

比如,一个使用了很多本地状态的应用程序在网络断开时,仍然希望停留在当前节点上运行一段较长的时间, 愿意等待网络恢复以避免被驱逐。在这种情况下,Pod 的容忍度可能是下面这样的:

tolerations:
- key: "node.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

说明:
Kubernetes 会自动给 Pod 添加一个 key 为 node.kubernetes.io/not-ready 的容忍度 并配置 tolerationSeconds=300,除非用户提供的 Pod 配置中已经已存在了 key 为 node.kubernetes.io/not-ready 的容忍度。

同样,Kubernetes 会给 Pod 添加一个 key 为 node.kubernetes.io/unreachable 的容忍度 并配置 tolerationSeconds=300,除非用户提供的 Pod 配置中已经已存在了 key 为 node.kubernetes.io/unreachable 的容忍度。

这种自动添加的容忍度意味着在其中一种问题被检测到时 Pod 默认能够继续停留在当前节点运行 5 分钟。
describe一个Pod,查看默认设置的容忍度

......
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
......

6.3 DaemonSet控制器的情况

DaemonSet 中的 Pod 被创建时, 针对以下污点自动添加的 NoExecute 的容忍度将不会指定 tolerationSeconds

  • node.kubernetes.io/unreachable
  • node.kubernetes.io/not-ready
    这保证了当Node节点出现上述问题时 DaemonSet 控制器中的 Pod 永远不会被驱逐。

7. 总结

在上面的叙述中我们发现,k8s的调度功能非常强大和灵活,基本上可以满足多种调度场景。我们可以从很多方面来配置和影响调度策略。

例如:

  • 固定节点和节点标签。
  • 节点亲和、Pod亲和和反亲和。
  • 污点和容忍度。
  • 资源请求和限制。

总体来看,影响调度总共分为三个方面:

  1. 如何选择节点。
  2. 是否将一些Pod运行在同一个节点,或者是不要将一些运行在同一个节点上。
  3. Pod对于资源的请求和限制。例如cpu、内存,节点剩余的cpu和内存对于调度也会产生影响。

在调度Pod之前,需要对集群进行整体规划和安排。不同类型的工作负载(cpu密集型、io密集型)需要调度到合适的节点上,以及不同的工作负载能否调度到同一个工作负载。我们学习了大量关于k8s的知识,发现label标签对于资源对象非常重要,无论是节点、pod、service等,在对资源对象打上label标签之前,需要进行合理规划和安排,以便能够更好地进行资源调度。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值