Overview

当我们创建一个 Pod 之后,Kubernetes 自身的一些调度规则将会根据集群节点的各项指标,为这个 Pod 选出一个合适的 Node 且将其调度上去。我们姑且可以认为这个调度的过程对我们来说是「随机」的。因为在没有了解清楚 Kubernetes 的调度规则之前,我们也不知道创建的一个 Pod 将会被调度到哪台节点上。 但是在日常开发的过程中,尤其是基于 k8s 开发一些数据库相关应用的时候,我们通常对 Pod 被调度的节点是有要求的,比如:我们需要将主节点强制调度到某台机器上,或者我们需要主从节点所在的 Pod 不能调度到同一个 Node 上。

这些要求在 kubernetes 上是可以实现的,它提供以下几种方式来方便使用者干预 Pod 的调度策略:

  1. NodeSelector
  2. Taints and Tolerations
  3. Anti-Affinity/Affinity

NodeSelector

NodeSelector 是依靠 LabelSelector 实现的一种调度策略。若想使用 NodeSelector, 需要分为两部分来考虑

Node

我们需要在集群中某一个我们想要调度到的 Node 上打上一个label,比如,A Node上的硬盘是 SSD,那我们就可以使用 kubectl 命令将 A Node 打上一个 disktype=ssd 的 Label。 kubectl label nodes <node-name> <label-key>=<label-value>

Pod

对于 Pod 来说,我们需要在它的 spec 段内,加入 nodeSelector 段。并且在 nodeSelector 段中填入「目的 Node」 所携带的 Label。比如我们想将一个 Pod 强制调度到 Node A 上,那么在构造 Pod 的基本信息的时候,就要相应的做如下修改(以 yaml 文件为例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  nodeSelector:
    disktype: ssd

(该 Pod 将会被强制调度到 Node Label 中有 disktype=ssd 的 Node 上) 一个可被调度的 Node 中的 labels 必须包含 Pod 的 nodeSelector 中指定的 labels。

Anti-Affinity/Affinity

Kubernetes 提供了一种基于「亲和性」和「反亲和性」的调度策略。它实现了 NodeSelector(NodeSelector 基本相当于 NodeAffinity)能够提供的所有功能,并且额外做了如下改进:

  1. 不但支持 Node 与 Pod 之间的「关系计算」(NodeSelector),还支持待调度 Pod 和已经运行在某个 Node 上面的 Pod 之间的「关系计算」
  2. 「关系计算」支持逻辑运算(AND, OR…)
  3. 对一个调度策略所需要满足的条件支持两种模式:「硬性」/「软性」。「硬性」意味着如果找不到符合指定条件的 Node,那么 Pod 将会调度失败。「软性」意味着,如果有符合调度条件的 Node 那么将优先采用。否则,将会根据默认的策略进行调度,从而最大限度的保证 Pod 被成功调度运行

NodeAffinity

NodeAffinity 有两种类型:

  1. requiredDuringSchedulingIgnoredDuringExecution
  2. preferredDuringSchedulingIgnoredDuringExecution

两种类型的区别在于前半部分:requiredDuringScheduling or preferredDuringScheduling。 简单来说,requiredDuringScheduling属于「硬性」要求,即如果它指定的条件没有被满足的话,那么相应的 Pod 可能会调度失败。而 preferredDuringScheduling 属于「软性」要求,即如果它指定的条件没有被满足的话,也会按照默认的策略继续调度 Pod。 NodeAffinity 提供的调度策略的功能基本上和 NodeSelector 是一样的,都是通过 Node 和 Pod 之间的「关系计算」指定一个调度策略。

两种类型的相同点在于后半部分: IgnoredDuringExecution。也就是说,如果某一个 Pod 所在的 Node 的 label 发生了变化,导致一些已经运行在该 Node 上的 Pod 不符合运行条件,那么这些 Pod 也不会受到影响。总结下来就是,目前的「调度策略」都是在调度的过程中生效,至于已经调度完成的 Pod 不会受到影响。

为了更加直观的了解 NodeAffinity 的功能,我们通过一个例子来了解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/pause:2.0

首先,如果在一个 Pod.Spec 段内同时指定了两种 NodeAffinity 的话,那么最好的情况下是调度到两个调度策略都满足的 Node 上。否则,优先满足 required 的。

其次,对于 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 类型来说:

  1. 如果一个 nodeSelectorTerms 中包含多个 item,那么必须要所有的 item 都满足,该条件才会被认为已满足
  2. 如果是有多个 nodeSelectorTerms 的话,那么只要有一个 nodeSelectorTerms 满足,该条件就会被认为已满足

两者之间的差别在于 weight 字段。因为 preferred是一个「软性」的要求,所在在调度 Pod 的时候,如果同时有多个候选 Node 的话,k8s 会对这些 Node 进行打分,从而选出一个分数最高的 Node 进行调度。打分的过程中,如果preferredDuringSchedulingIgnoredDuringExecution 中指定的条件被满足的话,Weight 就会被计入相应 Node 所获得的总分内。

如果 NodeAffinity 和 NodeSelector 同时被指定的时候,必须两者都满足,该 Pod 才可被成功调度。

Inter-pod affinity and anti-affinity

pod affinity/anti-affinity 是基于 Pod 的 label 制定调度策略,而不是 Node 的 label。同时,除了 affinity 特性之外,Pod 还增加了 anti-affinity。两者的区别可以做如下理解:

  • affinity: 如果 affinity 的条件满足,那么 Pod 可以被调度到对应的 Node 上
  • anti-affinity:如果 anti-affinity 的条件满足,那么 Pod 不可以被调度到对应的 Node 上

Pod affinity/anti-affinity 实现的调度策略可以用一句话来概括

The rules are of the form “this pod should (or, in the case of anti-affinity, should not) run in an X if that X is already running one or more pods that meet rule Y”

其中 Y 可以被认为是 PodSelector, 即去筛选特定的一类 Pod。X 在这里也不仅仅代表某一个 Node,而是代表了一组 Node。Nodes 以一个特殊的字段作为标准进行分组:topologyKey(其实 topologyKey 就是 NodeLabel 中的一个 Key, 可以认为我们把带有 Label 为 topologyKey 且值相同的 Node 划分成了一组)。所以,对于 pod 的 affinity/anti-affinity 来说,我们首先要找到一组候选的 Node,然后再根据这些 Node 上面运行的 Pod 的 Label 来决定该 Pod 是否应该调度(or 不应该调度)到相应的节点上。

为了更加直观的了解,我们可以看一个具体的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: failure-domain.beta.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:2.0

该 Pod 设置了 pod affinity/anti-affinity 两个调度条件。 对于 podAffinity 来说,我们需要先找到一组带有 label 为 failure-domain.beta.kubernetes.io/zone 的 Node。如果这些 Node 上有运行 label key 为 security, value 为 S1 的 Pod,那么应该将 Pod 调度到这些 Node 上。该条件为「硬性」限制。也就是说,如果找不到符合上面要求的 Node,这个 Pod 将会面临着调度失败的情况。

对于 podAntiAffinity 来说, 我们首先要找到一组带有 label 为 kubernetes.io/hostname 的 Node(这个 Label 是每个 Node 默认具有的,不需要我们制定,属于 built-in 类型)。如果这些 Node 上有运行 label key 为 security , value 为 S2 的 Pod,那么不应该将 Pod 调度到这些 Node 上。但是这个条件为「软性」限制,满不满足都不会影响 Pod 的调度。

综合起来,对于实例中的这个 Pod 来说,它的调度策略应为如下内容:

首先要找到一组 Node 带有 label 为 failure-domain.beta.kubernetes.io/zone 且 Node 上需要运行 label key 为 security, value 为 S1 的 Pod。否则,调度失败。

若 podAffinity 条件成立,则在已经筛选出来的 Node 中继续寻找带有 label 为 kubernetes.io/hostname 的 Node 且 Node 上有运行 label key 为 security , value 为 S2 的 Pod。若找到符合 pod anti-Affinity 条件的 Node 则不会将 Pod 调度到这些机器上。不过 pod anti-Affinity 指定的是一个「软性」条件,如果此时两个筛选条件找出的 Node 是重合的,Scheduler 将不会管 pod anti-Affinity 的策略。

除了上面我们谈到的几点之外,pod affinity/anti-affinity 还有一个比较特殊的地方,就是 namespace。因为 Pod 是处于某个 namespace 下面的。所以在通过 pod 的 labelSelector 筛选符合条件的 Node 的时候,必须要带上 namespace。默认情况下,如果不指定 namespace,可以认为它就是 定义了 pod affinity/anti-affinity 的 pod 所在的 namespace(被调度的 Pod 所在的 namespace)。

Taints and Tolerations

taints 和 toleraitons 其实是一对的,需要配合在一起使用。taints 作用于 Node 侧,tolerations 作用于 Pod 侧。taints 和 tolerations 本质上来说和 label 的作用差不多,是一种起到标记作用的文本。

当一个 Node 打上了一个或者多个 taints,如果一个 Pod 没有指定 tolerations 或者指定的 tolerations 和 这个 Node 所指定的 taints “不匹配”。这个 Pod 就不能够被调度到这个 Node 上。如果“匹配”,那么这个 Pod 就可以被调度到这个 Node 上。

我们可以通过 kubectl 命令去指定 Node 的 taints kubectl taint nodes node1 key=value: NoSchedule。指定之后,通过 kubectl 观察 Node 的基本信息,可以看到如下内容:

1
2
3
4
  taints:
  - effect: NoSchedule
    key: key
    value: "value"

这个 taints 的作用就是:不允许任何 Pod 调度到该 Node 上,除非这个 Pod 指定了 tolerations 且和该 taints“匹配”。

taints 的作用通过 effect 字段进行标识。除了 NoSchedule 之外,还有一个 PreferNoSchedule,与 NoSchedule 相比,它是一个「软性」的作用。也就是说,k8s 尽量不把 Pod 调度到 tolerations 和 taints 不匹配的 Node上,但是不排除「特殊情况」。taints 还有另外一个作用就是 NoExecute。当一个 Node 指定了 effect 为 NoExecute 的 taints,如果该 Node 上面的 Pod 的 tolerations 没有匹配它的 taints,这些 Pod 会被 kubelet 给 evicted 掉。(小朋友不要轻易尝试这个操作 o( ̄︶ ̄)o)。不过,对于 tolerations 和 taints 匹配的 Pod,在 effect 为 NoExecute 的时候也不一定是「安全」的。原因在于,我们可以在 pod.Spec 中配置一个 tolerationSeconds 的字段,它的作用是:即使是 pod 的 tolerations 和 Node 的 taints 匹配,该 Pod 也只会存活 tolerationSeconds 秒的时间(从匹配成功开始算起),最终仍然会被 evicted 掉。

tolerations 需要在 pod.Spec 中进行指定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
tolerations:
- key: "key"
  operator: "Equal"
  value: "value"
  effect: "NoSchedule" // "PreferNoSchedule"

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

tolerations 和 taints 的匹配过程如下:

  1. tolerations.key, tolerations.effect 必须要与 taints 的同名字段的值严格匹配
  2. tolerations.value 在与 taints.value 匹配的过程中,需要遵循 operator 的规则

operator 的规则有 Exists 和 Equal 之分:

  1. Exists:只要是 key 匹配就可以了,value 是什么并不重要。且在指定 tolerations 的时候,如果 operator 为 Exists,不应该指定 value
  2. Equal:tolerations.value 在与 taints.value 必须要严格匹配

Add taints automatically

在 k8s 1.6 的版本之后,对于 pod 新增了一些基于 taints 的 evict 策略。在 1.8 版本之后更是增加了「根据 node condition 自动添加 taints」 的特性。综合起来可以认为:nodeController(or kubelet) 会根据 node 当前的健康状态为其添加一些 effect 为 NoExecute 的taints。从而实现「根据 Node 状态 evict Pod」的功能。

例如,当某个 Node 节点的网络情况发生异常的时候,nodeController 就会给该 Node 打上一个如下的 taints:

1
2
3
4
  taints:
  - effect: NoExecute
    key: node.kubernetes.io/network-unavailable
    value: "value"