九、高级调度

在第八章中,介绍了调度器的工作过程,以及调度器的过滤和打分行为,这些操作都是k8s的自主行为,用户并不需要干预。而高级调度就是指Kubernetes允许你去影响pod被调度到哪个节点。

1 污点和容忍度

1.1 介绍污点和容忍度

首先要介绍的高级调度的两个特性是节点污点, 以及pod对于污点的容忍度,这些特性可以阻止pod调度到特定节点。

先来看看现有节点的污点信息,默认情况下,一个集群中的主节点需要设置污点,这样才能保证只有控制面板pod才能部署在主节点上:

1
kubectl describe node k8s-master

输出的结果如下(部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Name:               k8s-master
Roles: master
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/arch=amd64
kubernetes.io/hostname=k8s-master
kubernetes.io/os=linux
node-role.kubernetes.io/master=
Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"1e:a5:06:32:5c:bd"}
flannel.alpha.coreos.com/backend-type: vxlan
flannel.alpha.coreos.com/kube-subnet-manager: true
flannel.alpha.coreos.com/public-ip: 192.168.142.20
kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock
node.alpha.kubernetes.io/ttl: 0
volumes.kubernetes.io/controller-managed-attach-detach: true
Taints: node-role.kubernetes.io/master:NoSchedule
...

Taints就是污点信息。主节点包含一个污点,污点包含了一个key、value,以及一个effect。

这里,主节点包含一个为node-role.kubernetes.io/master的key ,一个空的value,以及值为NoSchedule的effect。这个污点将阻止pod调度到这个节点上面,除非有pod能容忍这个污点,而通常容忍这个污点的pod都是系统级别pod。如下图所示,—个pod只有容忍了节点的污点,才能被调度到该节点上面:

image-20220108221941167

前面提到,kube-proxy集群组件以pod的形式运行在每个节点上,其中也包括主节点。为了确保kube-proxy pod也能够运行在主节点上,该pod需要添加相应的污点容忍度。我们可以查看一下:

1
kubectl describe pod kube-proxy-k4nlq -n kube-system

在输出的结果中,查看Toleration列:

1
2
3
4
5
6
7
8
9
10
11
...
Tolerations:
CriticalAddonsOnly
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
...

每一个污点都可以关联一个效果, 效果包含了以下三种

  • NoSchedule表示如果pod没有容忍这些污点, pod则不能被调度到包含这些污点的节点上。
  • PreferNoSchedule是NoSchedule的一个宽松的版本,表示尽量阻止pod被调度到这个节点上,但是如果没有其他节点可以调度, pod 依然会被调度到这个节点上。
  • NoExecute不同于NoSchedule以及PreferNoSchedule,后两者只在调度期间起作用,而NoExecute也会影响正在节点上运行着的pod。如果在一个节点上添加了NoExecute污点,那些在该节点上运行着的pod,如果没有容忍这个NoExecute污点,将会从这个节点驱逐。

1.2 在节点上添加自定义污点

可以使用命令kubectl taint给节点增加一个污点。比如:

1
kubectl taint nodes k8s-node-1 key1=value1:NoSchedule

给节点k8s-node-1增加一个污点,它的键名是key1,键值是value1,效果是NoSchedule。这表示只有拥有和这个污点相匹配的容忍度的pod才能够被分配到k8s-node-1这个节点。如果现在你部署一个常规pod的多个副本,你会发现没有一个pod被部署到你添加了污点信息的节点上面。

现在尝试通过Deployment部署10个副本的pod,资源清单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 10
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9

运行成功后,查看pod信息:

1
kubectl get po -o wide

结果如下图所示,所有的pod都没有被部署在拥有污点的节点上。

image-20220108225137621

若要移除上述命令所添加的污点,你可以执行:

1
kubectl taint nodes k8s-node-1 key1=value1:NoSchedule-

1.3 在pod上添加污点容忍度

如果想要pod部署到这些添加污点的节点,那就需要pod能容忍那些你添加在节点上的污点。在pod的资源清单中添加容忍度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 10
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
tolerations: # 添加容忍度
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"

再次查看结果如下图,添加了与污点相匹配的容忍度,使得pod可以部署在k8s-node-1节点:

image-20220108225559912

operator 的默认值是 Equal。

一个容忍度和一个污点相“匹配”是指它们有一样的键名(key)和效果(effects),并且:

  • 如果 operator 是 Exists (此时容忍度不能指定 value),或者
  • 如果 operator 是 Equal ,则它们的 value 应该相等

存在两种特殊情况:

如果一个容忍度的 key 为空且 operator 为 Exists, 表示这个容忍度与任意的 key 、value 和 effect 都匹配,即这个容忍度能容忍任意 taint。

如果 effect 为空,则可以与所有键名 key1 的效果相匹配。

可以给一个节点添加多个污点,也可以给一个 Pod 添加多个容忍度设置。 Kubernetes 处理多个污点和容忍度的过程就像一个过滤器:从一个节点的所有污点开始遍历,过滤掉那些 Pod 中存在与之相匹配的容忍度的污点。余下未被过滤的污点的 effect 值决定了 Pod 是否会被分配到该节点,特别是以下情况:

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

例如,假设给一个节点添加了如下污点

1
2
3
kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule

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

1
2
3
4
5
6
7
8
9
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 还能继续在节点上运行的时间。例如,

1
2
3
4
5
6
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
tolerationSeconds: 3600

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

1.4 基于污点的驱逐

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

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

当某种条件为真时,节点控制器会自动给节点添加一个污点。当前内置的污点包括:

  • node.kubernetes.io/not-ready:节点未准备好。这相当于节点状态 Ready 的值为 “False”。
  • node.kubernetes.io/unreachable:节点控制器访问不到节点. 这相当于节点状态 Ready 的值为 “Unknown”。
  • node.kubernetes.io/memory-pressure:节点存在内存压力。
  • node.kubernetes.io/disk-pressure:节点存在磁盘压力。
  • node.kubernetes.io/pid-pressure: 节点的 PID 压力。
  • node.kubernetes.io/network-unavailable:节点网络不可用。
  • node.kubernetes.io/unschedulable: 节点不可调度。
  • node.cloudprovider.kubernetes.io/uninitialized:如果 kubelet 启动时指定了一个 “外部” 云平台驱动, 它将给当前节点添加一个污点将其标志为不可用。在 cloud-controller-manager 的一个控制器初始化这个节点后,kubelet 将删除这个污点。

在节点被驱逐时,节点控制器或者 kubelet 会添加带有 NoExecute 效应的相关污点。 如果异常状态恢复正常,kubelet 或节点控制器能够移除相关的污点。

使用这个功能特性,结合 tolerationSeconds,Pod 就可以指定当节点出现一个 或全部上述问题时还将在这个节点上运行多长的时间。

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

1
2
3
4
5
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 分钟。

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

  • node.kubernetes.io/unreachable
  • node.kubernetes.io/not-ready

这保证了出现上述问题时 DaemonSet 中的 Pod 永远不会被驱逐。

2 节点亲和性/反亲和性

在介绍节点亲和性之前,先来看一种更加简单的节点约束方式nodeSelector。

2.1 nodeSelector

来看一个使用 nodeSelector 的例子。

执行 kubectl get nodes 命令获取集群的节点名称。 选择一个你要增加标签的节点,然后执行 kubectl label nodes <node-name> <label-key>=<label-value> 命令将标签添加到你所选择的节点上。 例如,如果你的节点名称为k8s-node-1并且想要的标签是 disktype=ssd,则可以执行下面的命令。

1
kubectl label nodes k8s-node-1 disktype=ssd

重新运行 kubectl get nodes --show-labels,查看节点当前具有了所指定的标签来验证它是否有效。 你也可以使用 kubectl describe node "nodename" 命令查看指定节点的标签完整列表。

然后,选择任何一个想运行的Pod的资源清单,并且在其中添加一个nodeSelector部分。 比如利用Deployment部署pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 10
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
nodeSelector: # 添加nodeSelector
disktype: ssd

然后部署这个Deployment,查看部署情况:

1
kubectl get pod -o wide

如下图,所有的pod都被部署在满足标签disktype=ssd的节点上(即k8s-node-1)

image-20220109102509533

2.2 节点亲和性/反亲和性

nodeSelector要求节点必须包含所有pod对应字段中的指定标签,才能成为pod调度的目标节点。节点选择器实现简单,但是它不能满足你的所有需求。正因为如此,一种更强大的机制被引入。就是节点亲和性/反亲和性。

亲和性/反亲和性功能极大地扩展了你可以表达约束的类型。关键的增强点包括:

  1. 语言更具表现力(不仅仅是“对完全匹配规则的 AND”)
  2. 你可以发现规则是“软需求”/“偏好”,而不是硬性要求,因此, 如果调度器无法满足该要求,仍然调度该 Pod
  3. 你可以使用节点上(或其他拓扑域中)的 Pod 的标签来约束,而不是使用 节点本身的标签,来允许哪些 pod 可以或者不可以被放置在一起。

亲和性功能包含两种类型的亲和性,即“节点亲和性”和“Pod 间亲和性/反亲和性”。 节点亲和性就像现有的 nodeSelector(但具有上面列出的前两个好处),然而 Pod 间亲和性/反亲和性约束 Pod 标签而不是节点标签。

目前有两种类型的节点亲和性,分别为

  • requiredDuringSchedulingIgnoredDuringExecution
  • preferredDuringSchedulingIgnoredDuringExecution

这是一个极其长的名字,所以,让我们先重点关注这个名字。让我们把这个名字分成两部分,然后分别看下它们的含义:

  • requiredDuringScheduling...表明了该宇段下定义的规则,为了让pod能调度到该节点上,明确指出了该节点必须包含的标签
  • preferredDuringScheduling...表明了该宇段下定义的规则,优先尝试调度到该节点,如果不可行,调度到其它节点也可以接受
  • ...IgnoredDuringExecution表明了该字段下定义的规则,不会影响己经在节点上运行着的pod

这二者的区别可以看做是“硬需求”和“软需求”。节点亲和性通过 PodSpec 的 affinity 字段下的 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
27
28
29
30
31
32
33
34
35
36
37
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 10
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
affinity: # 节点亲和性
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd
- ssd2
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value

此节点亲和性规则表示,Pod 只能放置在具有标签键 disktype 且标签值为 ssd 或 ssd2 的节点上。 另外,在满足这些标准的节点中,具有标签键为 another-node-label-key 且标签值为 another-node-label-value 的节点应该优先使用。

你可以在上面的例子中看到 In 操作符的使用。新的节点亲和性语法支持下面的操作符:

  • In,标签的值在某个列表中

  • NotIn,标签的值不在某个列表中

  • Exists,标签存在

  • DoesNotExist,标签不存在

  • Gt,标签的值大于某个值

  • Lt,标签的值小于某个值

你可以使用 NotIn 和 DoesNotExist 来实现节点反亲和性行为,或者使用污点将 Pod 从特定节点中驱逐。

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

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

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

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

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

2.3 pod间亲和性/反亲和性

Pod 间亲和性与反亲和性使你可以基于已经在节点上运行的 Pod 的标签来约束 Pod 可以调度到的节点,而不是基于节点上的标签。

与节点亲和性一样,当前有两种类型的 Pod 亲和性与反亲和性,即 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution,分别表示“硬性”与“软性”要求。

Pod 间亲和性通过 PodSpec 中 affinity 字段下的 podAffinity 字段进行指定。 而 Pod 间反亲和性通过 PodSpec 中 affinity 字段下的 podAntiAffinity 字段进行指定。

下面是一个使用pod间亲和性的例子:

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: topology.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone
containers:
- name: with-pod-affinity
image: k8s.gcr.io/pause:2.0

在这个 Pod 的亲和性配置定义了一条 Pod 亲和性规则和一条 Pod 反亲和性规则。 在此示例中,podAffinity 配置为 requiredDuringSchedulingIgnoredDuringExecution, 然而 podAntiAffinity 配置为 preferredDuringSchedulingIgnoredDuringExecution。

Pod 亲和性规则表示,仅当节点和至少一个已运行且有键为“security”且值为“S1”的标签 的 Pod 处于同一区域时,才可以将该 Pod 调度到节点上。(更确切的说,如果节点 N 具有带有键 topology.kubernetes.io/zone 和某个值 V 的标签, 则 Pod 有资格在节点 N 上运行,以便集群中至少有一个节点具有键 topology.kubernetes.io/zone 和值为 V 的节点正在运行具有键“security”和值 “S1”的标签的 pod。)

Pod 反亲和性规则表示,如果节点处于 Pod 所在的同一可用区且具有键“security”和值“S2”的标签, 则该 pod 不应将其调度到该节点上。 (如果 topologyKey 为 topology.kubernetes.io/zone,则意味着当节点和具有键 “security”和值“S2”的标签的 Pod 处于相同的区域,Pod 不能被调度到该节点上。)

Pod 亲和性与反亲和性的合法操作符有 In,NotIn,Exists,DoesNotExist。

原则上,topologyKey 可以是任何合法的标签键。 然而,出于性能和安全原因,topologyKey 受到一些限制:

  1. 对于 Pod 亲和性而言,在 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 不允许为空。
  2. 对于 Pod 反亲和性而言,requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 都不可以为空。
  3. 对于 requiredDuringSchedulingIgnoredDuringExecution 要求的 Pod 反亲和性, 准入控制器 LimitPodHardAntiAffinityTopology 被引入以确保 topologyKey 只能是 kubernetes.io/hostname。如果你希望 topologyKey 也可用于其他定制 拓扑逻辑,你可以更改准入控制器或者禁用之。
  4. 除上述情况外,topologyKey 可以是任何合法的标签键。

注意:

Pod 间亲和性与反亲和性需要大量的处理,这可能会显著减慢大规模集群中的调度。 不建议在超过数百个节点的集群中使用它们。

Pod 反亲和性需要对节点进行一致的标记,即集群中的每个节点必须具有适当的标签能够匹配 topologyKey。如果某些或所有节点缺少指定的 topologyKey 标签,可能会导致意外行为。

3 固定节点

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

下面的是使用 nodeName 字段的 Pod 配置文件的例子:

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeName: kube-01

上面的 pod 将运行在 kube-01 节点上。

使用 nodeName 来选择节点可能有一些限制:

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