本文发表已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已不再准确。

介绍 PodTopologySpread

管理 Pod 在集群中的分布是一项困难的任务。众所周知的 Kubernetes Pod affinity 和 anti-affinity 特性允许在不同的拓扑中对 Pod 的位置进行一些控制。然而,这些特性只解决了部分 Pod 分布用例:要么将无限量的 Pod 放在单一拓扑中,要么不允许两个 Pod 共存同一拓扑中。在这两种极端情况之间,通常需要将 Pod 平均分布在拓扑中,以便更好地利用集群资源和实现应用程序的高可用性。

PodTopologySpread 调度插件(最初提议命名为 EvenPodsSpread)旨在弥补这一空白。我们在 1.18 版本中将其提升到了 Beta 阶段。

API 变更

在 Pod 的 spec API 中引入了一个新的字段 topologySpreadConstraints

spec:
  topologySpreadConstraints:
  - maxSkew: <integer>
    topologyKey: <string>
    whenUnsatisfiable: <string>
    labelSelector: <object>

由于此 API 嵌入在 Pod 的 spec 中,您可以在所有高级工作负载 API 中使用此特性,例如 Deployment、DaemonSet、StatefulSet 等。

我们通过一个集群示例来理解这个 API。

API

  • labelSelector 用于查找匹配的 Pod。对于每个拓扑,我们计算匹配此标签选择器的 Pod 数量。在上面的示例中,给定 labelSelector 为 "app: foo",在 "zone1" 中匹配的数量为 2;而在 "zone2" 中匹配的数量为 0。
  • topologyKey 是定义节点标签中拓扑的关键。在上面的示例中,某些节点如果带有 "zone=zone1" 标签则被归类到 "zone1" 中;而其他节点被归类到 "zone2" 中。
  • maxSkew 描述了 Pod 分布不均的最大允许程度。在上面的示例中:
    • 如果我们将传入的 Pod 放置到 "zone1",则 "zone1" 的 skew 将变为 3("zone1" 中匹配的 Pod 为 3;全局最小匹配 Pod 数为 "zone2" 中的 0),这违反了 "maxSkew: 1" 的约束。
    • 如果传入的 Pod 被放置到 "zone2",则 "zone2" 的 skew 为 0("zone2" 中匹配的 Pod 为 1;全局最小匹配 Pod 数为其自身的 "zone2" 中的 1),这满足了 "maxSkew: 1" 的约束。请注意,skew 是针对每个符合条件的节点计算的,而不是一个全局 skew。
  • whenUnsatisfiable 指定当无法满足 "maxSkew" 时应采取的行动:
    • DoNotSchedule(默认)告诉调度器不要调度它。这是一个硬约束。
    • ScheduleAnyway 告诉调度器仍然调度它,同时优先选择能够减少 skew 的节点。这是一个软约束。

高级用法

正如特性名称 "PodTopologySpread" 所暗示的,此特性的基本用法是以绝对均匀的方式(maxSkew=1)或相对均匀的方式(maxSkew>=2)运行您的工作负载。有关更多详细信息,请参阅官方文档

除了这种基本用法之外,还有一些高级用法示例可以使您的工作负载在高可用性和集群利用率方面受益。

与 NodeSelector / NodeAffinity 一起使用

您可能已经发现我们没有一个 "topologyValues" 字段来限制 Pod 将被调度到的拓扑。默认情况下,它将搜索所有节点并按 "topologyKey" 对它们进行分组。有时这可能不是理想的情况。例如,假设有一个集群,节点标记有 "env=prod"、"env=staging" 和 "env=qa",现在您想将 Pod 平均分布到 "qa" 环境中的各个区域,这是否可能?

答案是肯定的。您可以利用 NodeSelector 或 NodeAffinity API 规范。在底层,PodTopologySpread 特性将遵守这些设置,并在满足选择器的节点中计算传播约束。

Advanced-Usage-1

如上所示,您可以指定 spec.affinity.nodeAffinity 将“搜索范围”限制为“qa”环境,在该范围内,Pod 将被调度到满足 topologySpreadConstraints 的区域。在这种情况下,是 "zone2"。

多个 TopologySpreadConstraints

理解单个 TopologySpreadConstraint 的工作原理是直观的。那么多个 TopologySpreadConstraints 的情况如何呢?在内部,每个 TopologySpreadConstraint 是独立计算的,其结果集将被合并以生成最终的结果集 - 即,合适的节点。

在下面的示例中,我们希望同时满足 2 个要求来调度一个 Pod 到集群中:

  • 将 Pod 与跨区域的 Pod 平均放置
  • 将 Pod 与跨节点的 Pod 平均放置

Advanced-Usage-2

对于第一个约束,zone1 中有 3 个 Pod,zone2 中有 2 个 Pod,因此传入的 Pod 只能放置到 zone2 中才能满足“maxSkew=1”的约束。换句话说,结果集是 nodeX 和 nodeY。

对于第二个约束,nodeB 和 nodeX 中有太多 Pod,因此传入的 Pod 只能放置到 nodeA 和 nodeY 中。

现在我们可以得出结论,唯一符合条件的节点是 nodeY - 这是 {nodeX, nodeY}(来自第一个约束)和 {nodeA, nodeY}(来自第二个约束)集合的交集。

多个 TopologySpreadConstraints 功能强大,但请务必理解与前面“NodeSelector/NodeAffinity”示例的区别:一个是独立计算结果集然后求交集;另一个是基于节点约束的过滤结果来计算 topologySpreadConstraints。

除了在所有 topologySpreadConstraints 中使用“硬”约束外,您还可以结合使用“硬”约束和“软”约束来适应更多样的集群情况。

PodTopologySpread 默认设置

PodTopologySpread 是一个 Pod 级别的 API。因此,要使用该特性,工作负载作者需要了解集群的底层拓扑,然后在 Pod spec 中为每个工作负载指定适当的 topologySpreadConstraints。虽然 Pod 级别的 API 提供了最大的灵活性,但也可以指定集群级别的默认设置。

默认的 PodTopologySpread 约束允许您为集群中的所有工作负载指定传播设置,并根据其拓扑进行调整。管理员或操作员可以在启动 kube-scheduler 时,在调度配置文件配置 API 中将这些约束指定为 PodTopologySpread 插件参数。

一个示例配置可能如下所示:

apiVersion: kubescheduler.config.k8s.io/v1alpha2
kind: KubeSchedulerConfiguration
profiles:
  pluginConfig:
  - name: PodTopologySpread
    args:
      defaultConstraints:
      - maxSkew: 1
        topologyKey: example.com/rack
        whenUnsatisfiable: ScheduleAnyway

配置默认约束时,label selectors 必须留空。kube-scheduler 将根据 Pod 所属的 Service、ReplicationController、ReplicaSet 或 StatefulSet 来推断 label selectors。Pod 始终可以通过在 PodSpec 中提供自己的约束来覆盖默认约束。

总结

PodTopologySpread 允许您使用灵活且富有表现力的 Pod 级别 API 为您的工作负载定义传播约束。过去,工作负载作者使用 Pod AntiAffinity 规则强制或提示调度器在每个拓扑域中只运行一个 Pod。相比之下,新的 PodTopologySpread 约束允许 Pod 指定 skew 级别,可以是强制(硬)的或期望(软)的。该特性可以与节点选择器(Node Selector)和节点亲和性(Node Affinity)结合使用,以限制传播范围到特定域。Pod 传播约束可以为不同的拓扑定义,例如主机名、区域、地域、机架等。

最后,集群操作员可以定义将应用于所有 Pod 的默认约束。这样,Pod 就无需了解集群的底层拓扑。