欢迎光临

Kubernetes Topology Spread Constraints 深度指南:如何实现 Pod 跨可用区的均匀分布与高可用部署

为什么需要 Topology Spread Constraints

在 Kubernetes 集群中,默认的调度器使用一种“最优匹配”(Best Fit)策略来放置 Pod——它倾向于将新 Pod 调度到资源最充足的节点上。这种策略在资源利用率上是高效的,但在可用性上却存在严重隐患:当你的集群横跨多个可用区(Availability Zone)时,所有 Pod 可能被集中调度到同一个区域,一旦该区域发生故障,服务将全部不可用。

很多工程师的第一反应是使用

1
podAntiAffinity

来打散 Pod,但它有两个关键限制:第一,anti-affinity 只能表达“不要和这些 Pod 放在一起”,无法表达“请均匀分布”;第二,当集群规模较大时,anti-affinity 会导致调度器遍历大量已有 Pod,性能开销显著。

Topology Spread Constraints(拓扑分布约束)正是为解决这一问题而设计的。它于 Kubernetes 1.19 进入稳定版,允许你精确控制 Pod 在不同拓扑域(如节点、区域、机架)上的分布策略,实现真正意义上的均匀打散与高可用部署。

数据中心服务器机房

核心概念与 API 详解

拓扑域与分布策略

Topology Spread Constraints 的核心思想是:将集群划分为若干拓扑域(Topology Domain),然后按照你指定的策略将 Pod 尽可能均匀地分布到这些域中。每个拓扑域由一个拓扑键(topology key)标识,常见的拓扑键包括:

  • 1
    kubernetes.io/hostname

    — 每个节点就是一个拓扑域

  • 1
    topology.kubernetes.io/zone

    — 每个可用区是一个拓扑域

  • 1
    topology.kubernetes.io/region

    — 每个地理区域是一个拓扑域

  • 自定义标签(如
    1
    rack

    1
    row

    等机房内部拓扑)

Pod Spec 中的字段定义

在 Pod 的

1
spec

中,

1
topologySpreadConstraints

字段的结构如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec:
  topologySpreadConstraints:
  - maxSkew: 1                    # 最大偏斜度
    topologyKey: topology.kubernetes.io/zone  # 拓扑键
    whenUnsatisfiable: ScheduleAnyway        # 不满足时的行为
    labelSelector:                 # 匹配的 Pod 选择器
      matchLabels:
        app: my-app
    matchLabelKeys:                # 可选:匹配的标签键列表
      - app
      - version
    minDomains: 4                  # 可选:最小拓扑域数量
    nodeAffinityPolicy: Honor      # 可选:是否考虑节点亲和性
    nodeTaintsPolicy: Honor        # 可选:是否考虑节点污点

下面逐一解析每个关键字段:

字段 含义 示例
1
maxSkew
描述允许的最大偏斜度,即拓扑域之间 Pod 数量的最大差值。值越小分布越均匀
1
1

表示任意两个域的 Pod 数差不超过 1

1
topologyKey
节点标签键,用于划分拓扑域
1
topology.kubernetes.io/zone
1
whenUnsatisfiable
当约束无法满足时的行为:

1
DoNotSchedule

(阻塞调度)或

1
ScheduleAnyway

(尽量满足但允许违反)

生产环境通常用

1
DoNotSchedule
1
labelSelector
选择要计算分布的 Pod 集合 匹配同一应用的 Pod
1
minDomains
期望的最小可用拓扑域数量(K8s 1.25+ 稳定),当活跃域少于该值时,调度器会认为缺失的域有 0 个 Pod
1
3

表示至少 3 个可用区

whenUnsatisfiable 的两种模式详解

1
whenUnsatisfiable

是整个机制中最关键的选择,它决定了调度器在无法满足均匀分布时的行为。

DoNotSchedule — 严格模式

当设置为

1
DoNotSchedule

时,如果将 Pod 调度到某个域会导致

1
maxSkew

被突破,调度器将拒绝该调度,Pod 进入 Pending 状态。这意味着:

  • 你的 Pod 会等到有合适的拓扑域可用才被调度
  • 如果某个可用区故障或资源不足,新 Pod 不会“退而求其次”地全堆到其他区域
  • 这是高可用场景的推荐设置

1
2
3
4
5
6
7
8
9
# 严格模式:必须均匀分布,否则不调度
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: payment-service

ScheduleAnyway — 柔性模式

当设置为

1
ScheduleAnyway

时,调度器会尽量将 Pod 放到 Pod 数量较少的域,但如果所有优选域都无法满足,它仍然会将 Pod 调度出去。这种模式下:

  • Pod 永远不会因为拓扑约束而 Pending
  • 分布均匀性是一种“偏好”而非“硬性要求”
  • 适合可用性优先级较低的批处理任务或开发环境

1
2
3
4
5
6
7
8
9
# 柔性模式:尽量均匀,但不阻塞调度
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app: data-pipeline

服务器机房网络线缆

实战场景一:跨可用区的高可用 Web 服务

假设你管理一个生产集群横跨 3 个可用区(us-east-1a、us-east-1b、us-east-1c),需要部署一个关键的前端服务,确保任意一个区故障时仍能保持 2/3 的容量。

Deployment 配置


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: production
spec:
  replicas: 9
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
        version: v2.1.0
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: frontend
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi

9 个副本,3 个可用区,

1
maxSkew: 1

— 最终分布将是每个区正好 3 个 Pod。如果某个区因为资源不足暂时只能放 2 个,其他区最多也只能有 3 个(差值为 1),剩余的 1 个 Pod 会 Pending 等待。

同时约束节点级分布

仅跨区分布还不够——你可能还想确保同一个区内不同节点上也有分散。可以叠加第二层约束:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spec:
  topologySpreadConstraints:
  # 第一层:跨可用区均匀分布
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: frontend
  # 第二层:同一区内跨节点分布
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app: frontend

这种双层约束的效果是:首先保证跨区均匀(硬性),其次在区内尽量分散到不同节点(柔性)。两层约束的组合使用是生产环境的最佳实践。

实战场景二:StatefulSet 的拓扑约束

StatefulSet 的拓扑分布行为与 Deployment 不同——StatefulSet 的 Pod 是逐个创建的(0、1、2…),每个 Pod 按序创建,调度器计算已有 Pod 分布后再决定新 Pod 的位置。这意味着你不需要担心多个 Pod 同时被调度到同一个域的问题。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  namespace: data
spec:
  serviceName: cassandra
  replicas: 6
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: cassandra
      containers:
      - name: cassandra
        image: cassandra:4.1
        ports:
        - containerPort: 9042
        volumeClaimTemplates:
        - metadata:
            name: data
          spec:
            accessModes: ["ReadWriteOnce"]
            storageClassName: local-ssd
            resources:
              requests:
                storage: 100Gi

对于 Cassandra 这种需要跨区容灾的分布式数据库,6 个副本将均匀分布在 3 个区(每个区 2 个 Pod),单区故障不会导致数据丢失。

云计算技术

maxSkew 参数的调优策略

1
maxSkew

是你需要仔细权衡的参数,它直接影响分布的均匀性和调度的灵活性。

maxSkew=1 的含义

设为 1 表示任何两个拓扑域之间的 Pod 数量差不能超过 1。这是最均匀的分布方式,但代价是调度灵活性最低。举例:3 个区各有 3 个 Pod(共 9 个),如果你扩展到 10 个副本,第 10 个 Pod 必须 Pending 等到某个区有资源能容纳第 4 个 Pod,因为其他区已经 3 个了,如果放到已有 3 个 Pod 的区会变成 4 vs 3,差值为 1,刚好满足;但如果两个区都资源不足,Pod 就会一直 Pending。

maxSkew=2 的场景

当集群规模不均匀(某些区节点多、某些区节点少)时,

1
maxSkew: 1

可能导致大量 Pod Pending。将

1
maxSkew

放宽到 2,允许拓扑域之间有 2 个 Pod 的差距,在保持基本分布均匀的同时提供更多调度弹性。


1
2
3
4
5
6
7
8
9
# 均衡高可用与调度灵活性的选择
spec:
  topologySpreadConstraints:
  - maxSkew: 2
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: log-processor

选择建议:

  • 关键业务服务(支付、认证):maxSkew=1 + DoNotSchedule
  • 一般业务服务(内部 API):maxSkew=1 + DoNotSchedule 或 maxSkew=2 + DoNotSchedule
  • 批处理任务(数据管道):maxSkew=2 + ScheduleAnyway
  • 开发/测试环境:maxSkew=3 + ScheduleAnyway

minDomains 参数的深度运用

1
minDomains

在 Kubernetes 1.25 进入稳定版,它解决了一个长期存在的痛点:当某个拓扑域完全不可用时,调度器的行为不符合预期。

假设你有 3 个可用区,设置了

1
maxSkew: 1

+

1
DoNotSchedule

。如果其中一个区因为故障完全不可用(所有节点 NotReady),此时只剩 2 个区。6 个副本在 2 个区中分布为 3-3,符合 maxSkew=1。但如果你扩容到 7 个副本,调度器计算时发现只有 2 个活跃区,3-3 分布下第 7 个 Pod 无论放到哪边都会变成 4-3,差值为 1,仍然满足 maxSkew——看起来没问题。

但问题来了:调度器忘记了第 3 个区的存在。当第 3 个区恢复后,7 个 Pod 分布为 3-3-0 或 3-4-0,这时候偏斜度变成了 3 或 4,严重违反了均匀分布的初衷。

1
minDomains: 3

解决了这个问题——它告诉调度器“我期望有 3 个可用区”,即使某个区当前不可用,调度器在计算时仍然将它视为一个存在但 Pod 数为 0 的域。这样:


1
2
3
4
5
6
7
8
9
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    minDomains: 3              # 关键:声明期望的拓扑域数量
    labelSelector:
      matchLabels:
        app: payment-service

在上述场景中,区 3 不可用时,调度器将分布计算为 3-3-0,偏斜度为 3。此时如果

1
maxSkew: 1

,新的 Pod 将无法被调度——这正是你想要的行为,因为你不希望在第三区恢复前过度集中到前两个区。

网络架构图

与 PodAntiAffinity 的对比与选型

很多团队在遇到分布问题时首先想到

1
podAntiAffinity

,但它和 Topology Spread Constraints 解决的是不同层次的问题:

特性 PodAntiAffinity Topology Spread Constraints
目标 避免 Pod 与特定 Pod 共存 在拓扑域间均匀分布 Pod
粒度 二元:要么共存,要么不共存 连续:控制偏斜度大小
可表达性 “不要放在一起” “每个区域最多差 N 个”
调度开销 高(需遍历所有匹配 Pod) 低(仅统计拓扑域计数)
跨区分布 需要硬编码区域名 自动适配任意数量区域
弹性扩缩 大量副本时约束失效 天然支持任意副本数

一个典型错误是用 podAntiAffinity 实现跨区分布:


1
2
3
4
5
6
7
8
9
10
# 不推荐的方式:需要硬编码区域名
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        labelSelector:
          matchLabels:
            app: my-service
        topologyKey: topology.kubernetes.io/zone

这种方式只是“偏好”不在同一区,无法保证均匀分布。如果区 A 资源更多,大部分 Pod 仍会被调度到区 A。而 Topology Spread Constraints 会主动平衡计数差异。

最佳实践:用 Topology Spread Constraints 处理分布问题,用 AntiAffinity 处理互斥问题。例如:同一个 Pod 不与自身反亲和(用 antiAffinity),同一应用的多个 Pod 均匀分布到各区域(用 topologySpread)。

调度器内部机制:打分与过滤

理解调度器内部如何处理 Topology Spread Constraints,有助于你在排障时快速定位问题。

过滤阶段(Filter Phase)

对于

1
whenUnsatisfiable: DoNotSchedule

的约束,调度器在过滤阶段会检查:如果将 Pod 放到目标节点所在拓扑域,是否会导致该域的 Pod 数量超过最小域 Pod 数 + maxSkew?如果超过,该节点被过滤掉。

打分阶段(Score Phase)

对于

1
whenUnsatisfiable: ScheduleAnyway

的约束,所有节点都能通过过滤,但调度器会给 Pod 数量少的拓扑域更高的分数,引导 Pod 流向负载较轻的区域。

打分公式简化为:


1
score = (maxCount - currentDomainCount) / (maxCount - minCount + 1)

其中

1
maxCount

是所有域中最大的 Pod 数,

1
minCount

是最小的,

1
currentDomainCount

是候选节点所在域的当前 Pod 数。分数越高表示该域越“空”,越值得放入新 Pod。

与其他调度插件的交互

Topology Spread Constraints 的打分权重默认为 1,但会与以下插件协同工作:

  • NodeAffinity — 如果节点不满足亲和性,优先过滤
  • TaintToleration — 不可容忍的污点节点被过滤
  • PodTopologySpread — 计算拓扑分布分数
  • NodeResourcesFit — 资源不足的节点被过滤

1
nodeAffinityPolicy

1
nodeTaintsPolicy

的控制下,调度器可以决定在统计拓扑域 Pod 数量时是否排除不满足亲和性或不可调度的节点。默认值都是

1
Honor

,即尊重这些约束。

技术代码背景

常见排障场景与解决方案

场景一:大量 Pod Pending,事件显示拓扑约束不满足


1
2
3
4
5
6
$ kubectl describe pod frontend-7d9f8b6c4d-x2k1j
Events:
  Warning  FailedScheduling  3m  default-scheduler  0/12 nodes available:
    2 node(s) didn't match pod anti-affinity rules,
    3 node(s) had untolerated taint,
    7 Insufficient zone topology spread constraints.

这意味着某个区的 Pod 数已经达到上限(其他区 + maxSkew),且该区没有更多节点可以接受新 Pod。解决方式:

  • 检查集群各区的资源是否均衡
  • 适当增大
    1
    maxSkew
  • 确认是否有大量节点处于 NotReady 状态
  • 检查
    1
    minDomains

    是否设置了过大值

场景二:扩容后分布不均匀

如果你一次性扩容大量副本,调度器可能来不及均匀分布。这是因为 Deployment 的 ReplicaSet 是并发创建 Pod 的,多个 Pod 同时通过调度流水线,都看到了相同的拓扑计数,最终选择了同一个域。

解决方案:


1
2
3
4
5
6
7
8
9
10
11
12
13
# 方法一:使用 matchLabelKeys 自动包含版本标签
# 这样不同版本的 Pod 不会互相计入分布统计
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: frontend
    matchLabelKeys:
      - app
      - version   # 滚动更新时,新旧版本独立计算分布
1
matchLabelKeys

是 Kubernetes 1.25+ 的特性。在滚动更新期间,新 ReplicaSet 的 Pod 只统计带相同版本标签的 Pod 分布,避免了新旧 Pod 互相干扰。

场景三:区域缩容后的再平衡

当你从 3 个可用区缩减到 2 个(比如成本优化),现有 Pod 的分布不会自动调整。Topology Spread Constraints 只影响调度决策,不影响已调度的 Pod。你需要手动驱逐或使用 Descheduler:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装 Descheduler
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/descheduler/master/manifests/base/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/descheduler/master/manifests/base/configmap.yaml

# 配置 RemoveDuplicates 策略
apiVersion: v1
kind: ConfigMap
metadata:
  name: descheduler-policy-configmap
data:
  policy.yaml: |
    strategies:
      RemoveDuplicates:
        enabled: true

Descheduler 会定期检查并驱逐分布不均的 Pod,让调度器重新按拓扑约束放置。

生产环境最佳实践清单

将以上知识总结为可操作的最佳实践:

  • 始终设置双层约束:zone 级 DoNotSchedule + hostname 级 ScheduleAnyway
  • 关键服务用 maxSkew=1 + DoNotSchedule:宁可 Pending 也不要集中风险
  • 多区域集群务必设置 minDomains:防止区域故障时调度器“遗忘”缺失区域
  • 利用 matchLabelKeys 管理滚动更新:避免新旧版本互相计入分布统计
  • 不要用 antiAffinity 替代拓扑分布:两者解决不同问题,组合使用效果最佳
  • 监控分布状态:用 kubectl 和自定义指标持续观察各域 Pod 分布
  • 配置 Descheduler 作为安全网:自动纠正长期运行后的分布偏斜
  • 在资源规划时预留余量:每个区的资源总量应略高于均匀分布所需的量

快速检查分布状态的命令:


1
2
3
4
5
6
7
8
9
10
11
12
# 查看各可用区的 Pod 分布
kubectl get pods -l app=frontend -o wide \
  --no-headers | awk '{print $7}' | sort | uniq -c | sort -rn

# 查看各节点的 Pod 分布
kubectl get pods -l app=frontend -o wide \
  --no-headers | awk '{print $7}' | sort | uniq -c | sort -rn

# 一行命令快速诊断偏斜度
kubectl get pods -l app=frontend -o json \
  | jq -r '.items[] | .spec.nodeName' \
  | sort | uniq -c | sort -rn | awk 'NR==1{max=$1} END{print "maxSkew:", max-$1}'

Topology Spread Constraints 是 Kubernetes 调度体系中一个非常实用的特性,它填补了 antiAffinity 无法表达的“均匀分布”语义,使跨可用区的高可用部署变得简单而可靠。掌握它的配置和排障技巧,是每一个 Kubernetes 生产环境运维者的必备技能。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Kubernetes Topology Spread Constraints 深度指南:如何实现 Pod 跨可用区的均匀分布与高可用部署
分享到: 更多 (0)