你可以使用拓扑分布约束 (topology spread constraints) 来控制 Pod 在集群中如何跨越故障域(例如区域、可用区、节点和其他用户定义的拓扑域)进行分布。这有助于实现高可用性以及高效的资源利用。
你可以设置集群级约束作为默认值,或者为单个工作负载配置拓扑分布约束。
想象一下,你有一个最多包含 20 个节点的集群,并且你想运行一个会自动缩放副本数量的工作负载。副本数量可能少至 2 个 Pod,多至 15 个。当只有 2 个 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
topologyKey 和 whenUnsatisfiable 值,只能有一个 topologySpreadConstraint。例如,如果你定义了一个使用 topologyKey 为 "kubernetes.io/hostname" 且 whenUnsatisfiable 值为 "DoNotSchedule" 的 topologySpreadConstraint,你只能在使用了不同的 whenUnsatisfiable 值时,才能为 "kubernetes.io/hostname" topologyKey 添加另一个 topologySpreadConstraint。你可以通过运行 kubectl explain Pod.spec.topologySpreadConstraints 阅读关于此字段的更多信息,或者参考 Pod API 参考的调度部分。
你可以定义一个或多个 topologySpreadConstraints 条目,以指示 kube-scheduler 如何根据集群中现有的 Pod 来放置每个新来的 Pod。这些字段如下:
maxSkew 描述了 Pod 分布不均匀的程度。你必须指定此字段,且数值必须大于零。其语义根据 whenUnsatisfiable 的值而不同:
whenUnsatisfiable: DoNotSchedule,则 maxSkew 定义了目标拓扑中匹配的 Pod 数量与全局最小值之间的最大允许差值(全局最小值是符合条件的域中的最小匹配 Pod 数量,如果符合条件的域数量少于 MinDomains,则为 0)。例如,如果你有 3 个区域,分别有 2、2 和 1 个匹配的 Pod,且 MaxSkew 设置为 1,则全局最小值为 1。whenUnsatisfiable: ScheduleAnyway,调度器会给予有助于减少偏斜的拓扑更高的优先级。minDomains 指示了符合条件的域的最小数量。此字段是可选的。域是拓扑的特定实例。符合条件的域是那些节点匹配节点选择器的域。
minDomains 字段仅在启用了 MinDomainsInPodTopologySpread 特性门控(v1.28 起默认为开启)时可用。在较旧的 Kubernetes 集群中,它可能被显式禁用,或者该字段不可用。minDomains 的值必须大于 0。你只能在与 whenUnsatisfiable: DoNotSchedule 一起使用时指定 minDomains。minDomains 时,Pod 拓扑分布将全局最小值视为 0,然后进行 skew 的计算。全局最小值是符合条件的域中的最小匹配 Pod 数量,或者如果符合条件的域数量少于 minDomains,则为 0。minDomains 时,此值对调度没有影响。minDomains,约束的行为就像 minDomains 为 1 一样。topologyKey 是节点标签的键。具有此键且值相同的节点被视为处于同一个拓扑中。我们将拓扑的每个实例(换句话说,一个 <键, 值> 对)称为一个域。调度器将尝试把平衡数量的 Pod 放入每个域中。此外,我们将符合条件的域定义为节点满足 nodeAffinityPolicy 和 nodeTaintsPolicy 要求的域。
whenUnsatisfiable 指示如果 Pod 不满足分布约束,该如何处理:
DoNotSchedule(默认)告诉调度器不要调度该 Pod。ScheduleAnyway 告诉调度器仍然调度它,同时优先考虑那些能最小化偏斜的节点。labelSelector 用于查找匹配的 Pod。匹配此标签选择器的 Pod 会被计数,以确定其对应拓扑域中 Pod 的数量。更多详细信息,请参见标签选择器。
matchLabelKeys 是 Pod 标签键的列表,用于选择计算分布偏斜的 Pod 组。在 Pod 创建时,kube-apiserver 会使用这些键从传入的 Pod 标签中查找值,这些键值标签将与任何现有的 labelSelector 合并。禁止在 matchLabelKeys 和 labelSelector 中同时存在相同的键。当未设置 labelSelector 时,不能设置 matchLabelKeys。不存在于 Pod 标签中的键将被忽略。空列表或 null 值意味着仅根据 labelSelector 进行匹配。
matchLabelKeys 与可能直接在 Pod 上更新的标签一起使用。即使你直接编辑了在 matchLabelKeys 中指定的 Pod 标签(即你编辑的是 Pod 而不是 Deployment),kube-apiserver 也不会将标签更新反映到合并后的 labelSelector 中。使用 matchLabelKeys,你不需要在不同的修订版本之间更新 pod.spec。控制器/操作员只需要为不同的修订版本在同一个标签键下设置不同的值。例如,如果你正在配置一个 Deployment,你可以使用带有 pod-template-hash 键的标签(这是由 Deployment 控制器自动添加的),来区分单个 Deployment 中的不同修订版本。
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: foo
matchLabelKeys:
- pod-template-hash
nodeAffinityPolicy 指示在计算 Pod 拓扑分布偏斜时,如何处理 Pod 的 nodeAffinity/nodeSelector。选项包括:
如果此值为 null,则其行为等同于 Honor 策略。
nodeAffinityPolicy 在 1.26 版本进入 Beta,在 1.33 版本进入 GA。在 Beta 阶段默认启用,你可以通过禁用 NodeInclusionPolicyInPodTopologySpread 特性门控来禁用它。nodeTaintsPolicy 指示在计算 Pod 拓扑分布偏斜时,如何处理节点污点。选项包括:
如果此值为 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。调度器仅考虑满足所有已定义约束的选项,因此唯一的有效放置位置是节点 node4。
多个约束可能导致冲突。假设你有一个跨 2 个区域的 3 节点集群:
如果你要将 two-constraints.yaml(上一个示例中的清单)应用到此集群,你会看到 Pod mypod 保持在 Pending(挂起)状态。这是因为:为了满足第一个约束,Pod mypod 只能被放置在区域 B;而根据第二个约束,Pod mypod 只能调度到节点 node2。这两个约束的交集为空集,调度器无法放置该 Pod。
为了克服这种情况,你可以增加 maxSkew 的值,或者修改其中一个约束以使用 whenUnsatisfiable: ScheduleAnyway。根据具体情况,你也可以决定手动删除一个现有的 Pod——例如,如果你正在排查为什么 Bug 修复的滚动更新没有进展。
如果新来的 Pod 定义了 spec.nodeSelector 或 spec.affinity.nodeAffinity,调度器将在计算 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 才能成为匹配候选者。
调度器仅考虑同时存在所有 topologySpreadConstraints[*].topologyKey 的节点。缺少这些 topologyKeys 中任何一个的节点都会被绕过。这意味着:
maxSkew 的计算——在上面的示例中,假设节点 node1 没有标签 "zone",那么那 2 个 Pod 将被忽略,因此新来的 Pod 将被调度到区域 A。node5 具有拼写错误的标签 zone-typo: zoneC(且没有设置 zone 标签)。在节点 node5 加入集群后,它将被绕过,该工作负载的 Pod 不会被调度到那里。注意:如果新来的 Pod 的 topologySpreadConstraints[*].labelSelector 与其自身的标签不匹配,会发生什么。在上面的示例中,如果你移除了新 Pod 的标签,它仍然可以被放置在区域 B 的节点上,因为约束仍然得到满足。然而,在该放置之后,集群的不平衡程度保持不变——仍然是区域 A 拥有 2 个标记为 foo: bar 的 Pod,而区域 B 拥有 1 个标记为 foo: bar 的 Pod。如果这不是你的预期,请更新工作负载的 topologySpreadConstraints[*].labelSelector 以匹配 Pod 模板中的标签。
可以为集群设置默认的拓扑分布约束。默认拓扑分布约束仅在以下情况下应用于 Pod:
.spec.topologySpreadConstraints 中没有定义任何约束。默认约束可以作为 调度配置文件 (scheduling 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 [稳定]如果你没有为 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
在 Kubernetes 中,Pod 间亲和性 (affinity) 和反亲和性 (anti-affinity) 控制 Pod 如何相对于彼此进行调度——要么更紧凑,要么更分散。
podAffinitypodAntiAffinityrequiredDuringSchedulingIgnoredDuringExecution 模式,则每个拓扑域只能调度一个 Pod;如果你选择 preferredDuringSchedulingIgnoredDuringExecution,则无法强制执行该约束。为了获得更精细的控制,你可以指定拓扑分布约束,将 Pod 分布到不同的拓扑域中——以实现高可用性或节省成本。这也有助于平滑地进行滚动更新工作负载和副本扩容。
有关更多上下文,请查看增强提案中关于 Pod 拓扑分布约束的动机 (Motivation) 部分。
无法保证在删除 Pod 时约束保持满足。例如,缩减 Deployment 可能会导致 Pod 分布不平衡。
你可以使用诸如 Descheduler 之类的工具来重新平衡 Pod 分布。
匹配到有污点节点上的 Pod 会受到尊重。请参阅 Issue 80921。
调度器没有关于集群拥有的所有区域或其他拓扑域的先验知识。它们是根据集群中的现有节点确定的。这可能导致自动扩缩容集群中的问题,当节点池(或节点组)缩容到零节点时,你期望集群进行扩容,因为在这种情况下,除非域中至少有一个节点,否则这些拓扑域将不会被考虑。
你可以通过使用能够感知 Pod 拓扑分布约束并感知总体拓扑域集的节点自动扩缩容工具来规避此问题。
不匹配其自身 labelSelector 的 Pod 会创建“幽灵 Pod”。如果 Pod 的标签与其拓扑分布约束中的 labelSelector 不匹配,该 Pod 将不会在分布计算中计算自己。这意味着:
确保你的 Pod 标签匹配分布约束中的 labelSelector。通常,Pod 应该匹配其自身的拓扑分布约束选择器。
maxSkew,并涵盖了一些高级用法示例。