Pod 拓扑传播约束
您可以使用 拓扑传播约束 来控制 Pod 如何在您的集群中跨越故障域(如区域、区域、节点和其他用户定义的拓扑域)分布。这有助于实现高可用性和高效的资源利用率。
您可以设置 集群级约束 作为默认值,或为单个工作负载配置拓扑传播约束。
动机
假设您有一个最多有 20 个节点的集群,并且您想运行一个 工作负载,该工作负载会自动扩展其使用的副本数量。副本数量可能只有两个 Pod,也可能多达 15 个。当只有两个 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 之前,只有在启用了MinDomainsInPodTopologySpread特性门控(从 v1.28 开始默认启用)的情况下,才能使用minDomains字段。在较旧的 Kubernetes 集群中,它可能被明确禁用,或者该字段可能不可用。- 当指定时,
minDomains的值必须大于 0。您只能与whenUnsatisfiable: DoNotSchedule结合使用minDomains。 - 当具有匹配拓扑键的合格域的数量小于
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 标签中不存在的键将被忽略。空列表或空列表表示仅与labelSelector匹配。使用
matchLabelKeys,您无需在不同的修订版本之间更新pod.spec。控制器/操作员只需要为不同的修订版本设置同一标签键的不同值即可。调度程序会根据matchLabelKeys自动假设这些值。例如,如果您正在配置 Deployment,则可以使用带有 pod-template-hash 键的标签(由 Deployment 控制器自动添加)来区分单个 Deployment 中的不同修订版本。topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: foo matchLabelKeys: - pod-template-hashnodeAffinityPolicy 指示我们在计算 pod 拓扑传播偏差时如何处理 Pod 的 nodeAffinity/nodeSelector。选项包括
- Honor:仅包含与 nodeAffinity/nodeSelector 匹配的节点。
- Ignore:忽略 nodeAffinity/nodeSelector。所有节点都包含在内。
如果此值为 null,则行为等效于 Honor 策略。
注意
nodeAffinityPolicy是处于 Beta 阶段的字段,在 1.26 中默认启用。您可以通过禁用NodeInclusionPolicyInPodTopologySpread特性门控 来禁用它。nodeTaintsPolicy 指示我们在计算 pod 拓扑传播偏差时如何处理节点污点。选项包括
- Honor:包含没有污点的节点,以及传入 pod 具有容忍度的污点节点。
- Ignore:忽略节点污点。所有节点都包含在内。
如果此值为 null,则行为等效于 Ignore 策略。
注意
nodeTaintsPolicy是处于 Beta 阶段的字段,在 1.26 中默认启用。您可以通过禁用NodeInclusionPolicyInPodTopologySpread特性门控 来禁用它。
当一个 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 模板会为您处理这一点。如果您混合了不同的传播约束,那么 Kubernetes 将遵循该字段的 API 定义;但是,行为可能会变得更加混乱,故障排除也更不直观。
您需要一种机制来确保拓扑域(例如云提供商区域)中的所有节点都一致地标记。为了避免您需要手动标记节点,大多数集群会自动填充众所周知的标签,例如kubernetes.io/hostname。检查您的集群是否支持此功能。
拓扑传播约束示例
示例:一个拓扑传播约束
假设您有一个 4 节点集群,其中 3 个标记为foo: bar的 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 分别位于 node1、node2 和 node3 上
您可以组合两个拓扑传播约束来控制 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",那么这两个 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中没有定义任何约束。 - 它属于 Service、ReplicaSet、StatefulSet 或 ReplicationController。
默认约束可以作为调度配置文件 中PodTopologySpread 插件参数的一部分进行设置。约束使用与上面的 API相同的 API 指定,但labelSelector 必须为空。选择器是从 Pod 所属的 Service、ReplicaSet、StatefulSet 或 ReplicationController 计算出来的。
一个示例配置可能如下所示
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 [稳定]如果您没有为 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
与 pod 亲和性和 pod 反亲和性的比较
在 Kubernetes 中,Pod 间亲和性和反亲和性控制 Pod 相互之间如何调度 - 既可以更紧密地打包,也可以更分散地分布。
pod 亲和性- 吸引 Pod;您可以尝试将任意数量的 Pod 打包到合格的拓扑域中。
pod 反亲和性- 排斥 Pod。如果您将其设置为
requiredDuringSchedulingIgnoredDuringExecution模式,那么只有一个 Pod 可以调度到单个拓扑域中;如果您选择preferredDuringSchedulingIgnoredDuringExecution,那么您将失去强制执行约束的能力。
为了进行更精细的控制,您可以指定拓扑传播约束以在不同的拓扑域之间分布 Pod - 以实现高可用性或节省成本。这还有助于在滚动更新工作负载和顺利扩展副本时实现。
有关更多背景信息,请参见关于 Pod 拓扑传播约束的增强提案的 动机 部分。
已知限制
无法保证在删除 Pod 后约束仍然满足。例如,缩减部署可能会导致 Pod 分布不均衡。
您可以使用诸如 Descheduler 之类的工具重新平衡 Pod 分布。
在受污染节点上匹配的 Pod 会得到尊重。请参见 问题 80921。
调度程序事先不知道集群拥有的所有区域或其他拓扑域。它们是从集群中现有的节点中确定的。这可能会导致自动扩展集群出现问题,当节点池(或节点组)缩放到零个节点时,并且您希望集群扩展,因为在这种情况下,这些拓扑域将不会被考虑,直到它们中至少有一个节点。
您可以通过使用了解 Pod 拓扑传播约束并了解所有拓扑域的集群自动扩展工具来解决此问题。
下一步
- 博文文章 介绍 PodTopologySpread 详细解释了
maxSkew,以及一些高级使用示例。 - 阅读 API 参考中有关 Pod 的 调度 部分。