Pod 拓扑扩展约束
你可以使用拓扑扩展约束来控制Pod如何在集群中的故障域(例如区域、可用区、节点和其他用户定义的拓扑域)中进行扩展。这有助于实现高可用性和高效的资源利用。
你可以将集群级约束设置为默认值,或为各个工作负载配置拓扑扩展约束。
动机
假设您有一个最多 20 个节点的集群,并且您希望运行一个工作负载,该工作负载会自动调整其使用的副本数。Pod 可能少至两个,多至十五个。当只有两个 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 表示合格域的最小数量。此字段是可选的。域是拓扑的特定实例。合格域是其节点与节点选择器匹配的域。
注意
在 Kubernetes v1.30 之前,只有在启用minDomains
字段MinDomainsInPodTopologySpread
功能闸门(自 v1.28 起为默认值)时才可用。在较旧的 Kubernetes 集群中,它可能被明确禁用,或者该字段可能不可用。- 指定时,
minDomains
的值必须大于 0。您只能将minDomains
与whenUnsatisfiable: DoNotSchedule
结合使用。 - 当具有匹配拓扑键的合格域的数量少于
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 的标签,以区分单个部署中的不同版本。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
注意
为了简洁起见,此示例未使用 众所周知的 标签键 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
那么集群在逻辑上如下所示
一致性
您应该对组中的所有 Pod 设置相同的 Pod 拓扑扩展约束。
通常,如果你使用的是工作负载控制器,例如 Deployment,则 Pod 模板会为你处理这种情况。如果你混合不同的 spread 约束,则 Kubernetes 会遵循该字段的 API 定义;但是,行为更有可能变得混乱,并且故障排除也不那么直接。
你需要一种机制来确保拓扑域(例如云提供商区域)中的所有节点都得到一致的标记。为了避免你手动标记节点,大多数集群会自动填充众所周知的标签,例如 kubernetes.io/hostname
。检查你的集群是否支持此功能。
拓扑扩展约束示例
示例:一个拓扑扩展约束
假设你有一个 4 节点集群,其中标记为 foo: bar
的 3 个 Pod 分别位于 node1、node2 和 node3 中
如果你希望传入的 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
中的节点上
或
你可以调整 Pod 规范以满足各种类型的要求
- 将
maxSkew
更改为更大的值(例如2
),以便将传入的 Pod 也放置到区域A
中。 - 将
topologyKey
更改为node
,以便在节点(而不是区域)之间均匀分布 Pod。在上述示例中,如果maxSkew
保持为1
,则传入的 Pod 只可以放置到节点node4
上。 - 将
whenUnsatisfiable: DoNotSchedule
更改为whenUnsatisfiable: ScheduleAnyway
,以确保传入的 Pod 始终可调度(假设满足其他调度 API)。但是,最好将其放置到具有较少匹配 Pod 的拓扑域中。(请注意,此首选项与其他内部调度优先级(例如资源使用率)共同标准化)。
示例:多个拓扑扩散约束
此示例基于前面的示例。假设您有一个 4 节点集群,其中 3 个标记为 foo: bar
的现有 Pod 分别位于节点 1、节点 2 和节点 3 上
您可以结合两个拓扑扩散约束来控制按节点和按区域扩散 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 节点集群
如果您将 two-constraints.yaml
(前一个示例中的清单)应用到此集群,您会看到 Pod mypod
处于 Pending
状态。发生这种情况的原因是:为了满足第一个约束,Pod mypod
只能放置在区域 B
中;而对于第二个约束,Pod mypod
只能调度到节点 node2
。两个约束的交集返回一个空集,调度程序无法放置 Pod。
为了克服这种情况,您可以增加 maxSkew
的值或修改其中一个约束以使用 whenUnsatisfiable: ScheduleAnyway
。根据情况,您可能还会决定手动删除现有 Pod - 例如,如果您在排查为什么错误修复推出没有取得进展。
与节点亲和性和节点选择器的交互
如果传入 Pod 定义了 spec.nodeSelector
或 spec.affinity.nodeAffinity
,调度程序将从倾斜计算中跳过不匹配的节点。
示例:具有节点亲和性的拓扑扩展约束
假设您有一个跨越区域 A 到 C 的 5 节点集群
并且您知道必须排除区域 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
的任何节点。这意味着- 位于那些被绕过节点上的任何 Pod 不会影响
maxSkew
计算 - 在上述 示例 中,假设节点node1
没有标签“zone”,那么将忽略这 2 个 Pod,因此传入 Pod 将被调度到区域A
。 - 传入 Pod 没有机会被调度到这种类型的节点 - 在上述示例中,假设节点
node5
具有错误输入标签zone-typo: zoneC
(并且没有设置zone
标签)。在节点node5
加入集群后,它将被绕过,并且不会在此处调度此工作负载的 Pod。
- 位于那些被绕过节点上的任何 Pod 不会影响
请注意,如果传入 Pod 的
topologySpreadConstraints[*].labelSelector
与其自己的标签不匹配,将会发生什么。在上述示例中,如果您删除传入 Pod 的标签,它仍然可以被放置到区域B
中的节点,因为约束仍然得到满足。然而,在放置之后,集群的不平衡程度保持不变 - 区域A
仍然有 2 个标记为foo: bar
的 Pod,区域B
有 1 个标记为foo: bar
的 Pod。如果您不希望出现这种情况,请更新工作负载的topologySpreadConstraints[*].labelSelector
以匹配 Pod 模板中的标签。
集群级默认约束
可以为集群设置默认拓扑扩展约束。仅当满足以下条件时,才会将默认拓扑扩展约束应用到 Pod
- 它在其
.spec.topologySpreadConstraints
中未定义任何约束。 - 它属于服务、副本集、有状态集或复制控制器。
可以在 调度配置文件 中的 PodTopologySpread
插件参数中设置默认约束。这些约束使用上述相同的 API 指定,但 labelSelector
必须为空。选择器是从 Pod 所属的服务、副本集、有状态集或复制控制器计算的。
示例配置可能如下所示
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 [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
留空来禁用这些默认值
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 时无法保证约束仍然得到满足。例如,缩小部署可能会导致 Pod 分布不平衡。
你可以使用诸如 Descheduler 之类的工具来重新平衡 Pod 分布。
尊重在受污染节点上匹配的 Pod。请参阅 问题 80921。
调度程序不会预先了解集群中的所有区域或其他拓扑域。它们由集群中的现有节点决定。当节点池(或节点组)缩放到零节点时,这可能会导致自动伸缩集群出现问题,并且您希望集群进行扩展,因为在这种情况下,在至少有一个节点之前,不会考虑这些拓扑域。
您可以通过使用了解 Pod 拓扑扩展约束并了解整体拓扑域集的集群自动伸缩工具来解决此问题。
下一步
- 博客文章 Introducing PodTopologySpread 详细解释了
maxSkew
,并介绍了一些高级用法示例。 - 阅读 Pod 的 API 参考的 调度 部分。