Kubernetes nodeAffinity 与 nodeSelector 介绍

Kubernetes 的亲和性调度

K8s 中的 POD 是由调度器自动选择某个 NODE 的。默认情况下,调度器会考虑资源是否充足,并尽量让各节点的负载保持均衡。但在某些场景下,我们需要更细粒度地控制 POD 的调度,比如要求多个 POD 调度到同一个 NODE 上。此时就需要借助 Kubernetes 中的一个重要概念:亲和性。亲和性主要分为两大类:nodeAffinity 和 podAffinity。

nodeSelector

用户可以非常灵活地利用 label 来管理集群中的资源。其中最常见的场景之一,就是 Service 通过匹配 label 来选择对应的 POD。POD 的调度也可以依据节点的 label 来完成特定的部署。

我们可以通过下面的命令查看节点的 label:

1
2
3
4
5
6
7
$ kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
192.168.1.140 Ready <none> 42d v1.8.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.1.140
192.168.1.161 Ready <none> 118d v1.8.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/cluster-service=true,kubernetes.io/hostname=192.168.1.161
192.168.1.170 Ready <none> 118d v1.8.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/cluster-service=true,kubernetes.io/hostname=192.168.1.170
192.168.1.172 Ready <none> 114d v1.8.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/cluster-service=true,kubernetes.io/hostname=192.168.1.172

现在我们先给节点 192.168.1.140 打上一个 source=qikqiak 的标签,命令如下:

1
2
3
$ kubectl label nodes 192.168.1.140 source=qikqiak
node "192.168.1.140" labeled

随后可以通过前面提到的 --show-labels 参数确认上述标签是否生效。当节点被打上相关标签后,在调度时就可以使用这些标签了,只需在 POD 的 spec 字段中添加 nodeSelector 字段,里面填写希望调度到的目标节点的 label 即可。例如,下面是一个默认的 busybox POD 的 YAML 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
labels:
app: busybox-pod
name: test-busybox
spec:
containers:
- command:
- sleep
- "3600"
image: busybox
imagePullPolicy: Always
name: test-busybox

如果希望上面的 POD 被调度到 140 节点,最简单的做法就是匹配 140 节点上的 label,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
labels:
app: busybox-pod
name: test-busybox
spec:
containers:
- command:
- sleep
- "3600"
image: busybox
imagePullPolicy: Always
name: test-busybox
nodeSelector:
source: qikqiak

然后可以通过 describe 命令查看调度结果:

1
2
3
4
5
6
7
8
9
10
11
12
$ kubectl describe pod test-busybox
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 49s default-scheduler Successfully assigned test-busybox to 192.168.1.140
Normal SuccessfulMountVolume 49s kubelet, 192.168.1.140 MountVolume.SetUp succeeded for volume "default-token-hmpbz"
Normal Pulling 49s kubelet, 192.168.1.140 pulling image "busybox"
Normal Pulled 41s kubelet, 192.168.1.140 Successfully pulled image "busybox"
Normal Created 41s kubelet, 192.168.1.140 Created container
Normal Started 41s kubelet, 192.168.1.140 Started container

从 Events 信息可以看到,上面的 POD 被正确地调度到了 140 节点。通过这个示例可以感受到,nodeSelector 的方式比较直观,但还不够灵活,控制粒度偏大。下面我们再看一种更加灵活的方式:nodeAffinity。

nodeAffinity

nodeAffinity 即节点亲和性,与之相对应的是 Anti-Affinity,也就是反亲和性。相较于前面的 nodeSelector,这种方式更加灵活,支持一些简单的逻辑组合,而不仅仅是简单的相等匹配。调度可以分为软策略和硬策略两种:软策略指的是如果没有满足条件的节点,POD 也会忽略这条规则继续完成调度,简而言之就是满足条件最好,没有也无所谓的策略;硬策略则比较强硬,如果没有满足条件的节点,就会一直重试,直到满足条件为止,简而言之就是必须满足,否则就不调度的策略。

nodeAffinity 分为以下两种策略:

  1. preferredDuringSchedulingIgnoredDuringExecution 软策略
  2. requiredDuringSchedulingIgnoredDuringExecution 硬策略。

示例如下(test-node-affinity.yaml):

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-node-affinity
labels:
app: node-affinity-pod
spec:
containers:
- name: with-node-affinity
image: nginx
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- 192.168.1.140
- 192.168.1.161
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: source
operator: In
values:
- qikqiak

上面这个 POD 首先要求不能运行在 140 和 161 两个节点上;如果有某个节点满足 source=qikqiak,则优先调度到该节点。同样,我们可以使用 describe 命令查看具体的调度情况是否符合预期。这里的匹配逻辑是:label 的值是否在某个列表中。目前 Kubernetes 提供的操作符有以下几种:

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

如果 nodeSelectorTerms 下面存在多个选项,只需满足其中任意一个条件即可;如果 matchExpressions 下面存在多个选项,则必须同时满足所有条件才能正常调度 POD。

podAffinity

上面两种方式都是让 POD 选择节点,但有时我们希望根据 POD 之间的关系进行调度。Kubernetes 在 1.4 版本引入的 podAffinity 就可以满足这种需求。

与 nodeAffinity 类似,podAffinity 同样支持 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 两种调度策略。唯一的区别在于,如果要实现互斥关系,需要使用 podAntiAffinity 字段。例如下面这个示例,我们希望 with-pod-affinity 与 busybox-pod 就近部署,同时不希望它与 node-affinity-pod 部署在同一个拓扑域下(test-pod-affinity.yaml):

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
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
labels:
app: pod-affinity-pod
spec:
containers:
- name: with-pod-affinity
image: nginx
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- busybox-pod
topologyKey: kubernetes.io/hostname
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- node-affinity-pod
topologyKey: kubernetes.io/hostname

上述示例中的 POD 需要被调度到这样一台主机上:至少存在一个正在运行的 POD,其带有 app=busybox-pod 的 label。而 podAntiAffinity 则表示最好不要调度到以下这种节点:该节点上运行的某个 POD 带有 app=node-affinity-pod 的 label。结合前面两个 POD 的定义可以预见,这个 POD 应当会被调度到 140 节点,因为 busybox-pod 已经被调度到了 140 节点,而 node-affinity-pod 被调度到了 140 以外的节点,正好满足上面的需求。通过 describe 进行查看:

1
2
3
4
5
6
7
8
9
$ kubectl describe pod with-pod-affinity
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 8s default-scheduler Successfully assigned with-pod-affinity to 192.168.1.140
Normal SuccessfulMountVolume 7s kubelet, 192.168.1.140 MountVolume.SetUp succeeded for volume "default-token-lcl77"
Normal Pulling 7s kubelet, 192.168.1.140 pulling image "nginx"

上面的事件信息也验证了我们的预期。

在 labelSelector 和 topologyKey 的同级,还可以定义 namespaces 列表,用于指定需要匹配哪些 namespace 中的 pod。默认情况下,会匹配当前所定义 pod 所在的 namespace;如果显式定义了该字段但其值为空,则会匹配所有的 namespaces。

查看上面定义的 3 个 POD 的运行结果:

1
2
3
4
5
6
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
test-busybox 1/1 Running 0 8m 172.30.95.18 192.168.1.140
with-node-affinity 1/1 Running 0 10m 172.30.81.25 192.168.1.172
with-pod-affinity 1/1 Running 0 8m 172.30.95.17 192.168.1.140

亲和性与反亲和性调度策略对比如下:

调度策略 匹配标签 操作符 拓扑域支持 调度目标
nodeAffinity 主机 In, NotIn, Exists, DoesNotExist, Gt, Lt 否 指定主机
podAffinity POD In, NotIn, Exists, DoesNotExist 是 POD 与指定 POD 同一拓扑域
podAnitAffinity POD In, NotIn, Exists, DoesNotExist 是 POD 与指定 POD 不在同一拓扑域

污点(Taints)与容忍(tolerations)

对于 nodeAffinity,无论是硬策略还是软策略,目的都是把 POD 调度到预期的节点上。而 Taints 恰好相反,如果一个节点被标记为 Taints,除非某个 POD 显式表示可以容忍该污点,否则该 Taints 节点不会被调度 POD。

比如用户希望将 Master 节点保留给 Kubernetes 系统组件使用,或者将一组具有特殊资源的节点预留给特定的 POD,此时污点就非常有用了,POD 不会再被调度到带有 taint 标记的节点上。taint 标记节点的示例如下:

1
2
3
$ kubectl taint nodes 192.168.1.40 key=value:NoSchedule
node "192.168.1.40" tainted

如果仍然希望某个 POD 调度到带有 taint 的节点上,就必须在 Spec 中做出 Toleration 定义,才能将其调度到该节点,示例如下:

1
2
3
4
5
6
tolerations:
- key: "key"
operator: "Equal"
value: "value"
effect: "NoSchedule"

effect 共有三个可选值,可按实际需求进行设置:

  1. NoSchedule:除非具有匹配的容忍度规约,否则新的 Pod 不会被调度到带有污点的节点上。当前正在该节点上运行的 Pod 不会被驱逐。
  2. PreferNoSchedule:PreferNoSchedule 是 “偏好” 或 “软性” 的 NoSchedule。控制平面会尝试避免将不能容忍污点的 Pod 调度到该节点上,但无法完全保证。
  3. NoExecute:该选项意味着一旦 Taint 生效,如果该节点上正在运行的 POD 没有对应的 Tolerate 设置,将会被直接驱逐。

Refer


Kubernetes nodeAffinity 与 nodeSelector 介绍
https://garychow-lgtm.github.io/zh-CN/2024/07/31/Kubernetes-nodeAffinity-And-Selector/
作者
Gary Chow (Yimin Cao)
发布于
2024年7月31日
许可协议