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 寻找一个能够满足所有约束的节点。
节点标签
拓扑分布约束依赖于节点标签来标识每个节点所在的拓扑域。例如,某节点可能具有标签:
region: us-east-1
zone: us-east-1a
说明:
为了简便,此示例未使用的标签键 topology.kubernetes.io/zone
和 topology.kubernetes.io/region
。 但是,建议使用那些已注册的标签键,而不是此处使用的私有(不合格)标签键 region
和 zone
。
你无法对不同上下文之间的私有标签键的含义做出可靠的假设。
假设你有一个 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;
必须启用 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
kind: Pod
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;
必须启用 JavaScript 才能查看此页内容
如果你将 (来自上一个示例的清单)应用到这个集群,你将看到 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;
必须启用 JavaScript 才能查看此页内容
而且你知道可用区 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
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- key: zone
operator: NotIn
values:
- zoneC
containers:
- name: pause
image: registry.k8s.io/pause:3.1
隐式约定
这里有一些值得注意的隐式约定:
只有与新来的 Pod 具有相同命名空间的 Pod 才能作为匹配候选者。
调度器会忽略没有任何
topologySpreadConstraints[*].topologyKey
的节点。这意味着:- 位于这些节点上的 Pod 不影响
maxSkew
计算,在上面的例子中,假设节点node1
没有标签 “zone”, 则 2 个 Pod 将被忽略,因此新来的 Pod 将被调度到可用区A
中。 - 新的 Pod 没有机会被调度到这类节点上。在上面的例子中, 假设节点
node5
带有 拼写错误的 标签zone-typo: zoneC
(且没有设置zone
标签)。 节点node5
接入集群之后,该节点将被忽略且针对该工作负载的 Pod 不会被调度到那里。
- 位于这些节点上的 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 的行为就像你指定了以下默认拓扑约束一样:
defaultConstraints:
- maxSkew: 3
topologyKey: "kubernetes.io/hostname"
whenUnsatisfiable: ScheduleAnyway
- maxSkew: 5
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: ScheduleAnyway
此外,原来用于提供等同行为的 SelectorSpread
插件默认被禁用。
说明:
对于分布约束中所指定的拓扑键而言,PodTopologySpread
插件不会为不包含这些拓扑键的节点评分。 这可能导致在使用默认拓扑约束时,其行为与原来的 SelectorSpread
插件的默认行为不同。
如果你的节点不会 同时 设置 kubernetes.io/hostname
和 topology.kubernetes.io/zone
标签, 你应该定义自己的约束而不是使用 Kubernetes 的默认约束。
如果你不想为集群使用默认的 Pod 分布约束,你可以通过设置 defaultingType
参数为 List
, 并将 PodTopologySpread
插件配置中的 defaultConstraints
参数置空来禁用默认 Pod 分布约束:
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints: []
defaultingType: List
在 Kubernetes 中,Pod 间亲和性和反亲和性控制 Pod 彼此的调度方式(更密集或更分散)。
对于 podAffinity
:吸引 Pod;你可以尝试将任意数量的 Pod 集中到符合条件的拓扑域中。 对于 podAntiAffinity
:驱逐 Pod。如果将此设为 requiredDuringSchedulingIgnoredDuringExecution
模式, 则只有单个 Pod 可以调度到单个拓扑域;如果你选择 preferredDuringSchedulingIgnoredDuringExecution
, 则你将丢失强制执行此约束的能力。
要实现更细粒度的控制,你可以设置拓扑分布约束来将 Pod 分布到不同的拓扑域下,从而实现高可用性或节省成本。 这也有助于工作负载的滚动更新和平稳地扩展副本规模。
有关详细信息,请参阅有关 Pod 拓扑分布约束的增强倡议的 一节。
已知局限性
当 Pod 被移除时,无法保证约束仍被满足。例如,缩减某 Deployment 的规模时,Pod 的分布可能不再均衡。
你可以使用 来重新实现 Pod 分布的均衡。
具有污点的节点上匹配的 Pod 也会被统计。 参考 Issue 80921。
接下来
- 阅读针对 Pod 的 API 参考的 调度一节。