k8s Pod 资源调度
k8s 调度器
在 Kubernetes 中,调度 是指将 Pod 放置到合适的节点上,以便对应节点上的 Kubelet 能够运行这些 Pod。
调度概览
调度器通过 Kubernetes 的监测(Watch)机制来发现集群中新创建且尚未被调度到节点上的 Pod。 调度器会将所发现的每一个未调度的 Pod 调度到一个合适的节点上来运行。 调度器会依据下文的调度原则来做出调度选择。
kube-scheduler
kube-scheduler 是 Kubernetes 的默认调度程序,作为控制平面的一部分运行。
Kube-scheduler 选择一个最佳节点来运行新创建的或尚未调度(unscheduled)的 Pod。 由于 Pod 中的容器和 Pod 本身可能有不同的要求,调度程序会过滤掉任何不满足 Pod 特定调度需求的节点。 或者,API 允许你在创建 Pod 时为它指定一个节点,但这并不常见,并且仅在特殊情况下才会这样做。
在一个集群中,满足一个 Pod 调度请求的所有节点称之为 可调度节点。 如果没有任何一个节点能满足 Pod 的资源请求, 那么这个 Pod 将一直停留在未调度状态直到调度器能够找到合适的 Node。
调度器先在集群中找到一个 Pod 的所有可调度节点,然后根据一系列函数对这些可调度节点打分, 选出其中得分最高的节点来运行 Pod。之后,调度器将这个调度决定通知给 kube-apiserver,这个过程叫做 绑定。而后由相应节点的代理程序kubelet启动Pod的创建和启动等过程。
在做调度决定时需要考虑的因素包括:单独和整体的资源请求、硬件/软件/策略限制、 亲和以及反亲和要求、数据局部性、负载间的干扰等等。
kube-scheduler 中的节点选择
kube-scheduler 给一个 Pod 做调度选择时包含两个步骤:Filtering(过滤)和Scoring(打分)
过滤
过滤阶段会将所有满足 Pod 调度需求的节点选出来。 例如,PodFitsResources 过滤函数会检查候选节点的可用资源能否满足 Pod 的资源请求。 在过滤之后,得出一个节点列表,里面包含了所有可调度节点;通常情况下, 这个节点列表包含不止一个节点。如果这个列表是空的,代表这个 Pod 不可调度,该Pod将被置于Pending状态,直到出现至少一个能够满足条件的节点为止。
打分
在打分阶段,调度器会为 Pod 从所有可调度节点中选取一个最合适的节点。 根据当前启用的打分规则,调度器会给每一个可调度节点进行打分。
最后,kube-scheduler 会将 Pod 调度到得分最高的节点上。 如果存在多个得分最高的节点,kube-scheduler 会从中随机选取一个。
调度框架
框架工作流程
调度框架定义了一些扩展点。调度器插件注册后在一个或多个扩展点处被调用。 这些插件中的一些可以改变调度决策,而另一些仅用于提供信息。
每次调度一个 Pod 的尝试都分为两个阶段,即 调度周期 和 绑定周期。
调度周期和绑定周期
调度周期为 Pod 选择一个节点,绑定周期将该决策应用于集群。 调度周期和绑定周期一起被称为“调度上下文”。
调度周期是串行运行的,而绑定周期可能是同时运行的。
如果确定 Pod 不可调度或者存在内部错误,则可以终止调度周期或绑定周期。 Pod 将返回队列并重试。
调度框架扩展点
▪ PreEnqueue:这些插件在将 Pod 被添加到内部活动队列之前被调用,在此队列中 Pod 被标记为准备好进行调度。只有当所有 PreEnqueue 插件返回 Success 时,Pod 才允许进入活动队列。 否则,它将被放置在内部无法调度的 Pod 列表中,并且不会获得 Unschedulable 状态。
▪ QueueSort:注册到该扩展点的插件负责对调度队列中的Pod资源进行排序,但一次仅支持启用单个插件;Pod排序队列的存在使得优选级调度及优选级抢占成为可能。
▪ PreFilter:PreFilter类的插件用于预处理Pod相关信息,或者检查Pod和集群必须满足的条件,任何错误都将会导致调度过程中止而返回。
▪ Filter:该类型的插件负责过滤无法满足Pod资源运行条件的节点,对于每一个节点,调度程序都会按顺序调用每个插件对其进行逐一评估,任何插件拒斥该节点都会直接导致该插件被排除,且不再由后续的插件进行检查。节点过滤能够以并行方式运行,并且在一个调度周期内可以多次调用该扩展点上的插件。
▪ PostFilter:该类插件对成功通过过滤插件检查的节点执行过滤后操作,较早版本的调度框架不支持PreScore,该扩展点后来被重命名为PreScore,而Kubernetes v1.19版本又重新添加了该扩展点。
▪ PreScore:该类插件对成功通过过滤插件检查的节点进行预评分,并生成可由各Score插件共享的状态结果,任何错误都将导致调度过程中止而返回。
▪ Score:该类插件负责对成功通过过滤的节点进行评分和排序,对于每个节点,调度程序会调用每个插件为其打分。
▪ NormalizeScore:该类插件可为Score扩展点中的同名插件提供节点得分修正逻辑,以使得其满足特定的规范(节点得分满足[MinNodeScore,MaxNodeScore]的范围要求),不提供NormalizeScore插件的话,Score插件自身必须确保得分满足该规范,否则调度周期将被中止。
▪ Reserve:信息类扩展点,一般用于为给定的Pod保留目标节点上的特定资源时提供状态信息,以避免将Pod绑定到目标节点的过程中发生资源争用。
▪ Permit:该类型插件用于准许(approve)、阻止(deny)或延迟(wait)Pod资源的绑定,所有插件都返回approve才意味着该Pod可进入绑定周期,任意一个插件返回deny都会导致Pod重新返回调度队列,并触发Unreserve类型的插件,而返回wait则意味着Pod将保持在该阶段,直接批准而返回approve或超时而返回deny。
▪ PreBind:负责执行绑定Pod之前所需要的所有任务,例如设置存储卷等;任何插件返回错误都会导致该Pod被打回调度队列。
▪ Bind:所有的PreBind类插件完成之后才能运行该类插件,以将Pod绑定至目标节点上,各插件依照其配置的顺序进行调用,或者由某个特定的插件全权“处理”该Pod,从而跳过后续的其他插件。
▪ PostBind:信息类扩展点,相关插件在Pod成功绑定之后被调用,通常用于设置清单关联的资源。
▪ Unreserve:信息类扩展点,对于在Reserve扩展点预留了资源的Pod对象,因被其他扩展点插件所拒绝时,可由该扩展点通知取消为其预留的资源;一般来说,注册到该扩展点的插件也必将注册到Reserve扩展点之上。
配置调度器
你可以通过编写配置文件,并将其路径传给 kube-scheduler 的命令行参数,定制 kube-scheduler 的行为。
调度模板(Profile)允许你配置 kube-scheduler 中的不同调度阶段。每个阶段都暴露于某个扩展点中。插件通过实现一个或多个扩展点来提供调度行为。
你可以通过运行 kube-scheduler --config <filename> 来设置调度模板, 使用 KubeSchedulerConfiguration v1 结构体。
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/srv/kubernetes/kube-scheduler/kubeconfig
配置文件
扩展点
调度行为发生在一系列阶段中,这些阶段是通过以下扩展点公开的:
1. queueSort:这些插件对调度队列中的悬决的 Pod 排序。 一次只能启用一个队列排序插件。
2. preFilter:这些插件用于在过滤之前预处理或检查 Pod 或集群的信息。 它们可以将 Pod 标记为不可调度。
3. filter:这些插件相当于调度策略中的断言(Predicates),用于过滤不能运行 Pod 的节点。 过滤器的调用顺序是可配置的。 如果没有一个节点通过所有过滤器的筛选,Pod 将会被标记为不可调度。
4. postFilter:当无法为 Pod 找到可用节点时,按照这些插件的配置顺序调用他们。 如果任何 postFilter 插件将 Pod 标记为可调度,则不会调用其余插件。
5. preScore:这是一个信息扩展点,可用于预打分工作。
6. score:这些插件给通过筛选阶段的节点打分。调度器会选择得分最高的节点。
7. reserve:这是一个信息扩展点,当资源已经预留给 Pod 时,会通知插件。 这些插件还实现了 Unreserve 接口,在 Reserve 期间或之后出现故障时调用。
8. permit:这些插件可以阻止或延迟 Pod 绑定。
9. preBind:这些插件在 Pod 绑定节点之前执行。
10. bind:这个插件将 Pod 与节点绑定。bind 插件是按顺序调用的,只要有一个插件完成了绑定,其余插件都会跳过。bind 插件至少需要一个。
11. postBind:这是一个信息扩展点,在 Pod 绑定了节点之后调用。
12. multiPoint:这是一个仅配置字段,允许同时为所有适用的扩展点启用或禁用插件。
对每个扩展点,你可以禁用默认插件或者是启用自己的插件,例如:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- plugins:
score:
disabled:
- name: PodTopologySpread
enabled:
- name: MyCustomPluginA
weight: 2
- name: MyCustomPluginB
weight: 1
你可以在 disabled 数组中使用 * 禁用该扩展点的所有默认插件。 如果需要,这个字段也可以用来对插件重新顺序。
多配置文件
你可以配置 kube-scheduler 运行多个配置文件。 每个配置文件都有一个关联的调度器名称,并且可以在其扩展点中配置一组不同的插件。
使用下面的配置样例,调度器将运行两个配置文件:一个使用默认插件,另一个禁用所有打分插件。
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
- schedulerName: no-scoring-scheduler
plugins:
preScore:
disabled:
- name: '*'
score:
disabled:
- name: '*'
节点亲和调度
节点亲和是调度程序用来确定Pod对象调度位置(哪个或哪类节点)的调度法则,这些规则基于节点上的自定义标签和Pod对象上指定的标签选择器进行定义,而支持这种调度机制的有NodeName和NodeAffinity调度插件。简单来说,节点亲和调度机制支持Pod资源定义自身对期望运行的某类节点的倾向性,倾向于运行指定类型的节点即为“亲和”关系,否则即为“反亲和”关系。
nodeSelector
nodeSelector 是节点选择约束的最简单推荐形式。你可以将 nodeSelector 字段添加到 Pod 的规约中设置你希望目标节点所具有的节点标签。 Kubernetes 只会将 Pod 调度到拥有你所指定的每个标签的节点上。
apiVersion: v1
kind: Pod
metadata:
name: pod-with-nodeselector
spec:
containers:
- name: demoapp
image: ikubernetes/demoapp:v1.0
nodeSelector:
gpu: ''
应该尽量避免使用.spec.nodeName静态指定Pod对象的运行位置,而是应该让调度器基于标签和标签选择器为Pod挑选匹配的工作节点。另外,Pod规范中的.spec.nodeSelector仅支持简单等值关系的节点选择器,而.spec.affinity.nodeAffinity支持更灵活的节点选择器表达式,而且可以实现强制和与首选和逻辑。
nodeName
nodeName 是比亲和性或者 nodeSelector 更为直接的形式。nodeName 是 Pod 规约中的一个字段。如果 nodeName 字段不为空,调度器会忽略该 Pod, 而指定节点上的 kubelet 会尝试将 Pod 放到该节点上。 使用 nodeName 规则的优先级会高于使用 nodeSelector 或亲和性与非亲和性的规则。
使用 nodeName 来选择节点的方式有一些局限性:
1. 如果所指代的节点不存在,则 Pod 无法运行,而且在某些情况下可能会被自动删除。
2. 如果所指代的节点无法提供用来运行 Pod 所需的资源,Pod 会失败, 而其失败原因中会给出是否因为内存或 CPU 不足而造成无法运行。
3. 在云环境中的节点名称并不总是可预测的,也不总是稳定的。
下面的示例中,Pod 只能运行在节点 kube-01 之上。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeName: k8s-node01
节点亲和性
节点亲和性概念上类似于 nodeSelector, 它使你可以根据节点上的标签来约束 Pod 可以调度到哪些节点上。 节点亲和性有两种:
requiredDuringSchedulingIgnoredDuringExecution: 调度器只有在规则被满足的时候才能执行调度。此功能类似于 nodeSelector, 但其语法表达能力更强。强制(required)亲和。
preferredDuringSchedulingIgnoredDuringExecution: 调度器会尝试寻找满足对应规则的节点。如果找不到匹配的节点,调度器仍然会调度该 Pod。首选(preferred)亲和。
强制节点亲和
Pod规范中的.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution字段用于定义节点的强制亲和关系,它的值是一个对象列表,可由一到多个nodeSelectorTerms对象组成,彼此间为“逻辑或”关系。nodeSelectorTerms用于定义节点选择器,其值为对象列表,它支持matchExpressions和matchFields两种复杂的表达机制。
▪ matchExpressions:标签选择器表达式,基于节点标签进行过滤;可重复使用以表达不同的匹配条件,各条件间为“或”关系。
▪ matchFields:以字段选择器表达的节点选择器;可重复使用以表达不同的匹配条件,各条件间为“或”关系。
说明:
如果你同时指定了 nodeSelector 和 nodeAffinity,两者 必须都要满足, 才能将 Pod 调度到候选节点上。
如果你在与 nodeAffinity 类型关联的 nodeSelectorTerms 中指定多个条件, 只要其中一个 nodeSelectorTerms 满足(各个条件按逻辑或操作组合)的话,Pod 就可以被调度到节点上。
如果你在与 nodeSelectorTerms 中的条件相关联的单个 matchExpressions 字段中指定多个表达式, 则只有当所有表达式都满足(各表达式按逻辑与操作组合)时,Pod 才能被调度到节点上。
以下示例中,Pod模板使用了强制节点亲和约束,它要求Pod只能运行在那些拥有gpu标签且不具有node-role.kubernetes.io/master标签的节点之上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-affinity-required
namespace: default
spec:
replicas: 5
selector:
matchLabels:
app: demoapp
ctlr: node-affinity-required
template:
metadata:
labels:
app: demoapp
ctlr: node-affinity-required
spec:
containers:
- name: demoapp
image: ikubernetes/demoapp:v1.0
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: Exists
- key: node-role.kubernetes.io/master
operator: DoesNotExist
首选节点亲和
节点首选亲和机制为节点选择机制提供了一种柔性控制逻辑,被调度的Pod对象不再是“必须”,而是“应该”放置到某些特定节点之上,但条件不满足时,该Pod也能够接受被编排到其他不符合条件的节点之上。
以下示例中,Pod资源模板定义了节点首选亲和,以选择尽量运行在指定范围内拥有gpu标签或者zone标签的节点之上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-affinity-required
namespace: default
spec:
replicas: 5
selector:
matchLabels:
app: demoapp
ctlr: node-affinity-required
template:
metadata:
labels:
app: demoapp
ctlr: node-affinity-required
spec:
containers:
- name: demoapp
image: ikubernetes/demoapp:v1.0
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 60
preference:
matchExpressions:
- key: gpu
operator: Exists
- weight: 30
preference:
matchExpressions:
- key: zone
operator: In
values: ["foo","bar"]
节点亲和性权重
你可以为 preferredDuringSchedulingIgnoredDuringExecution 亲和性类型的每个实例设置 weight 字段,其取值范围是 1 到 100。数字越大优先级越高。 当调度器找到能够满足 Pod 的其他调度请求的节点时,调度器会遍历节点满足的所有的偏好性规则, 并将对应表达式的 weight 值加和。
最终的加和值会添加到该节点的其他优先级函数的评分之上。 在调度器为 Pod 作出调度决定时,总分最高的节点的优先级也最高。
apiVersion: v1
kind: Pod
metadata:
name: with-affinity-anti-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: label-1
operator: In
values:
- key-1
- weight: 50
preference:
matchExpressions:
- key: label-2
operator: In
values:
- key-2
containers:
- name: with-node-affinity
image: registry.k8s.io/pause:2.0
如果存在两个候选节点,都满足 preferredDuringSchedulingIgnoredDuringExecution 规则, 其中一个节点具有标签 label-1:key-1,另一个节点具有标签 label-2:key-2, 调度器会考察各个节点的 weight 取值,并将该权重值添加到节点的其他得分值之上。
Pod亲和调度
出于高效通信等需求,偶尔需要把一些Pod对象组织在相近的位置(同一节点、机架、区域或地区等),例如应用程序的Pod及其后端提供数据服务的Pod等,我们可以认为这是一类具有亲和关系的Pod对象。偶尔,出于安全或分布式容灾等原因,也会需要把一些Pod对象与其所运行的位置隔离开来,例如在IDC中的区域运行某应用的单个代理Pod对象等,我们可把这类Pod对象间的关系称为反亲和。
Pod 间亲和性与反亲和性使你可以基于已经在节点上运行的 Pod 的标签来约束 Pod 可以调度到的节点,而不是基于节点上的标签。
Pod 间亲和性与反亲和性的类型
与节点亲和性类似,Pod 的亲和性与反亲和性也有两种类型:
requiredDuringSchedulingIgnoredDuringExecution 亲和性,将两个服务的 Pod 放到同一个云提供商可用区内,因为它们彼此之间通信非常频繁。
preferredDuringSchedulingIgnoredDuringExecution 反亲和性,将同一服务的多个 Pod 分布到多个云提供商可用区中。
Pod间亲和定义
Pod间的亲和关系定义在spec.affinity.podAffinity字段中,而反亲和关系定义在spec.affinity.podAntiAffinity字段中,它们各自的约束特性也存在强制与首选两种,它们都支持使用如下关键字段。
▪ topologyKey <string>:拓扑键,用来划分拓扑结构的节点标签,在指定的键上具有相同值的节点归属为同一拓扑;必选字段。
▪ labelSelector <Object>:Pod标签选择器,用于指定该Pod将针对哪类现有Pod的位置来确定可放置的位置。
▪ namespaces <[]string>:用于指示labelSelector字段的生效目标名称空间,默认为当前Pod所属的同一名称空间。
出于性能和安全原因,topologyKey 有一些限制:
1. 对于 Pod 亲和性而言,在 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 不允许为空。
2. 对于 requiredDuringSchedulingIgnoredDuringExecution 要求的 Pod 反亲和性, 准入控制器 LimitPodHardAntiAffinityTopology 要求 topologyKey 只能是 kubernetes.io/hostname。如果你希望使用其他定制拓扑逻辑, 你可以更改准入控制器或者禁用之。
Pod间的强制亲和
Pod间的亲和关系用于描述一个Pod对象与具有某特征的现存Pod对象运行位置的依赖关系,因而,满足Pod亲和约束的前提是确定“被依赖的”Pod对象,以及节点拓扑位置的确定机制。
强制亲和性规则表示,仅当节点和至少一个已运行且有 security=S1 的标签的 Pod 处于同一区域时,才可以将该 Pod 调度到节点上。 更确切的说,调度器必须将 Pod 调度到具有 topology.kubernetes.io/zone=V 标签的节点上,并且集群中至少有一个位于该可用区的节点上运行着带有 security=S1 标签的 Pod。
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
containers:
- name: with-pod-affinity
image: registry.k8s.io/pause:2.0
Pod间的首选亲和
Pod间的首选亲和约束也使用preferredDuringSchedulingIgnoredDuringExecution字段进行定义,它同样允许用户定义具有不同权重的多重亲和条件,以定义出多个不同适配级别位置。下面资源示意示例中Pod将以首选亲和的方式期望与security=S2运行在同一节点,当条件无法满足时,则期望运行在同一区域(topology.kubernetes.io/zone=V 标签),否则也能接受运行在集群中的其他任何节点之上。
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone
containers:
- name: with-pod-affinity
image: registry.k8s.io/pause:2.0
Pod间的反亲和关系
Pod间的反亲和关系(podAntiAffinity)要实现的调度目标刚好与亲和关系相反,它的主要目标在于确保存在互斥关系的Pod对象不会运行在同一位置,或者确保仅需要在指定的位置配置单个代理程序(类似于DaemonSet确保每个节点仅运行单个某类Pod)等场景应用场景。因此,反亲和性调度一般用于分散同一类应用的Pod对象等,也包括把不同安全级别的Pod对象调度至不同的区域、机架或节点等。
Pod 间亲和性与反亲和性为其 operator 字段使用 In、NotIn、Exists、 DoesNotExist 等值。
反亲和性规则表示,如果节点处于 Pod 所在的同一可用区且至少一个 Pod 具有 security=S2 标签,则该 Pod 不应被调度到该节点上。 更确切地说, 如果同一可用区中存在其他运行着带有 security=S2 标签的 Pod 节点, 并且节点具有标签 topology.kubernetes.io/zone=R,Pod 不能被调度到该节点上。
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone
containers:
- name: with-pod-affinity
image: registry.k8s.io/pause:2.0
Namespace selector
用户也可以使用 namespaceSelector 选择匹配的名字空间,namespaceSelector 是对名字空间集合进行标签查询的机制。 亲和性条件会应用到 namespaceSelector 所选择的名字空间和 namespaces 字段中所列举的名字空间之上。 注意,空的 namespaceSelector({})会匹配所有名称空间,而 null 或者空的 namespaces 列表以及 null 值 namespaceSelector 意味着“当前 Pod 的名字空间”。
Pod 间亲和性与反亲和性在与更高级别的集合(例如 ReplicaSet、StatefulSet、 Deployment 等)一起使用时,它们可能更加有用。 这些规则使得你可以配置一组工作负载,使其位于所定义的同一拓扑中; 例如优先将两个相关的 Pod 置于相同的节点上。
以一个三节点的集群为例。你使用该集群运行一个带有内存缓存(例如 Redis)的 Web 应用程序。 在此例中,还假设 Web 应用程序和内存缓存之间的延迟应尽可能低。 你可以使用 Pod 间的亲和性和反亲和性来尽可能地将该 Web 服务器与缓存并置。
在下面的 Redis 缓存 Deployment 示例中,副本上设置了标签 app=store。 podAntiAffinity 规则告诉调度器避免将多个带有 app=store 标签的副本部署到同一节点上。 因此,每个独立节点上会创建一个缓存实例。
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
spec:
selector:
matchLabels:
app: store
replicas: 3
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
下例的 Deployment 为 Web 服务器创建带有标签 app=web-store 的副本。 Pod 亲和性规则告诉调度器将每个副本放到存在标签为 app=store 的 Pod 的节点上。 Pod 反亲和性规则告诉调度器决不要在单个节点上放置多个 app=web-store 服务器。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
selector:
matchLabels:
app: web-store
replicas: 3
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: nginx:1.16-alpine
节点污点与Pod容忍度
节点亲和性 是 Pod 的一种属性,它使 Pod 被吸引到一类特定的节点 (这可能出于一种偏好,也可能是硬性要求)。 污点(Taint) 则相反——它使节点能够排斥一类特定的 Pod。
容忍度(Toleration) 是应用于 Pod 上的。容忍度允许调度器调度带有对应污点的 Pod。 容忍度允许调度但并不保证调度:作为其功能的一部分, 调度器也会评估其他参数。
污点和容忍度(Toleration)相互配合,可以用来避免 Pod 被分配到不合适的节点上。 每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的 Pod, 是不会被该节点接受的。
污点与容忍度基础概念
污点定义在节点的nodeSpec中,而容忍度定义在Pod的podSpec中,它们都是键值型数据,但又都额外支持一个效用(effect)标识,语法格式为key=value:effect,其中key和value的用法及格式与资源注解信息相似,而污点上的效用标识则用于定义其对Pod对象的排斥等级,容忍度上的效用标识则用于定义其对污点的容忍级别。效用标识主要有以下3种类型。
▪ NoSchedule:不能容忍此污点的Pod对象不可调度至当前节点,属于强制型约束关系,但添加污点对节点上现存的Pod对象不产生影响。
▪ PreferNoSchedule:NoSchedule的柔性约束版本,即调度器尽量确保不会将那些不能容忍此污点的Pod对象调度至当前节点,除非不存在其他任何能够容忍此污点的可用节点;添加该类效用的污点同样对节点上现存的Pod对象不产生影响。
▪ NoExecute:不能容忍此污点的新Pod对象不可调度至当前节点,属于强制型约束关系,而且节点上现存的Pod对象因节点污点变动或Pod容忍度变动而不再满足匹配条件时,Pod对象将会被驱逐。
此外,在Pod对象上定义容忍度时,它支持两种操作符:
一种是等值比较(Equal),表示容忍度与污点必须在key、value和effect三者之上完全匹配;
一种是存在性判断(Exists),表示二者的key和effect必须完全匹配,而容忍度中的value字段要使用空值。
说明:
存在两种特殊情况:
如果一个容忍度的 key 为空且 operator 为 Exists, 表示这个容忍度与任意的 key、value 和 effect 都匹配,即这个容忍度能容忍任何污点。
如果 effect 为空,则可以与所有键名 key1 的效果相匹配。
一个节点可以配置使用多个污点,而一个Pod对象也可以有多个容忍度,Kubernetes 处理多个污点和容忍度的过程就像一个过滤器:从一个节点的所有污点开始遍历, 过滤掉那些 Pod 中存在与之相匹配的容忍度的污点。余下未被过滤的污点的 effect 值决定了 Pod 是否会被分配到该节点。需要注意以下情况:
如果未被忽略的污点中存在至少一个 effect 值为 NoSchedule 的污点, 则 Kubernetes 不会将 Pod 调度到该节点。
如果未被忽略的污点中不存在 effect 值为 NoSchedule 的污点, 但是存在至少一个 effect 值为 PreferNoSchedule 的污点, 则 Kubernetes 会 尝试 不将 Pod 调度到该节点。
如果未被忽略的污点中存在至少一个 effect 值为 NoExecute 的污点, 则 Kubernetes 不会将 Pod 调度到该节点(如果 Pod 还未在节点上运行), 并且会将 Pod 从该节点驱逐(如果 Pod 已经在节点上运行)。
污点
定义污点
任何符合键值规范要求的字符串均可用于定义污点信息:可使用字母、数字、连接符、点号和下划线,且仅能以字母或数字开头,其中键名的长度上限为253个字符,值最长为63个字符。实践中,污点通常用于描述具体的部署规划,它们的键名形如node-type、noderole、node-project或node-geo等,而且一般还会在必要时带上域名以描述一些额外信息。kubectl taint 命令可用于管理Node对象的污点信息,该命令的语法格式如下:
# kubectl taint nodes node1 key1=value1:NoSchedule
# kubectl taint nodes node1 key1=value1:NoExecute
# kubectl taint nodes node1 key2=value2:NoSchedule
# kubectl taint nodes node1 key2=value2:PreferNoSchedule
给节点 node1 增加一个污点,它的键名是 key1,键值是 value1,效果是 NoSchedule。 这表示只有拥有和这个污点相匹配的容忍度的 Pod 才能够被分配到 node1 这个节点。
查看污点
# kubectl get nodes node1 -o jsonpath={.spec.taints}
移除单个污点
# kubectl taint nodes node1 key1=value1:NoSchedule-
# kubectl taint nodes node1 key1=value1:NoExecute-
# kubectl taint nodes node1 key2=value2:NoSchedule-
# kubectl taint nodes node1 key2=value2:PreferNoSchedule-
移除所有污点
# kubectl taint nodes node1 key1-
容忍度
定义容忍度
Pod对象的容忍度通过其spec.tolerations字段添加,根据使用的操作符不同,主要有两种可用形式:一种是与污点信息完全匹配的等值关系;另一种是判断污点信息存在性的匹配方式,它们分别使用Equal和Exists操作符表示。
下面容忍度的定义示例使用了Equal操作符,其中tolerationSeconds用于定义延迟驱逐当前Pod对象的时长。
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
tolerationSeconds: 3600
下面的示例中定义了一个使用存在性判断机制的容忍度,它表示能够容忍以key1为键名的、效用标识为NoExcute的污点。
tolerations:
- key: "key1"
operator: "Exists"
effect: "NoSchedule"
Pod 容忍度示例
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
tolerations:
- key: "example-key"
operator: "Exists"
effect: "NoSchedule"
Pod 多容忍度示例
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
基于污点的驱逐
污点的效果值 NoExecute 会影响已经在节点上运行的如下 Pod:
如果 Pod 不能忍受这类污点,Pod 会马上被驱逐。
如果 Pod 能够忍受这类污点,但是在容忍度定义中没有指定 tolerationSeconds, 则 Pod 还会一直在这个节点上运行。
如果 Pod 能够忍受这类污点,而且指定了 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 或节点控制器能够移除相关的污点。
在某些情况下,当节点不可达时,API 服务器无法与节点上的 kubelet 进行通信。 在与 API 服务器的通信被重新建立之前,删除 Pod 的决定无法传递到 kubelet。 同时,被调度进行删除的那些 Pod 可能会继续运行在分区后的节点上。
说明:
Kubernetes 会自动给 Pod 添加针对 node.kubernetes.io/not-ready 和 node.kubernetes.io/unreachable 的容忍度,且配置 tolerationSeconds=300, 除非用户自身或者某控制器显式设置此容忍度。
这些自动添加的容忍度意味着 Pod 可以在检测到对应的问题之一时,在 5 分钟内保持绑定在该节点上。
DaemonSet 中的 Pod 被创建时, 针对以下污点自动添加的 NoExecute 的容忍度将不会指定 tolerationSeconds:
node.kubernetes.io/unreachable
node.kubernetes.io/not-ready
这保证了出现上述问题时 DaemonSet 中的 Pod 永远不会被驱逐。
基于节点状态添加污点
控制平面使用节点控制器自动创建 与节点状况 对应的、效果为 NoSchedule 的污点。
调度器在进行调度时检查污点,而不是检查节点状况。这确保节点状况不会直接影响调度。 例如,如果 DiskPressure 节点状况处于活跃状态,则控制平面添加 node.kubernetes.io/disk-pressure 污点并且不会调度新的 Pod 到受影响的节点。 如果 MemoryPressure 节点状况处于活跃状态,则控制平面添加 node.kubernetes.io/memory-pressure 污点。
对于新创建的 Pod,可以通过添加相应的 Pod 容忍度来忽略节点状况。 控制平面还在具有除 BestEffort 之外的 QoS 类的 Pod 上添加 node.kubernetes.io/memory-pressure 容忍度。 这是因为 Kubernetes 将 Guaranteed 或 Burstable QoS 类中的 Pod(甚至没有设置内存请求的 Pod) 视为能够应对内存压力,而新创建的 BestEffort Pod 不会被调度到受影响的节点上。
DaemonSet 控制器自动为所有守护进程添加如下 NoSchedule 容忍度,以防 DaemonSet 崩溃:
node.kubernetes.io/memory-pressure
node.kubernetes.io/disk-pressure
node.kubernetes.io/pid-pressure (1.14 或更高版本)
node.kubernetes.io/unschedulable (1.10 或更高版本)
node.kubernetes.io/network-unavailable (只适合主机网络配置)
Pod 拓扑分布式调度
可以使用 拓扑分布约束(Topology Spread Constraints) 来控制 Pod 在集群内故障域之间的分布, 例如区域(Region)、可用区(Zone)、节点和其他用户自定义拓扑域。 这样做有助于实现高可用并提升资源利用率。
适用场景
假设你有一个最多包含二十个节点的集群,你想要运行一个自动扩缩的 工作负载,请问要使用多少个副本? 答案可能是最少 2 个 Pod,最多 15 个 Pod。 当只有 2 个 Pod 时,你倾向于这 2 个 Pod 不要同时在同一个节点上运行: 你所遭遇的风险是如果放在同一个节点上且单节点出现故障,可能会让你的工作负载下线。
除了这个基本的用法之外,还有一些高级的使用案例,能够让你的工作负载受益于高可用性并提高集群利用率。
随着你的工作负载扩容,运行的 Pod 变多,将需要考虑另一个重要问题。 假设你有 3 个节点,每个节点运行 5 个 Pod。这些节点有足够的容量能够运行许多副本; 但与这个工作负载互动的客户端分散在三个不同的数据中心(或基础设施可用区)。 现在你可能不太关注单节点故障问题,但你会注意到延迟高于自己的预期, 在不同的可用区之间发送网络流量会产生一些网络成本。
你决定在正常运营时倾向于将类似数量的副本调度 到每个基础设施可用区,且你想要该集群在遇到问题时能够自愈。
topologySpreadConstraints 资源定义
---
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
# 配置一个拓扑分布约束
topologySpreadConstraints:
- maxSkew: <integer> # 描述这些 Pod 可能被不均匀分布的程度。你必须指定此字段且该数值必须大于零。 其语义将随着 whenUnsatisfiable 的值发生变化。如果你选择 whenUnsatisfiable: DoNotSchedule,则 maxSkew 定义目标拓扑中匹配 Pod 的数量与 全局最小值(符合条件的域中匹配的最小 Pod 数量,如果符合条件的域数量小于 MinDomains 则为零) 之间的最大允许差值。例如,如果你有 3 个可用区,分别有 2、2 和 1 个匹配的 Pod,则 MaxSkew 设为 1, 且全局最小值为 1。如果你选择 whenUnsatisfiable: ScheduleAnyway,则该调度器会更为偏向能够降低偏差值的拓扑域。
minDomains: <integer> # 可选;自从 v1.25 开始成为 Beta。表示符合条件的域的最小数量。域是拓扑的一个特定实例。 符合条件的域是其节点与节点选择器匹配的域。
topologyKey: <string> # 拓扑键,用来划分拓扑结构的节点标签,在指定的键上具有相同值的节点归属为同一拓扑;必选字段。
whenUnsatisfiable: <string> # 拓扑无法满足maxSkew时采取的调度策略,默认值DoNotSchedule是一种强制约束,即不予调度至该区域;而另一可用值Schedule-Anyway则是柔性约束,无法满足约束关系时仍可将Pod放入该拓扑中。
labelSelector: <object> # Pod标签选择器,用于定义该Pod需要针对哪类Pod对象的位置来确定自身可放置的位置。
matchLabels:
app: foo
matchLabelKeys: <list> # 可选;自从 v1.27 开始成为 Beta 。是一个 Pod 标签键的列表,用于选择需要计算分布方式的 Pod 集合。 这些键用于从 Pod 标签中查找值,这些键值标签与 labelSelector 进行逻辑与运算,以选择一组已有的 Pod, 通过这些 Pod 计算新来 Pod 的分布方式。matchLabelKeys 和 labelSelector 中禁止存在相同的键。 未设置 labelSelector 时无法设置 matchLabelKeys。Pod 标签中不存在的键将被忽略。 null 或空列表意味着仅与 labelSelector 匹配。
- pod-template-hash
nodeAffinityPolicy: [Honor|Ignore] # 可选;自从 v1.26 开始成为 Beta。表示我们在计算 Pod 拓扑分布偏差时将如何处理 Pod 的 nodeAffinity/nodeSelector。 选项为:Honor:只有与 nodeAffinity/nodeSelector 匹配的节点才会包括到计算中。Ignore:nodeAffinity/nodeSelector 被忽略。所有节点均包括到计算中。如果此值为 nil,此行为等同于 Honor 策略。
nodeTaintsPolicy: [Honor|Ignore] # 可选;自从 v1.26 开始成为 Beta。 表示我们在计算 Pod 拓扑分布偏差时将如何处理节点污点。选项为:Honor:包括不带污点的节点以及污点被新 Pod 所容忍的节点。Ignore:节点污点被忽略。包括所有节点。如果此值为 null,此行为等同于 Ignore 策略。
### 其他 Pod 字段置于此处
Pod 拓扑分布式调度示例
节点信息
NAME STATUS ROLES AGE VERSION LABELS
node1 Ready <none> 4m26s v1.16.0 node=node1,zone=zoneA
node2 Ready <none> 3m58s v1.16.0 node=node2,zone=zoneA
node3 Ready <none> 3m17s v1.16.0 node=node3,zone=zoneB
node4 Ready <none> 2m43s v1.16.0 node=node4,zone=zoneB
一个拓扑分布示例
拥有一个 4 节点集群,其中标记为 foo: bar 的 3 个 Pod 分别位于 node1、node2 和 node3 中:
kind: Pod
apiVersion: v1
metadata:
name: mypod
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
containers:
- name: pause
image: registry.k8s.io/pause:3.1
从此清单看,topologyKey: zone 意味着均匀分布将只应用于存在标签键值对为 zone: <any value> 的节点 (没有 zone 标签的节点将被跳过)。如果调度器找不到一种方式来满足此约束, 则 whenUnsatisfiable: DoNotSchedule 字段告诉该调度器将新来的 Pod 保持在 pending 状态。
如果该调度器将这个新来的 Pod 放到可用区 A,则 Pod 的分布将成为 [3, 1]。 这意味着实际偏差是 2(计算公式为 3 - 1),这违反了 maxSkew: 1 的约定。 为了满足这个示例的约束和上下文,新来的 Pod 只能放到可用区 B 中的一个节点上:
或者
你可以调整 Pod 规约以满足各种要求:
将 maxSkew 更改为更大的值,例如 2,这样新来的 Pod 也可以放在可用区 A 中。
将 topologyKey 更改为 node,以便将 Pod 均匀分布在节点上而不是可用区中。 在上面的例子中,如果 maxSkew 保持为 1,则新来的 Pod 只能放到 node4 节点上。
将 whenUnsatisfiable: DoNotSchedule 更改为 whenUnsatisfiable: ScheduleAnyway, 以确保新来的 Pod 始终可以被调度(假设满足其他的调度 API)。但是,最好将其放置在匹配 Pod 数量较少的拓扑域中。 请注意,这一优先判定会与其他内部调度优先级(如资源使用率等)排序准则一起进行标准化。
多个拓扑分布示例
拥有一个 4 节点集群,其中标记为 foo: bar 的 3 个 Pod 分别位于 node1、node2 和 node3 中:
可以组合使用 2 个拓扑分布约束来控制 Pod 在节点和可用区两个维度上的分布:
kind: Pod
apiVersion: v1
metadata:
name: mypod
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
- maxSkew: 1
topologyKey: node
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
containers:
- name: pause
image: registry.k8s.io/pause:3.1
在这种情况下,为了匹配第一个约束,新的 Pod 只能放置在可用区 B 中; 而在第二个约束中,新来的 Pod 只能调度到节点 node4 上。 该调度器仅考虑满足所有已定义约束的选项,因此唯一可行的选择是放置在节点 node4 上。
有冲突的拓扑分布示例
有一个跨 2 个可用区的 3 节点集群:
kind: Pod
apiVersion: v1
metadata:
name: mypod
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
- maxSkew: 1
topologyKey: node
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
containers:
- name: pause
image: registry.k8s.io/pause:3.1
你将看到 Pod mypod 保持在 Pending 状态。 出现这种情况的原因为:为了满足第一个约束,Pod mypod 只能放置在可用区 B 中; 而在第二个约束中,Pod mypod 只能调度到节点 node2 上。 两个约束的交集将返回一个空集,且调度器无法放置该 Pod。
为了应对这种情形,你可以提高 maxSkew 的值或修改其中一个约束才能使用 whenUnsatisfiable: ScheduleAnyway。 根据实际情形,例如若你在故障排查时发现某个漏洞修复工作毫无进展,你还可能决定手动删除一个现有的 Pod。
带节点亲和性的拓扑分布示例
有一个跨可用区 A 到 C 的 5 节点集群:
而且你知道可用区 C 必须被排除在外。在这种情况下,可以按如下方式编写清单, 以便将 Pod mypod 放置在可用区 B 上,而不是可用区 C 上。 同样,Kubernetes 也会一样处理 spec.nodeSelector。
kind: Pod
apiVersion: v1
metadata:
name: mypod
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: zone
operator: NotIn
values:
- zoneC
containers:
- name: pause
image: registry.k8s.io/pause:3.1
集群级别的默认约束
为集群设置默认的拓扑分布约束也是可能的。默认拓扑分布约束在且仅在以下条件满足时才会被应用到 Pod 上:
Pod 没有在其 .spec.topologySpreadConstraints 中定义任何约束。
Pod 隶属于某个 Service、ReplicaSet、StatefulSet 或 ReplicationController。
默认约束可以设置为调度方案中 PodTopologySpread 插件参数的一部分。约束的设置采用如前所述的 API, 只是 labelSelector 必须为空。 选择算符是根据 Pod 所属的 Service、ReplicaSet、StatefulSet 或 ReplicationController 来设置的。
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints: # 可选值:[] 禁用默认选项
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
defaultingType: List
内置默认约束
defaultConstraints:
- maxSkew: 3
topologyKey: "kubernetes.io/hostname"
whenUnsatisfiable: ScheduleAnyway
- maxSkew: 5
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: ScheduleAnyway
参考文档
https://kubernetes.io/docs/concepts/scheduling-eviction/