Pod 拓扑分布约束
你可以使用 拓扑分布约束(topology spread constraints) 来控制 Pod 在集群的故障域(如区域、可用区、节点以及其他用户定义的拓扑域)中如何分布。这有助于实现高可用性及高效的资源利用。
你可以设置集群级别默认约束,或者为独立的工作负载配置拓扑分布约束。
动机
假设你有一个由最多二十个节点组成的集群,并且你想运行一个工作负载,该工作负载可以自动伸缩其使用的副本数量。Pod 数量可能少至两个,多至十五个。当只有两个 Pod 时,你可能不希望这两个 Pod 都运行在同一个节点上:这样存在单个节点故障导致工作负载下线的风险。
除了这种基本用法外,还有一些高级用法示例可以帮助你的工作负载提升高可用性和集群利用率。
当你扩容并运行更多 Pod 时,另一个问题变得重要起来。假设你有三个节点,每个节点运行五个 Pod。这些节点有足够的容量运行那么多副本;然而,与此工作负载交互的客户端分布在三个不同的数据中心(或基础设施区域)。现在你对单个节点故障的担忧减少了,但你注意到延迟比你期望的要高,而且你还要为跨区域发送网络流量相关的网络费用付费。
你决定在正常操作下,你更希望每个基础设施区域中调度的副本数量相似,并且你希望集群在出现问题时能够自愈。
Pod 拓扑分布约束为你提供了一种声明式的方式来配置这一点。
topologySpreadConstraints
字段
Pod API 包含一个字段,spec.topologySpreadConstraints
。此字段的用法如下所示:
---
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
# Configure a topology spread constraint
topologySpreadConstraints:
- maxSkew: <integer>
minDomains: <integer> # optional
topologyKey: <string>
whenUnsatisfiable: <string>
labelSelector: <object>
matchLabelKeys: <list> # optional; beta since v1.27
nodeAffinityPolicy: [Honor|Ignore] # optional; beta since v1.26
nodeTaintsPolicy: [Honor|Ignore] # optional; beta since v1.26
### other Pod fields go here
你可以通过运行 kubectl explain Pod.spec.topologySpreadConstraints
或参考 Pod 的 API 参考中调度一节来阅读更多关于此字段的信息。
分布约束定义
你可以定义一个或多个 topologySpreadConstraints
条目来指示 kube-scheduler 如何根据集群中现有 Pod 的分布来放置每个传入的 Pod。这些字段是:
maxSkew 描述了 Pod 可能不均匀分布的程度。你必须指定此字段,并且数字必须大于零。其语义根据
whenUnsatisfiable
的值而不同:- 如果你选择
whenUnsatisfiable: DoNotSchedule
,则maxSkew
定义了目标拓扑中匹配 Pod 数量与 全局最小值(符合条件的域中的最小匹配 Pod 数量,如果符合条件的域数量小于 MinDomains 则为零)之间的最大允许差异。例如,如果你有 3 个区域分别有 2、2 和 1 个匹配的 Pod,MaxSkew
设置为 1,则全局最小值为 1。 - 如果你选择
whenUnsatisfiable: ScheduleAnyway
,调度器会优先考虑有助于减小偏差的拓扑。
- 如果你选择
minDomains 指示符合条件的域的最小数量。此字段是可选的。域是拓扑的特定实例。符合条件的域是其节点匹配节点选择器的域。
说明
在 Kubernetes v1.30 之前,只有当MinDomainsInPodTopologySpread
特性门控被启用(自 v1.28 起默认启用)时,minDomains
字段才可用。在较旧的 Kubernetes 集群中,它可能被显式禁用或该字段不可用。- 当指定时,
minDomains
的值必须大于 0。你只能将minDomains
与whenUnsatisfiable: DoNotSchedule
一起指定。 - 当匹配拓扑键的符合条件的域数量小于
minDomains
时,Pod 拓扑分布将全局最小值视为 0,然后执行skew
的计算。全局最小值是符合条件的域中的最小匹配 Pod 数量,如果符合条件的域数量小于minDomains
则为零。 - 当匹配拓扑键的符合条件的域数量等于或大于
minDomains
时,此值对调度没有影响。 - 如果你未指定
minDomains
,该约束的行为如同minDomains
为 1。
- 当指定时,
topologyKey 是节点标签的键。具有此键和相同值的标签的节点被认为是同一拓扑中的节点。我们将拓扑的每个实例(换句话说,一个 <key, value> 对)称为一个域。调度器将尝试在每个域中放置平衡数量的 Pod。此外,我们将符合条件的域定义为其节点满足 nodeAffinityPolicy 和 nodeTaintsPolicy 要求的域。
whenUnsatisfiable 指示如果 Pod 不满足分布约束该如何处理:
DoNotSchedule
(默认)告诉调度器不要调度它。ScheduleAnyway
告诉调度器仍然调度它,同时优先选择那些能使偏差最小化的节点。
labelSelector 用于查找匹配的 Pod。匹配此标签选择器的 Pod 将被计数,以确定其对应拓扑域中的 Pod 数量。更多详情请参阅标签选择器。
matchLabelKeys 是用于选择计算分布时所依据的 Pod 标签键列表。这些键用于从 Pod 标签中查找值,这些键值标签与
labelSelector
通过 AND 逻辑组合,以选择计算传入 Pod 分布时所依据的现有 Pod 组。同一个键不能同时存在于matchLabelKeys
和labelSelector
中。当labelSelector
未设置时,不能设置matchLabelKeys
。Pod 标签中不存在的键将被忽略。Null 或空列表表示仅匹配labelSelector
。使用
matchLabelKeys
,你无需在不同修订版本之间更新pod.spec
。控制器/操作符只需为不同修订版本设置相同标签键的不同值。调度器将根据matchLabelKeys
自动推断这些值。例如,如果你正在配置一个 Deployment,你可以使用 Deployment 控制器自动添加的键为 pod-template-hash 的标签来区分同一 Deployment 中的不同修订版本。topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: foo matchLabelKeys: - pod-template-hash
nodeAffinityPolicy 指示在计算 Pod 拓扑分布偏差时,如何处理 Pod 的 nodeAffinity/nodeSelector。选项包括:
- Honor:只有匹配 nodeAffinity/nodeSelector 的节点才包含在计算中。
- Ignore:忽略 nodeAffinity/nodeSelector。所有节点都包含在计算中。
如果此值为 null,其行为等同于 Honor 策略。
说明
nodeAffinityPolicy
在 1.26 版本中成为 Beta,并在 1.33 版本中升级到 GA。它在 Beta 版本中默认启用,你可以通过禁用NodeInclusionPolicyInPodTopologySpread
特性门控来禁用它。nodeTaintsPolicy 指示在计算 Pod 拓扑分布偏差时,如何处理节点污点。选项包括:
- Honor:不带污点的节点,以及传入 Pod 具有容忍度的带污点节点,都包含在内。
- Ignore:忽略节点污点。所有节点都包含在内。
如果此值为 null,其行为等同于 Ignore 策略。
说明
nodeTaintsPolicy
在 1.26 版本中成为 Beta,并在 1.33 版本中升级到 GA。它在 Beta 版本中默认启用,你可以通过禁用NodeInclusionPolicyInPodTopologySpread
特性门控来禁用它。
当 Pod 定义了不止一个 topologySpreadConstraint
时,这些约束使用逻辑 AND 操作组合:kube-scheduler 为传入的 Pod 寻找一个满足所有已配置约束的节点。
节点标签
拓扑分布约束依赖节点标签来标识每个节点所在的拓扑域。例如,一个节点可能具有以下标签:
region: us-east-1
zone: us-east-1a
说明
为简洁起见,本例未使用已知标签键 topology.kubernetes.io/zone
和 topology.kubernetes.io/region
。然而,建议使用这些注册的标签键,而不是此处使用的私有(未限定)标签键 region
和 zone
。
你不能对不同上下文中私有标签键的含义做出可靠的假设。
假设你有一个 4 节点集群,具有以下标签:
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
那么集群的逻辑视图如下:
一致性
你应该对同一组中的所有 Pod 设置相同的 Pod 拓扑分布约束。
通常,如果你使用像 Deployment 这样的工作负载控制器,Pod 模板会帮你处理这个问题。如果你混合使用不同的分布约束,Kubernetes 会遵循字段的 API 定义;但是,行为更有可能变得混乱,故障排除也更不直接。
你需要一种机制来确保拓扑域(如云提供商区域)中的所有节点都保持一致的标签。为了避免手动标记节点,大多数集群会自动填充已知标签,例如 kubernetes.io/hostname
。请检查你的集群是否支持此功能。
拓扑分布约束示例
示例:一个拓扑分布约束
假设你有一个 4 节点集群,其中 3 个带有 foo: bar
标签的 Pod 分别位于 node1、node2 和 node3 上。
如果你希望传入的 Pod 在区域内与现有 Pod 均匀分布,你可以使用类似以下的清单:
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: <任何值>
标签的节点(没有 zone
标签的节点将被跳过)。字段 whenUnsatisfiable: DoNotSchedule
告诉调度器,如果找不到满足约束的方式,则让传入的 Pod 保持待处理状态。
如果调度器将此传入的 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 节点集群,其中 3 个带有 foo: bar
标签的现有 Pod 分别位于 node1、node2 和 node3 上。
你可以组合两个拓扑分布约束,以按节点和区域控制 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
上。两个约束的交集返回空集,调度器无法放置 Pod。
示例:冲突的拓扑分布约束
多个约束可能导致冲突。假设你有一个 3 节点集群,跨越 2 个区域:
如果你将 two-constraints.yaml
(上一个示例中的清单)应用于**此**集群,你会看到 Pod mypod
保持在 Pending
状态。这是因为:为了满足第一个约束,Pod mypod
只能放置到区域 B
中;而就第二个约束而言,Pod mypod
只能调度到节点 node2
上。两个约束的交集返回空集,调度器无法放置 Pod。
为了克服这种情况,你可以增加 maxSkew
的值,或者修改其中一个约束以使用 whenUnsatisfiable: ScheduleAnyway
。根据具体情况,你也可以决定手动删除现有的 Pod,例如,如果你正在排查错误修复回滚未能正常进行的原因。
与节点亲和性(node affinity)和节点选择器(node selector)的交互
如果传入的 Pod 定义了 spec.nodeSelector
或 spec.affinity.nodeAffinity
,调度器在计算偏差时会跳过不匹配的节点。
示例:带节点亲和性的拓扑分布约束
假设你有一个 5 节点集群,跨越区域 A 到 C。
并且你知道区域 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 才能成为匹配候选。
调度器只考虑那些同时存在所有
topologySpreadConstraints[*].topologyKey
的节点。缺少任何这些topologyKeys
的节点将被绕过。这意味着:- 位于这些绕过节点上的任何 Pod 不影响
maxSkew
计算 - 在上面的示例中,假设节点node1
没有标签 "zone",那么这 2 个 Pod 将被忽略,因此传入的 Pod 将被调度到区域A
中。 - 传入的 Pod 没有机会调度到这类节点上 - 在上面的示例中,假设一个节点
node5
带有**打错字**的标签zone-typo: zoneC
(并且没有设置zone
标签)。节点node5
加入集群后,将被绕过,并且此工作负载的 Pod 不会调度到那里。
- 位于这些绕过节点上的任何 Pod 不影响
注意如果传入 Pod 的
topologySpreadConstraints[*].labelSelector
与其自身的标签不匹配时会发生什么。在上面的示例中,如果你移除传入 Pod 的标签,它仍然可以放置到区域B
的节点上,因为约束仍然满足。然而,放置之后,集群的不平衡程度保持不变 - 区域A
仍然有 2 个带有foo: bar
标签的 Pod,区域B
有 1 个带有foo: bar
标签的 Pod。如果这不是你期望的,请更新工作负载的topologySpreadConstraints[*].labelSelector
以匹配 Pod 模板中的标签。
集群级别默认约束
可以为集群设置默认的拓扑分布约束。仅当满足以下条件时,默认拓扑分布约束才会被应用于 Pod:
- 它在
.spec.topologySpreadConstraints
中未定义任何约束。 - 它属于 Service、ReplicaSet、StatefulSet 或 ReplicationController。
默认约束可以在调度 Profile 中作为 PodTopologySpread
插件参数的一部分进行设置。约束的指定与上面的API相同,但 labelSelector
必须为空。选择器是根据 Pod 所属的 Service、ReplicaSet、StatefulSet 或 ReplicationController 计算得出的。
一个示例配置可能如下所示:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
defaultingType: List
内置默认约束
Kubernetes v1.24 [stable]
如果你未为 Pod 拓扑分布配置任何集群级别默认约束,则 kube-scheduler 的行为就像你指定了以下默认拓扑约束一样:
defaultConstraints:
- maxSkew: 3
topologyKey: "kubernetes.io/hostname"
whenUnsatisfiable: ScheduleAnyway
- maxSkew: 5
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: ScheduleAnyway
此外,提供类似行为的旧版 SelectorSpread
插件默认被禁用。
说明
PodTopologySpread
插件不为不具有扩散约束中指定的拓扑键的节点打分。与使用默认拓扑约束时的遗留 SelectorSpread
插件相比,这可能会导致不同的默认行为。
如果你的节点预期不同时具有 kubernetes.io/hostname
和 topology.kubernetes.io/zone
标签,那么请定义你自己的约束,而不是使用 Kubernetes 默认值。
如果你不想使用集群的默认 Pod 扩散约束,可以在 PodTopologySpread
插件配置中将 defaultingType
设置为 List
并将 defaultConstraints
留空,以此禁用这些默认值
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints: []
defaultingType: List
与 podAffinity 和 podAntiAffinity 的比较
在 Kubernetes 中,Pod 间亲和性与反亲和性 控制 Pod 如何相互关联地进行调度 - 更密集或更分散。
podAffinity
- 吸引 Pods;你可以尝试将任意数量的 Pods 密集放置到符合条件的拓扑域中。
podAntiAffinity
- 排斥 Pods。如果你将此设置为
requiredDuringSchedulingIgnoredDuringExecution
模式,那么只有一个 Pod 可以被调度到单个拓扑域中;如果你选择preferredDuringSchedulingIgnoredDuringExecution
,那么你就失去了强制执行该约束的能力。
为了进行更精细的控制,你可以指定拓扑扩散约束,将 Pods 分散到不同的拓扑域中 - 以实现高可用性或节省成本。这也有助于滚动更新工作负载和平滑地扩容副本。
欲了解更多背景信息,请参阅关于 Pod 拓扑扩散约束的增强提案的动机章节。
已知限制
无法保证在 Pods 被移除后约束仍能得到满足。例如,缩减 Deployment 可能会导致 Pods 分布不平衡。
你可以使用一个工具,例如 Descheduler,来重新平衡 Pods 的分布。
污点节点上匹配到的 Pods 会被尊重。参阅 Issue 80921。
调度器事先并不知道集群中所有的可用区域或其他拓扑域。它们是根据集群中现有的节点确定的。这可能会导致在自动伸缩集群中出现问题,例如当节点池(或节点组)被缩容到零个节点,而你预期集群会进行扩容时,因为在这种情况下,这些拓扑域在其中至少存在一个节点之前将不会被考虑。
你可以通过使用一个能够感知 Pod 拓扑扩散约束,并且也能够感知完整的拓扑域集合的节点自动伸缩器来解决这个问题。
接下来
- 博客文章 Introducing PodTopologySpread 详细解释了
maxSkew
,并涵盖了一些高级使用示例。 - 阅读 Pod 的 API 参考中的调度章节。