Kubernetes-高级调度
九、高级调度
在第八章中,介绍了调度器的工作过程,以及调度器的过滤和打分行为,这些操作都是k8s的自主行为,用户并不需要干预。而高级调度就是指Kubernetes允许你去影响pod被调度到哪个节点。
1 污点和容忍度
1.1 介绍污点和容忍度
首先要介绍的高级调度的两个特性是节点污点, 以及pod对于污点的容忍度,这些特性可以阻止pod调度到特定节点。
先来看看现有节点的污点信息,默认情况下,一个集群中的主节点需要设置污点,这样才能保证只有控制面板pod才能部署在主节点上:
1 | kubectl describe node k8s-master |
输出的结果如下(部分):
1 | Name: k8s-master |
Taints
就是污点信息。主节点包含一个污点,污点包含了一个key、value,以及一个effect。
这里,主节点包含一个为node-role.kubernetes.io/master
的key ,一个空的value,以及值为NoSchedule
的effect。这个污点将阻止pod调度到这个节点上面,除非有pod能容忍这个污点,而通常容忍这个污点的pod都是系统级别pod。如下图所示,—个pod只有容忍了节点的污点,才能被调度到该节点上面:
前面提到,kube-proxy集群组件以pod的形式运行在每个节点上,其中也包括主节点。为了确保kube-proxy pod也能够运行在主节点上,该pod需要添加相应的污点容忍度。我们可以查看一下:
1 | kubectl describe pod kube-proxy-k4nlq -n kube-system |
在输出的结果中,查看Toleration
列:
1 | ... |
每一个污点都可以关联一个效果, 效果包含了以下三种
- 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 | apiVersion: apps/v1 |
运行成功后,查看pod信息:
1 | kubectl get po -o wide |
结果如下图所示,所有的pod都没有被部署在拥有污点的节点上。
若要移除上述命令所添加的污点,你可以执行:
1 | kubectl taint nodes k8s-node-1 key1=value1:NoSchedule- |
1.3 在pod上添加污点容忍度
如果想要pod部署到这些添加污点的节点,那就需要pod能容忍那些你添加在节点上的污点。在pod的资源清单中添加容忍度:
1 | apiVersion: apps/v1 |
再次查看结果如下图,添加了与污点相匹配的容忍度,使得pod可以部署在k8s-node-1
节点:
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 | kubectl taint nodes node1 key1=value1:NoSchedule |
假定有一个Pod,它有两个容忍度:
1 | tolerations: |
在这种情况下,上述 Pod 不会被分配到上述节点,因为其没有容忍度和第三个污点相匹配。 但是如果在给节点添加上述污点之前,该 Pod 已经在上述节点运行, 那么它还可以继续运行在该节点上,因为第三个污点是三个污点中唯一不能被这个 Pod 容忍的。
通常情况下,如果给一个节点添加了一个 effect 值为 NoExecute
的污点, 则任何不能忍受这个污点的 Pod 都会马上被驱逐, 任何可以忍受这个污点的 Pod 都不会被驱逐。 但是,如果 Pod 存在一个 effect 值为 NoExecute
的容忍度指定了可选属性 tolerationSeconds
的值,则表示在给节点添加了上述污点之后, Pod 还能继续在节点上运行的时间。例如,
1 | tolerations: |
这表示如果这个 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 | tolerations: |
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 | apiVersion: apps/v1 |
然后部署这个Deployment,查看部署情况:
1 | kubectl get pod -o wide |
如下图,所有的pod都被部署在满足标签disktype=ssd
的节点上(即k8s-node-1
)
2.2 节点亲和性/反亲和性
nodeSelector
要求节点必须包含所有pod对应字段中的指定标签,才能成为pod调度的目标节点。节点选择器实现简单,但是它不能满足你的所有需求。正因为如此,一种更强大的机制被引入。就是节点亲和性/反亲和性。
亲和性/反亲和性功能极大地扩展了你可以表达约束的类型。关键的增强点包括:
- 语言更具表现力(不仅仅是“对完全匹配规则的 AND”)
- 你可以发现规则是“软需求”/“偏好”,而不是硬性要求,因此, 如果调度器无法满足该要求,仍然调度该 Pod
- 你可以使用节点上(或其他拓扑域中)的 Pod 的标签来约束,而不是使用 节点本身的标签,来允许哪些 pod 可以或者不可以被放置在一起。
亲和性功能包含两种类型的亲和性,即“节点亲和性”和“Pod 间亲和性/反亲和性”。 节点亲和性就像现有的 nodeSelector
(但具有上面列出的前两个好处),然而 Pod 间亲和性/反亲和性约束 Pod 标签而不是节点标签。
目前有两种类型的节点亲和性,分别为
requiredDuringSchedulingIgnoredDuringExecution
preferredDuringSchedulingIgnoredDuringExecution
这是一个极其长的名字,所以,让我们先重点关注这个名字。让我们把这个名字分成两部分,然后分别看下它们的含义:
requiredDuringScheduling...
表明了该宇段下定义的规则,为了让pod能调度到该节点上,明确指出了该节点必须包含的标签preferredDuringScheduling...
表明了该宇段下定义的规则,优先尝试调度到该节点,如果不可行,调度到其它节点也可以接受...IgnoredDuringExecution
表明了该字段下定义的规则,不会影响己经在节点上运行着的pod
这二者的区别可以看做是“硬需求”和“软需求”。节点亲和性通过 PodSpec 的 affinity
字段下的 nodeAffinity
字段进行指定。
下面是一个使用节点亲和性的例子:
1 | apiVersion: apps/v1 |
此节点亲和性规则表示,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 | apiVersion: v1 |
在这个 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 受到一些限制:
- 对于 Pod 亲和性而言,在
requiredDuringSchedulingIgnoredDuringExecution
和preferredDuringSchedulingIgnoredDuringExecution
中,topologyKey
不允许为空。 - 对于 Pod 反亲和性而言,
requiredDuringSchedulingIgnoredDuringExecution
和preferredDuringSchedulingIgnoredDuringExecution
中,topologyKey
都不可以为空。 - 对于
requiredDuringSchedulingIgnoredDuringExecution
要求的 Pod 反亲和性, 准入控制器LimitPodHardAntiAffinityTopology
被引入以确保topologyKey
只能是kubernetes.io/hostname
。如果你希望topologyKey
也可用于其他定制 拓扑逻辑,你可以更改准入控制器或者禁用之。 - 除上述情况外,
topologyKey
可以是任何合法的标签键。
注意:
Pod 间亲和性与反亲和性需要大量的处理,这可能会显著减慢大规模集群中的调度。 不建议在超过数百个节点的集群中使用它们。
Pod 反亲和性需要对节点进行一致的标记,即集群中的每个节点必须具有适当的标签能够匹配
topologyKey
。如果某些或所有节点缺少指定的topologyKey
标签,可能会导致意外行为。
3 固定节点
最后,来看一种最为简单的节点调度nodeName
。nodeName
是节点选择约束的最简单方法,但是由于其自身限制,通常不使用它。 nodeName
是 PodSpec 的一个字段。 如果它不为空,调度器将忽略 Pod,并且给定节点上运行的 kubelet 进程尝试执行该 Pod。 因此,如果 nodeName
在 PodSpec 中指定了,则它优先于上面的节点选择方法。
下面的是使用 nodeName
字段的 Pod 配置文件的例子:
1 | apiVersion: v1 |
上面的 pod 将运行在 kube-01 节点上。
使用 nodeName
来选择节点可能有一些限制:
- 指定的节点可能不存在
- 如果指定的节点没有资源来容纳 Pod,Pod 将会调度失败并且其原因将显示为
OutOfmemory
或OutOfcpu
- 云环境中的节点名称并非总是可预测或稳定的