Pod 拓扑扩展约束

你可以使用**拓扑分布约束**来控制 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 指示合格域的最小数量。此字段是可选的。域是拓扑的特定实例。合格域是其节点匹配节点选择器的域。

    • 当指定时,`minDomains` 的值必须大于 0。你只能在 `whenUnsatisfiable: DoNotSchedule` 的情况下指定 `minDomains`。
    • 当匹配拓扑键的合格域数量小于 `minDomains` 时,Pod 拓扑分布会将全局最小值视为 0,然后执行 `skew` 的计算。全局最小值是合格域中匹配 Pod 的最小数量,如果合格域的数量小于 `minDomains`,则为零。
    • 当匹配拓扑键的合格域数量等于或大于 `minDomains` 时,此值对调度没有影响。
    • 如果你不指定 `minDomains`,则约束的行为如同 `minDomains` 为 1。
  • topologyKey节点标签的键。具有此键和相同值的标签的节点被认为是属于同一拓扑。我们将拓扑的每个实例(换句话说,一个 对)称为一个域。调度器将尝试将平衡数量的 Pod 放入每个域。此外,我们将合格域定义为其节点满足 nodeAffinityPolicy 和 nodeTaintsPolicy 要求的域。

  • whenUnsatisfiable 指示当 Pod 不满足分布约束时如何处理它:

    • DoNotSchedule(默认)告诉调度器不要调度它。
    • ScheduleAnyway 告诉调度器仍然调度它,同时优先选择最小化倾斜的节点。
  • labelSelector 用于查找匹配的 Pod。匹配此标签选择器的 Pod 将被计数,以确定其相应拓扑域中的 Pod 数量。有关更多详细信息,请参阅标签选择器

  • matchLabelKeys 是用于选择计算分布倾斜的 Pod 组的 Pod 标签键列表。在 Pod 创建时,kube-apiserver 使用这些键从传入的 Pod 标签中查找值,并且这些键值标签将与任何现有的 `labelSelector` 合并。相同的键不允许同时存在于 `matchLabelKeys` 和 `labelSelector` 中。当未设置 `labelSelector` 时,不能设置 `matchLabelKeys`。Pod 标签中不存在的键将被忽略。空列表或 null 表示仅匹配 `labelSelector`。

    使用 `matchLabelKeys`,你无需在不同的修订版本之间更新 `pod.spec`。控制器/操作员只需为不同的修订版本设置相同标签键的不同值。例如,如果你正在配置 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 策略。

  • nodeTaintsPolicy 指示在计算 Pod 拓扑分布倾斜时如何处理节点污点。选项包括:

    • Honor: 不带污点的节点,以及传入 Pod 具有容忍度的带污点节点,都包含在内。
    • Ignore: 节点污点被忽略。所有节点都包含在内。

    如果此值为 null,则行为等同于 Ignore 策略。

当一个 Pod 定义了多个 `topologySpreadConstraint` 时,这些约束通过逻辑 AND 操作组合:kube-scheduler 为传入的 Pod 寻找一个满足所有已配置约束的节点。

节点标签

拓扑分布约束依赖节点标签来识别每个节点所在的拓扑域。例如,一个节点可能具有以下标签:

  region: us-east-1
  zone: us-east-1a

假设你有一个 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

那么集群在逻辑上如下图所示:

graph TB subgraph "zoneB" n3(Node3) n4(Node4) end subgraph "zoneA" n1(Node1) n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4 k8s; class zoneA,zoneB cluster;

一致性

你应该在组中的所有 Pod 上设置相同的 Pod 拓扑分布约束。

通常,如果你使用 Deployment 等工作负载控制器,Pod 模板会为你处理此问题。如果你混合使用不同的分布约束,Kubernetes 会遵循字段的 API 定义;但是,行为更容易令人困惑,并且故障排除也更不直接。

你需要一种机制来确保拓扑域中的所有节点(例如云提供商区域)都具有一致的标签。为了避免你手动标记节点,大多数集群会自动填充知名标签,例如 `kubernetes.io/hostname`。请检查你的集群是否支持此功能。

拓扑分布约束示例

示例:一个拓扑分布约束

假设你有一个 4 节点集群,其中 3 个标记为 `foo: bar` 的 Pod 分别位于 node1、node2 和 node3:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class zoneA,zoneB cluster;

如果你希望传入的 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 保持 Pending 状态。

如果调度器将此传入 Pod 放置到区域 `A` 中,Pod 的分布将变为 `[3, 1]`。这意味着实际倾斜度为 2(计算为 `3 - 1`),这违反了 `maxSkew: 1`。为了满足此示例的约束和上下文,传入的 Pod 只能放置到区域 `B` 中的节点上:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) p4(mypod) --> n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

或者

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) p4(mypod) --> n3 n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

你可以调整 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:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

你可以组合两个拓扑分布约束来控制 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 节点集群:

graph BT subgraph "zoneB" p4(Pod) --> n3(Node3) p5(Pod) --> n3 end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n1 p3(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3,p4,p5 k8s; class zoneA,zoneB cluster;

如果你将two-constraints.yaml(上一个示例中的清单)应用到**这个**集群,你将看到 Pod `mypod` 保持在 `Pending` 状态。这是因为:为了满足第一个约束,Pod `mypod` 只能放置到区域 `B` 中;而对于第二个约束,Pod `mypod` 只能调度到节点 `node2`。两个约束的交集返回一个空集,调度器无法放置 Pod。

为了解决这种情况,你可以增加 `maxSkew` 的值,或者修改其中一个约束以使用 `whenUnsatisfiable: ScheduleAnyway`。根据情况,你也可以决定手动删除现有 Pod——例如,如果你正在排查错误修复滚动升级为何没有进展。

与节点亲和性和节点选择器的交互

如果传入的 Pod 定义了 `spec.nodeSelector` 或 `spec.affinity.nodeAffinity`,调度器将跳过不匹配的节点进行倾斜计算。

示例:带节点亲和性的拓扑分布约束

假设你有一个跨越 A 到 C 区域的 5 节点集群:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;
graph BT subgraph "zoneC" n5(Node5) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n5 k8s; class zoneC cluster;

你知道区域 `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` 的节点将被绕过。这意味着:

    1. 位于这些被绕过节点上的任何 Pod 都不影响 `maxSkew` 计算——在上面的示例中,假设节点 `node1` 没有标签 "zone",那么这两个 Pod 将被忽略,因此传入的 Pod 将被调度到区域 `A`。
    2. 传入的 Pod 没有机会调度到这种节点上——在上面的示例中,假设节点 `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` 中定义任何约束。
  • 它属于 Service、ReplicaSet、StatefulSet 或 ReplicationController。

默认约束可以作为调度配置文件中 `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` 插件默认是禁用的。

如果你不希望为集群使用默认的 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`,那么你将失去强制执行约束的能力。

为了进行更精细的控制,你可以指定拓扑分布约束,将 Pod 分布到不同的拓扑域中——以实现高可用性或成本节约。这也有助于平滑地进行工作负载的滚动更新和副本的扩缩。

更多背景信息,请参阅 Pod 拓扑分布约束增强提案的动机部分。

已知限制

  • 当 Pod 被移除时,无法保证约束仍然满足。例如,缩减 Deployment 可能会导致 Pod 分布不平衡。

    你可以使用 Descheduler 等工具来重新平衡 Pod 的分布。

  • 匹配到被污染节点上的 Pod 仍然受到尊重。请参阅 Issue 80921

  • 调度器事先不了解集群中所有的区域或其他拓扑域。它们是从集群中的现有节点确定的。这可能导致自动扩缩集群中的问题,当节点池(或节点组)扩缩到零个节点时,你期望集群扩缩,因为在这种情况下,直到这些拓扑域中至少有一个节点,它们才会被考虑。

    你可以通过使用了解 Pod 拓扑分布约束并了解整体拓扑域集的节点自动扩缩器来解决此问题。

下一步

上次修改时间:2025 年 7 月 8 日上午 7:35 (PST):fix1 (8ad8c109d1)