Pod 拓扑分布约束

你可以使用拓扑分布约束来控制 Pod 在你的集群中如何跨越诸如区域、可用区、节点和其他用户定义的拓扑域进行分布。这有助于实现高可用性以及高效的资源利用。

你可以设置集群级别的约束作为默认值,或者为单个工作负载配置拓扑分布约束。

动机

假设你有一个最多 20 个节点的集群,并且你想要运行一个可以自动调整其使用副本数量的工作负载。可能只有两个 Pod,也可能多达 15 个。当只有两个 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节点标签的键。具有带有此键和相同值的标签的节点被认为在同一拓扑中。我们称拓扑的每个实例(换句话说,一个 <key, value> 对)为一个域。调度程序将尝试将均衡数量的 Pod 放入每个域中。此外,我们将符合条件的域定义为满足 nodeAffinityPolicy 和 nodeTaintsPolicy 要求的节点的域。

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

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

  • matchLabelKeys 是用于选择将计算分布的 Pod 的 Pod 标签键列表。这些键用于从 Pod 标签中查找值,这些键值标签与 `labelSelector` 进行 AND 运算,以选择将计算传入 Pod 的分布的现有 Pod 组。禁止在 `matchLabelKeys` 和 `labelSelector` 中同时存在相同的键。当未设置 `labelSelector` 时,不能设置 `matchLabelKeys`。Pod 标签中不存在的键将被忽略。空列表或 null 列表表示仅与 `labelSelector` 匹配。

    使用 `matchLabelKeys`,你无需在不同修订版本之间更新 `pod.spec`。控制器/操作员只需要为不同修订版本为同一标签键设置不同的值。调度程序将根据 `matchLabelKeys` 自动假定这些值。例如,如果你正在配置 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。选项是

    • 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: <any value> 的节点(跳过没有 zone 标签的节点)。字段 whenUnsatisfiable: DoNotSchedule 告诉调度器,如果调度器找不到满足约束的方法,则让传入的 Pod 保持挂起状态。

如果调度器将此传入的 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.nodeSelectorspec.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”,则将忽略 2 个 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/v1beta3
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 插件默认情况下是禁用的。

如果你不想为你的集群使用默认的 Pod 分布约束,你可以通过将 defaultingType 设置为 List 并在 PodTopologySpread 插件配置中保留空的 defaultConstraints 来禁用这些默认值。

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
    pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints: []
          defaultingType: List

与 podAffinity 和 podAntiAffinity 的比较

在 Kubernetes 中,Pod 间亲和性和反亲和性控制 Pod 如何相互关联地调度 - 更密集地打包或更分散地分布。

podAffinity
吸引 Pod;你可以尝试将任意数量的 Pod 打包到符合条件的拓扑域中。
podAntiAffinity
排斥 Pod。如果你将其设置为 requiredDuringSchedulingIgnoredDuringExecution 模式,那么只有一个 Pod 可以被调度到单个拓扑域中;如果你选择 preferredDuringSchedulingIgnoredDuringExecution,那么你将失去强制执行约束的能力。

为了更精细的控制,你可以指定拓扑分布约束,以便将 Pod 分布在不同的拓扑域中 - 以实现高可用性或节省成本。这也有助于滚动更新工作负载和顺利扩展副本。

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

已知限制

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

    你可以使用诸如 Descheduler 之类的工具来重新平衡 Pod 的分布。

  • 与污点节点匹配的 Pod 会被尊重。请参阅Issue 80921

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

    你可以通过使用一个了解 Pod 拓扑分布约束并且也了解拓扑域整体集合的集群自动缩放工具来解决这个问题。

下一步是什么

  • 博客文章 介绍 PodTopologySpread 详细解释了 maxSkew,以及涵盖了一些高级用法示例。
  • 阅读 Pod 的 API 参考中的调度部分。
上次修改时间:2024 年 9 月 15 日下午 8:58 PST:修复 topologySpreadConstraints 中的歧义语句 (04b6fdf80c)