本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

介绍 PodTopologySpread

管理 Pod 在集群中的分布是困难的。众所周知的 Kubernetes Pod 亲和性和反亲和性功能允许对 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”上的倾斜将变为 3(“zone1”中有 3 个 Pod 匹配;“zone2”中全局最小值为 0 个 Pod 匹配),这违反了“maxSkew: 1”约束。
    • 如果传入的 Pod 放置到“zone2”,则“zone2”上的倾斜为 0(“zone2”中有 1 个 Pod 匹配;“zone2”中全局最小值为 1 个 Pod 匹配),这满足了“maxSkew: 1”约束。请注意,倾斜是按每个合格节点计算的,而不是全局倾斜。
  • whenUnsatisfiable 指定当“maxSkew”无法满足时应采取的操作
    • DoNotSchedule (默认)告诉调度器不要调度它。这是一个硬约束。
    • ScheduleAnyway 告诉调度器仍然调度它,同时优先考虑减少倾斜的节点。这是一个软约束。

高级用法

正如功能名称“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 都是独立计算的,结果集将合并以生成最终结果集——即合适的节点。

在以下示例中,我们希望同时将 Pod 调度到具有 2 个要求的集群中

  • 将 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 规范中指定适当的 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

配置默认约束时,标签选择器必须留空。kube-scheduler 将根据 Pod 与 Services、ReplicationControllers、ReplicaSets 或 StatefulSets 的隶属关系推断标签选择器。Pod 始终可以通过在 PodSpec 中提供自己的约束来覆盖默认约束。

总结

PodTopologySpread 允许您使用灵活且富有表现力的 Pod 级 API 为工作负载定义传播约束。过去,工作负载作者使用 Pod 反亲和性规则来强制或提示调度器在每个拓扑域中运行单个 Pod。相比之下,新的 PodTopologySpread 约束允许 Pod 指定倾斜级别,这些级别可以是必需的(硬约束)或期望的(软约束)。该功能可以与节点选择器和节点亲和性配对使用,以将传播限制到特定域。Pod 传播约束可以为不同的拓扑(例如主机名、区域、区域、机架等)定义。

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