Pod 拓扑分布约束

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

    假设你有一个最多包含二十个节点的集群,你想要运行一个自动扩缩的 工作负载,请问要使用多少个副本? 答案可能是最少 2 个 Pod,最多 15 个 Pod。 当只有 2 个 Pod 时,你倾向于这 2 个 Pod 不要同时在同一个节点上运行: 你所遭遇的风险是如果放在同一个节点上且单节点出现故障,可能会让你的工作负载下线。

    除了这个基本的用法之外,还有一些高级的使用案例,能够让你的工作负载受益于高可用性并提高集群利用率。

    随着你的工作负载扩容,运行的 Pod 变多,将需要考虑另一个重要问题。 假设你有 3 个节点,每个节点运行 5 个 Pod。这些节点有足够的容量能够运行许多副本; 但与这个工作负载互动的客户端分散在三个不同的数据中心(或基础设施可用区)。 现在你可能不太关注单节点故障问题,但你会注意到延迟高于自己的预期, 在不同的可用区之间发送网络流量会产生一些网络成本。

    你决定在正常运营时倾向于将类似数量的副本 到每个基础设施可用区,且你想要该集群在遇到问题时能够自愈。

    Pod 拓扑分布约束使你能够以声明的方式进行配置。

    topologySpreadConstraints 字段

    Pod API 包括一个 spec.topologySpreadConstraints 字段。这里有一个示例:

    你可以运行 kubectl explain Pod.spec.topologySpreadConstraints 阅读有关此字段的更多信息。

    你可以定义一个或多个 topologySpreadConstraints 条目以指导 kube-scheduler 如何将每个新来的 Pod 与跨集群的现有 Pod 相关联。这些字段包括:

    • maxSkew 描述这些 Pod 可能被均匀分布的程度。你必须指定此字段且该数值必须大于零。 其语义将随着 whenUnsatisfiable 的值发生变化:

      • 如果你选择 whenUnsatisfiable: DoNotSchedule,则 maxSkew 定义目标拓扑中匹配 Pod 的数量与 全局最小值(与拓扑域中标签选择算符匹配的最小 Pod 数量)之间的最大允许差值。 例如,如果你有 3 个可用区,分别有 2、4 和 5 个匹配的 Pod,则全局最小值为 2, 而 maxSkew 相对于该数字进行比较。
      • 如果你选择 whenUnsatisfiable: ScheduleAnyway,则该调度器会更为偏向能够降低偏差值的拓扑域。
    • minDomains 表示符合条件的域的最小数量。此字段是可选的。域是拓扑的一个特定实例。 符合条件的域是其节点与节点选择器匹配的域。

      说明: minDomains 字段是 1.24 中添加的一个 Alpha 字段。 你必须启用 MinDomainsInPodToplogySpread 特性门控,才能使用该字段。

      • 指定的 minDomains 值必须大于 0。你可以结合 whenUnsatisfiable: DoNotSchedule 仅指定 minDomains
      • 当符合条件的、拓扑键匹配的域的数量小于 minDomains 时,拓扑分布将“全局最小值”(global minimum)设为 0, 然后进行 skew 计算。“全局最小值” 是一个符合条件的域中匹配 Pod 的最小数量, 如果符合条件的域的数量小于 minDomains,则全局最小值为零。
      • 当符合条件的拓扑键匹配域的个数等于或大于 minDomains 时,该值对调度没有影响。
      • 如果你未指定 minDomains,则约束行为类似于 minDomains 等于 1。
    • topologyKey 是的键。如果两个节点使用此键标记并且具有相同的标签值, 则调度器会将这两个节点视为处于同一拓扑域中。该调度器尝试在每个拓扑域中放置数量均衡的 Pod。

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

      • DoNotSchedule(默认)告诉调度器不要调度。
      • ScheduleAnyway 告诉调度器仍然继续调度,只是根据如何能将偏差最小化来对节点进行排序。
    • labelSelector 用于查找匹配的 Pod。匹配此标签的 Pod 将被统计,以确定相应拓扑域中 Pod 的数量。 有关详细信息,请参考标签选择算符

    当 Pod 定义了不止一个 topologySpreadConstraint,这些约束之间是逻辑与的关系。 kube-scheduler 会为新的 Pod 寻找一个能够满足所有约束的节点。

    节点标签

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

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

    说明:

    为了简便,此示例未使用的标签键 topology.kubernetes.io/zonetopology.kubernetes.io/region。 但是,建议使用那些已注册的标签键,而不是此处使用的私有(不合格)标签键 regionzone

    你无法对不同上下文之间的私有标签键的含义做出可靠的假设。

    假设你有一个 4 节点的集群且带有以下标签:

    1. NAME STATUS ROLES AGE VERSION LABELS
    2. node1 Ready <none> 4m26s v1.16.0 node=node1,zone=zoneA
    3. node2 Ready <none> 3m58s v1.16.0 node=node2,zone=zoneA
    4. node3 Ready <none> 3m17s v1.16.0 node=node3,zone=zoneB
    5. 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;

    必须启用 JavaScript 才能查看此页内容

    一致性

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

    通常,如果你正使用一个工作负载控制器,例如 Deployment,则 Pod 模板会帮你解决这个问题。 如果你混合不同的分布约束,则 Kubernetes 会遵循该字段的 API 定义; 但是,该行为可能更令人困惑,并且故障排除也没那么简单。

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

    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;

    必须启用 JavaScript 才能查看此页内容

    如果你希望新来的 Pod 均匀分布在现有的可用区域,则可以按如下设置其清单:

    从此清单看,topologyKey: zone 意味着均匀分布将只应用于存在标签键值对为 zone: <any value> 的节点 (没有 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;

    必须启用 JavaScript 才能查看此页内容

    或者

    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;

    必须 JavaScript 才能查看此页内容

    你可以调整 Pod 规约以满足各种要求:

    • maxSkew 更改为更大的值,例如 2,这样新来的 Pod 也可以放在可用区 A 中。
    • 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;

    必须 JavaScript 才能查看此页内容

    可以组合使用 2 个拓扑分布约束来控制 Pod 在节点和可用区两个维度上的分布:

    pods/topology-spread-constraints/two-constraints.yaml Pod 拓扑分布约束 - 图2

    1. kind: Pod
    2. metadata:
    3. name: mypod
    4. labels:
    5. foo: bar
    6. spec:
    7. topologySpreadConstraints:
    8. - maxSkew: 1
    9. topologyKey: zone
    10. whenUnsatisfiable: DoNotSchedule
    11. labelSelector:
    12. matchLabels:
    13. foo: bar
    14. - maxSkew: 1
    15. topologyKey: node
    16. whenUnsatisfiable: DoNotSchedule
    17. labelSelector:
    18. matchLabels:
    19. foo: bar
    20. containers:
    21. - name: pause
    22. 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;

    必须启用 JavaScript 才能查看此页内容

    如果你将 (来自上一个示例的清单)应用到这个集群,你将看到 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;

    必须启用 JavaScript 才能查看此页内容

    而且你知道可用区 C 必须被排除在外。在这种情况下,可以按如下方式编写清单, 以便将 Pod mypod 放置在可用区 B 上,而不是可用区 C 上。 同样,Kubernetes 也会一样处理 spec.nodeSelector

    1. kind: Pod
    2. apiVersion: v1
    3. metadata:
    4. name: mypod
    5. labels:
    6. foo: bar
    7. spec:
    8. topologySpreadConstraints:
    9. - maxSkew: 1
    10. topologyKey: zone
    11. whenUnsatisfiable: DoNotSchedule
    12. labelSelector:
    13. matchLabels:
    14. foo: bar
    15. nodeAffinity:
    16. requiredDuringSchedulingIgnoredDuringExecution:
    17. nodeSelectorTerms:
    18. - key: zone
    19. operator: NotIn
    20. values:
    21. - zoneC
    22. containers:
    23. - name: pause
    24. image: registry.k8s.io/pause:3.1

    隐式约定

    这里有一些值得注意的隐式约定:

    • 只有与新来的 Pod 具有相同命名空间的 Pod 才能作为匹配候选者。

    • 调度器会忽略没有任何 topologySpreadConstraints[*].topologyKey 的节点。这意味着:

      1. 位于这些节点上的 Pod 不影响 maxSkew 计算,在上面的例子中,假设节点 node1 没有标签 “zone”, 则 2 个 Pod 将被忽略,因此新来的 Pod 将被调度到可用区 A 中。
      2. 新的 Pod 没有机会被调度到这类节点上。在上面的例子中, 假设节点 node5 带有 拼写错误的 标签 zone-typo: zoneC(且没有设置 zone 标签)。 节点 node5 接入集群之后,该节点将被忽略且针对该工作负载的 Pod 不会被调度到那里。
    • 注意,如果新 Pod 的 topologySpreadConstraints[*].labelSelector 与自身的标签不匹配,将会发生什么。 在上面的例子中,如果移除新 Pod 的标签,则 Pod 仍然可以放置到可用区 B 中的节点上,因为这些约束仍然满足。 然而,在放置之后,集群的不平衡程度保持不变。可用区 A 仍然有 2 个 Pod 带有标签 foo: bar, 而可用区 B 有 1 个 Pod 带有标签 foo: bar。如果这不是你所期望的, 更新工作负载的 topologySpreadConstraints[*].labelSelector 以匹配 Pod 模板中的标签。

    集群级别的默认约束

    为集群设置默认的拓扑分布约束也是可能的。默认拓扑分布约束在且仅在以下条件满足时才会被应用到 Pod 上:

    • Pod 没有在其 .spec.topologySpreadConstraints 中定义任何约束。
    • Pod 隶属于某个 Service、ReplicaSet、StatefulSet 或 ReplicationController。

    默认约束可以设置为调度方案PodTopologySpread 插件参数的一部分。约束的设置采用, 只是 labelSelector 必须为空。 选择算符是根据 Pod 所属的 Service、ReplicaSet、StatefulSet 或 ReplicationController 来设置的。

    配置的示例可能看起来像下面这个样子:

    说明: 默认配置下,SelectorSpread 插件是被禁用的。 Kubernetes 项目建议使用 PodTopologySpread 以执行类似行为。

    特性状态: Kubernetes v1.24 [stable]

    如果你没有为 Pod 拓扑分布配置任何集群级别的默认约束, kube-scheduler 的行为就像你指定了以下默认拓扑约束一样:

    1. defaultConstraints:
    2. - maxSkew: 3
    3. topologyKey: "kubernetes.io/hostname"
    4. whenUnsatisfiable: ScheduleAnyway
    5. - maxSkew: 5
    6. topologyKey: "topology.kubernetes.io/zone"
    7. whenUnsatisfiable: ScheduleAnyway

    此外,原来用于提供等同行为的 SelectorSpread 插件默认被禁用。

    说明:

    对于分布约束中所指定的拓扑键而言,PodTopologySpread 插件不会为不包含这些拓扑键的节点评分。 这可能导致在使用默认拓扑约束时,其行为与原来的 SelectorSpread 插件的默认行为不同。

    如果你的节点不会 同时 设置 kubernetes.io/hostnametopology.kubernetes.io/zone 标签, 你应该定义自己的约束而不是使用 Kubernetes 的默认约束。

    如果你不想为集群使用默认的 Pod 分布约束,你可以通过设置 defaultingType 参数为 List, 并将 PodTopologySpread 插件配置中的 defaultConstraints 参数置空来禁用默认 Pod 分布约束:

    1. apiVersion: kubescheduler.config.k8s.io/v1beta3
    2. kind: KubeSchedulerConfiguration
    3. profiles:
    4. - schedulerName: default-scheduler
    5. pluginConfig:
    6. - name: PodTopologySpread
    7. args:
    8. defaultConstraints: []
    9. defaultingType: List

    在 Kubernetes 中,Pod 间亲和性和反亲和性控制 Pod 彼此的调度方式(更密集或更分散)。

    对于 podAffinity:吸引 Pod;你可以尝试将任意数量的 Pod 集中到符合条件的拓扑域中。 对于 podAntiAffinity:驱逐 Pod。如果将此设为 requiredDuringSchedulingIgnoredDuringExecution 模式, 则只有单个 Pod 可以调度到单个拓扑域;如果你选择 preferredDuringSchedulingIgnoredDuringExecution, 则你将丢失强制执行此约束的能力。

    要实现更细粒度的控制,你可以设置拓扑分布约束来将 Pod 分布到不同的拓扑域下,从而实现高可用性或节省成本。 这也有助于工作负载的滚动更新和平稳地扩展副本规模。

    有关详细信息,请参阅有关 Pod 拓扑分布约束的增强倡议的 一节。

    已知局限性

    • 当 Pod 被移除时,无法保证约束仍被满足。例如,缩减某 Deployment 的规模时,Pod 的分布可能不再均衡。

      你可以使用 来重新实现 Pod 分布的均衡。

    • 具有污点的节点上匹配的 Pod 也会被统计。 参考 Issue 80921

    接下来

    • 阅读针对 Pod 的 API 参考的 调度一节。