3-11-5-pod拓扑分布约束
1. 拓扑分布约束基础
你可以使用 拓扑分布约束(Topology Spread Constraints) 来控制 Pod在集群内故障域之间的分布 , 例如区域(Region)、可用区(Zone)、节点和其他用户自定义拓扑域。 这样做有助于实现高可用并提升资源利用率。
你可以将集群级约束设为默认值 , 或为个别工作负载配置拓扑分布约束。
假设你有一个最多包含二十个节点的集群 , 你想要运行一个自动扩缩的 工作负载 , 请问要使用多少个副本? 答案可能是最少 2 个 Pod , 最多 15 个 Pod。 当只有 2 个 Pod 时 , 你倾向于这 2 个 Pod 不要同时在同一个节点上运行 : 你所遭遇的风险是如果放在同一个节点上且单节点出现故障 , 可能会让你的工作负载下线。
除了这个基本的用法之外 , 还有一些高级的使用案例 , 能够让你的工作负载受益于高可用性并提高集群利用率。
随着你的工作负载扩容 , 运行的 Pod 变多 , 将需要考虑另一个重要问题。 假设你有 3 个节点 , 每个节点运行 5 个 Pod。这些节点有足够的容量能够运行许多副本。 但与这个工作负载互动的客户端分散在三个不同的数据中心(或基础设施可用区)。 现在你可能不太关注单节点故障问题 , 但你会注意到延迟高于自己的预期 , 在不同的可用区之间发送网络流量会产生一些网络成本。
你决定在正常运营时倾向于将类似数量的副本调度到每个基础设施可用区 , 且你想要该集群在遇到问题时能够自愈。
Pod 拓扑分布约束使你能够以声明的方式进行配置。
topologySpreadConstraints 字段
Pod API 包括一个 spec.topologySpreadConstraints 字段。这个字段的用法如下所示 :
---
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
# 配置一个拓扑分布约束
topologySpreadConstraints:
- maxSkew: <integer>
minDomains: <integer> # 可选;自从 v1.25 开始成为 Beta
topologyKey: <string>
whenUnsatisfiable: <string>
labelSelector: <object>
matchLabelKeys: <list> # 可选;自从 v1.27 开始成为 Beta
nodeAffinityPolicy: [Honor|Ignore] # 可选;自从 v1.26 开始成为 Beta
nodeTaintsPolicy: [Honor|Ignore] # 可选;自从 v1.26 开始成为 Beta
### 其他 Pod 字段置于此处你可以运行 kubectl explain Pod.spec.topologySpreadConstraints 。了解有关此字段的更多信息。
2. 分布约束定义
2.1 基础选项
你可以定义一个或多个 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表示符合条件的域的最小数量。此字段是可选的。域是拓扑的一个特定实例。 符合条件的域是其节点与节点选择器匹配的域。说明 :
MinDomainsInPodTopologySpread特性门控为Pod拓扑分布启用minDomains。自v1.28起 ,MinDomainsInPodTopologySpread特性门控默认被启用。 在早期的Kubernetes集群中 , 此特性门控可能被显式禁用或此字段可能不可用。指定的
minDomains值必须大于0。你可以结合whenUnsatisfiable: DoNotSchedule仅指定minDomains。当符合条件的、拓扑键匹配的域的数量小于
minDomains时 , 拓扑分布将 "全局最小值 " (global minimum) 设为0, 然后进行skew计算。 " 全局最小值 " 是一个符合条件的域中匹配Pod的最小数量 , 如果符合条件的域的数量小于minDomains, 则全局最小值为零。当符合条件的拓扑键匹配域的个数等于或大于
minDomains时 , 该值对调度没有影响。如果你未指定
minDomains, 则约束行为类似于minDomains等于 1。
topologyKey是节点标签的键。如果节点使用此键标记并且具有相同的标签值 , 则将这些节点视为处于同一拓扑域中。我们将拓扑域中 ( 即键值对 ) 的每个实例称为一个域。 调度器将尝试在每个拓扑域中放置数量均衡的Pod。 另外 , 我们将符合条件的域定义为其节点满足nodeAffinityPolicy和nodeTaintsPolicy要求的域。whenUnsatisfiable指示如果Pod不满足分布约束时如何处理 :DoNotSchedule( 默认 ) 告诉调度器不要调度。ScheduleAnyway告诉调度器仍然继续调度 , 只是根据如何能将偏差最小化来对节点进行排序。
labelSelector用于查找匹配的Pod。匹配此标签的Pod将被统计 , 以确定相应拓扑域中Pod的数量。matchLabelKeys是一个Pod标签键的列表 , 用于选择需要计算分布方式的Pod集合。 这些键用于从Pod标签中查找值 , 这些键值标签与labelSelector进行逻辑与运算 , 以选择一组已有的Pod, 通过这些Pod计算新来Pod的分布方式。matchLabelKeys和labelSelector中禁止存在相同的键。 未设置labelSelector时无法设置matchLabelKeys。Pod标签中不存在的键将被忽略。 null 或空列表意味着仅与labelSelector匹配。借助
matchLabelKeys, 你无需在变更Pod修订版本时更新pod.spec。 控制器或Operator只需要将不同修订版的标签键设为不同的值。 调度器将根据matchLabelKeys自动确定取值。例如 , 如果你正在配置一个Deployment, 则你可以使用由Deployment控制器自动添加的、以 pod-template-hash 为键的标签来区分同一个Deployment的不同修订版。topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: foo matchLabelKeys: - pod-template-hash说明 :
matchLabelKeys字段是1.27中默认启用的一个Beta级别字段。 你可以通过禁用MatchLabelKeysInPodTopologySpread特性门控来禁用此字段。nodeAffinityPolicy表示我们在计算Pod拓扑分布偏差时将如何处理Pod的nodeAffinity/nodeSelector。 选项为 :Honor: 只有与nodeAffinity/nodeSelector匹配的节点才会包括到计算中。Ignore:nodeAffinity/nodeSelector被忽略。所有节点均包括到计算中。
如果此值为
nil, 此行为等同于Honor策略。说明 :
nodeAffinityPolicy是 1.26 中默认启用的一个Beta级别字段。 你可以通过禁用NodeInclusionPolicyInPodTopologySpread特性门控来禁用此字段。nodeTaintsPolicy表示我们在计算Pod拓扑分布偏差时将如何处理节点污点。选项为 :Honor: 包括不带污点的节点以及污点被新Pod所容忍的节点。Ignore: 节点污点被忽略。包括所有节点。
如果此值为
null, 此行为等同于Ignore策略。说明 :
nodeTaintsPolicy是一个Beta级别字段 , 在1.26版本默认启用。 你可以通过禁用NodeInclusionPolicyInPodTopologySpread特性门控来禁用此字段。当
Pod定义了不止一个topologySpreadConstraint, 这些约束之间是逻辑与的关系。 kube-scheduler 会为新的 Pod 寻找一个能够满足所有约束的节点。
2.2 节点标签
拓扑分布约束依赖于节点标签来标识每个节点所在的拓扑域。 例如 , 某节点可能具有标签 :
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那么 , 从逻辑上看集群如下 :
3. 一致性
你应该为一个组中的所有 Pod 设置相同的 Pod 拓扑分布约束。
通常 , 如果你正使用一个工作负载控制器 , 例如 Deployment , 则 Pod 模板会帮你解决这个问题。 如果你混合不同的分布约束 , 则 Kubernetes 会遵循该字段的 API 定义。 但是 , 该行为可能更令人困惑 , 并且故障排除也没那么简单。
你需要一种机制来确保拓扑域 ( 例如云提供商区域 ) 中的所有节点具有一致的标签。 为了避免你需要手动为节点打标签 , 大多数集群会自动填充知名的标签 , 例如 kubernetes.io/hostname。检查你的集群是否支持此功能。
4.拓扑分布约束示例
4.1 示例 : 一个拓扑分布约束
假设你拥有一个 4 节点集群 , 其中标记为 foo: bar 的 3 个 Pod 分别位于 node1、node2 和 node3 中 :
如果你希望新来的 Pod 均匀分布在现有的可用区域 , 则可以按如下设置其清单 :
pods/topology-spread-constraints/one-constraint.yaml
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 保持在 pending 状态。
如果该调度器将这个新来的 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.2 示例 : 多个拓扑分布约束
下面的例子建立在前面例子的基础上。假设你拥有一个 4 节点集群 , 其中 3 个标记为 foo: bar 的 Pod 分别位于 node1、node2 和 node3 上 :
可以组合使用 2 个拓扑分布约束来控制 Pod 在节点和可用区两个维度上的分布 :
pods/topology-spread-constraints/two-constraints.yaml
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 上。
4.3 示例 : 有冲突的拓扑分布约束
多个约束可能导致冲突。假设有一个跨 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 , 调度器将在偏差计算中跳过不匹配的节点。
4.4 示例 : 带节点亲和性的拓扑分布约束
假设你有一个跨可用区 A 到 C 的 5 节点集群 :
而且你知道可用区 C 必须被排除在外。在这种情况下 , 可以按如下方式编写清单 , 以便将 Pod mypod 放置在可用区 B 上 , 而不是可用区 C 上。 同样 , Kubernetes 也会一样处理 spec.nodeSelector。
pods/topology-spread-constraints/one-constraint-with-nodeaffinity.yaml
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.15.隐式约定
这里有一些值得注意的隐式约定 :
只有与新来的
Pod具有相同命名空间的Pod才能作为匹配候选者。调度器会忽略没有任何
topologySpreadConstraints[*].topologyKey的节点。这意味着 :位于这些节点上的
Pod不影响maxSkew计算 , 在上面的例子中 , 假设节点node1没有标签 "zone" , 则2个Pod将被忽略 , 因此新来的Pod将被调度到可用区A中。新的
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 模板中的标签。
6. 集群级别的默认约束
为集群设置默认的拓扑分布约束也是可能的。默认拓扑分布约束在且仅在以下条件满足时才会被应用到 Pod 上 :
Pod没有在其.spec.topologySpreadConstraints中定义任何约束。Pod隶属于某个Service、ReplicaSet、StatefulSet或ReplicationController。
默认约束可以设置为调度方案中 PodTopologySpread 插件参数的一部分。约束的设置采用如前所述的 API , 只是 labelSelector 必须为空。 选择算符是根据 Pod 所属的 Service、ReplicaSet、StatefulSet 或 ReplicationController 来设置的。
配置的示例可能看起来像下面这个样子 :
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints: []
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 参数置空来禁用默认 Pod 分布约束 :
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints: []
defaultingType: List7. 比较 podAffinity 和 podAntiAffinity
在 Kubernetes 中 , Pod 间亲和性和反亲和性控制 Pod 彼此的调度方式 ( 更密集或更分散 ) 。podAffinity吸引 Pod;你可以尝试将任意数量的 Pod 集中到符合条件的拓扑域中。
podAntiAffinity驱逐 Pod。如果将此设为 requiredDuringSchedulingIgnoredDuringExecution 模式 , 则只有单个 Pod 可以调度到单个拓扑域;如果你选择 preferredDuringSchedulingIgnoredDuringExecution , 则你将丢失强制执行此约束的能力。
要实现更细粒度的控制 , 你可以设置拓扑分布约束来将 Pod 分布到不同的拓扑域下 , 从而实现高可用性或节省成本。 这也有助于工作负载的滚动更新和平稳地扩展副本规模。
8.已知局限性
当
Pod被移除时 , 无法保证约束仍被满足。例如 , 缩减某Deployment的规模时 , Pod 的分布可能不再均衡。你可以使用 Descheduler 来重新实现
Pod分布的均衡。具有污点的节点上匹配的
Pod也会被统计。该调度器不会预先知道集群拥有的所有可用区和其他拓扑域。 拓扑域由集群中存在的节点确定。在自动扩缩的集群中 , 如果一个节点池 ( 或节点组 ) 的节点数量缩减为零 , 而用户正期望其扩容时 , 可能会导致调度出现问题。 因为在这种情况下 , 调度器不会考虑这些拓扑域 , 直至这些拓扑域中至少包含有一个节点。
你可以通过使用感知
Pod拓扑分布约束并感知整个拓扑域集的集群自动扩缩工具来解决此问题。