Kubernetes 拓扑调度完全实战

------ TopologyKey 分类、节点打标、亲和性/反亲和性配置与空域调度深度解析

在 Kubernetes 中,topologyKey 是控制 Pod 分布的核心杠杆。它决定了调度器如何划分"故障域",以及如何在这些域之间实现高可用(分散)高性能(集中/亲和)均匀分布

本教程将系统性地讲解:

  1. TopologyKey 的分类与选择逻辑
  2. 不同场景下的节点打标最佳实践及配置方法
  3. 核心应用 :**Pod 亲和性(Affinity) 反亲和性(Anti-Affinity)**如何配合 topologyKey 使用。
  4. 核心难点:当节点缺少标签(即处于"空"拓扑域)时,Pod 能否被调度?
  5. 完整配置模板与避坑指南

第一部分:TopologyKey 的分类与选择

topologyKey 本质上引用的是节点上的一个 Label Key。Kubernetes 调度器根据这个 Key 的值将节点分组。

1. 标准内置拓扑键(最常用)

云厂商和 K8s 默认注入的标签,适用于 90% 的场景。

Topology Key 含义 粒度 适用场景
kubernetes.io/hostname 主机名 节点级 防单点故障。确保同一服务的副本不跑在同一台物理机/VM 上。这是最基础的分散策略。
topology.kubernetes.io/zone 可用区 (AZ) 机房级 跨可用区容灾。确保副本分散在不同的可用区。即使整个机房断电,服务仍可用。
topology.kubernetes.io/region 地域 (Region) 地域级 跨地域容灾。极少用于普通应用,因为跨地域延迟高。

2. 自定义拓扑键(高级玩法)

你可以给节点打上任何自定义标签,并将其作为 topologyKey

自定义 Key 示例值 适用场景
rack rack-01, rack-02 机架感知。防止单个机架断电导致服务全挂。常用于自建 IDC 集群。
disk-type ssd, hdd 硬件隔离。确保 IO 密集型应用只调度到 SSD 节点。
gpu-model a100, v100 算力隔离。确保 AI 训练任务分散在不同型号的 GPU 节点上。

原则 :选择 topologyKey 时,问自己一个问题:"如果这个'域'挂了,我的服务会受影响吗?"


第二部分:节点打标策略与使用方法

为了让 topologyKey 生效,节点必须拥有对应的标签。

场景 1:公有云集群

操作 :通常无需手动打标,CCM 会自动注入 topology.kubernetes.io/zone

检查

复制代码
kubectl get nodes -L topology.kubernetes.io/zone

场景 2:自建物理集群(必须手动打标)

假设你有 2 个机架,需实现机架级感知。

1. 执行打标
复制代码
# 为 Rack A 的节点打标
kubectl label node node-a1 rack=rack-a
kubectl label node node-a2 rack=rack-a

# 为 Rack B 的节点打标
kubectl label node node-b1 rack=rack-b
kubectl label node node-b2 rack=rack-b
2. 验证
复制代码
kubectl get nodes -L rack

第三部分:Pod 亲和性与反亲和性如何使用 TopologyKey

这是本教程的核心补充部分。topologyKey 本身不产生动作,它必须配合 Affinity(亲和性)Anti-Affinity(反亲和性) 才能发挥作用。

1. Pod 亲和性 (Pod Affinity) ------ "我想靠近"

目的 :让新 Pod 尽量调度到与特定目标 Pod 相同的拓扑域中。

典型场景

  • 缓存加速:Web 应用 Pod 尽量靠近 Redis Cache Pod,减少网络延迟。
  • 数据本地性:计算任务尽量靠近存储数据所在的节点/机架。
配置示例:Web 应用靠近 Redis

假设 Redis Pod 运行在 zone=az-1 的节点上,我们希望 Web Pod 也尽量去 az-1

复制代码
apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  affinity:
    podAffinity:
      # 软策略:尽量靠近,如果不行也没关系
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app: redis-cache  # 目标 Pod 的标签
            topologyKey: "topology.kubernetes.io/zone" # 关键:在"可用区"级别靠近
  containers:
  - name: web
    image: nginx

调度逻辑

  1. 调度器找到所有运行着 app=redis-cache 的节点。
  2. 获取这些节点的 topology.kubernetes.io/zone 值(例如 az-1)。
  3. 优先将 web-app 调度到同样拥有 zone=az-1 标签的其他节点上。
  4. 如果 az-1 资源不足,由于是 preferred,它会调度到其他 Zone,但得分较低。

2. Pod 反亲和性 (Pod Anti-Affinity) ------ "我想远离"

目的 :让新 Pod 尽量避免特定目标 Pod 调度到同一个拓扑域中。

典型场景

  • 高可用:同一服务的多个副本分散在不同节点或可用区,防止单点故障。
  • 资源隔离:CPU 密集型任务不要和内存密集型任务挤在同一台机器。
配置示例:Prometheus 实例分散部署

我们希望 prometheus-manager 的每个副本都跑在不同的节点上。

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-manager
spec:
  replicas: 3
  template:
    spec:
      affinity:
        podAntiAffinity:
          # 硬策略:严禁两个副本在同一节点
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: prometheus-manager # 目标 Pod 就是我自己
              topologyKey: "kubernetes.io/hostname" # 关键:在"主机"级别远离
      containers:
      - name: prometheus
        image: prom/prometheus

调度逻辑

  1. 调度器检查集群中所有已存在的 app=prometheus-manager Pod。
  2. 获取它们所在节点的 kubernetes.io/hostname
  3. 禁止将新 Pod 调度到这些 hostname 对应的节点上。
  4. 如果只有 2 个节点,而你要部署 3 个副本,第 3 个 Pod 将永远 Pending (因为是 required 硬限制)。

第四部分:核心难点------"空"拓扑域的调度行为

什么是"空"拓扑域?

指节点上没有配置topologyKey 对应的标签。

关键问题:Pod 能调度到这种"无标签"节点上吗?

1. Pod 亲和性 (Affinity) 下的空域行为

策略类型 目标 Pod 位置 新节点状态 (空域) 能否调度? 原因解析
Required (硬) Node A (zone=az-1) Node C (无标签) ❌ 否 硬匹配要求值相等。null != az-1。无法进入目标域。
Preferred (软) Node A (zone=az-1) Node C (无标签) ✅ 是 软偏好不满足仅降低得分,不阻塞调度。

结论 :如果你想让 Pod 靠近 某些服务,且使用了硬亲和性,那么无标签节点是不可用的,因为它们无法匹配目标域。

2. Pod 反亲和性 (Anti-Affinity) 下的空域行为

策略类型 目标 Pod 位置 新节点状态 (空域) 能否调度? 原因解析
Required (硬) Node A (zone=az-1) Node C (无标签) ✅ 是 null != az-1。不在同一个域,所以不违反"远离"规则。
Required (硬) Node A (无标签) Node B (无标签) ❌ 否 陷阱! 所有无标签节点被视为同一个"空域"。如果 A 占了空域,B 就不能再进。
Preferred (软) 任意 任意 ✅ 是 软策略永不阻塞。

结论 :如果你想让 Pod 远离 某些服务:

  1. 如果目标在有标签节点,无标签节点是安全的(可以调度)。
  2. 如果目标也在无标签节点,无标签节点是危险的(互斥)。

3. 拓扑分布约束 (Topology Spread Constraints) 下的空域行为

策略类型 当前分布情况 新节点状态 (空域) 能否调度? 原因解析
DoNotSchedule (硬) Az-1: 2, Empty: 0 Node C (无标签) ✅ 是 调度到 Empty 有助于平衡(2->1),若 maxSkew>=1 则允许。
ScheduleAnyway (软) 任意 任意 ✅ 是 总是允许。

第五部分:完整配置模板汇总

1. 场景:Web 应用靠近 Redis 缓存(亲和性)

复制代码
affinity:
  podAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: redis
          topologyKey: "topology.kubernetes.io/zone" # 在同一可用区内靠近

2. 场景:数据库主从严格分散(反亲和性)

复制代码
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: mysql
        topologyKey: "kubernetes.io/hostname" # 严禁同一主机

3. 场景:通用微服务均匀分布(拓扑分布约束 - 推荐)

复制代码
topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: "topology.kubernetes.io/zone"
    whenUnsatisfiable: ScheduleAnyway # 软限制,保证可用性
    labelSelector:
      matchLabels:
        app: my-service

第六部分:调试与避坑指南

🕳️ 坑 1:亲和性导致 Pod 无法调度

  • 现象 :使用 required 亲和性,目标 Pod 在 zone-1,但 zone-1 资源耗尽。
  • 结果:新 Pod Pending,即使其他 Zone 有空闲资源也不会用。
  • 解决 :改为 preferred,或者确保目标 Zone 有足够资源。

🕳️ 坑 2:混合标签导致的"隐形"互斥

  • 现象:使用反亲和性,部分节点有标签,部分没有。
  • 结果:无标签节点之间互相排斥,导致调度成功率低于预期。
  • 解决统一打标。给所有节点打上默认的拓扑标签。

🛠️ 调试命令

复制代码
# 1. 查看节点标签
kubectl get nodes -L <your-topology-key>

# 2. 查看 Pod 事件
kubectl describe pod <pod-name>

# 3. 查看调度器日志(高级)
kubectl logs -n kube-system <scheduler-pod-name>

总结

  1. TopologyKey 是基础:它定义了"域"。
  2. Affinity/Anti-Affinity 是动作
    • Affinity + TopologyKey = 聚拢(靠近目标域)。
    • Anti-Affinity + TopologyKey = 分散(远离目标域)。
  3. 空域行为需谨慎
    • 亲和性 :硬模式下,空域不可用
    • 反亲和性 :硬模式下,空域可用(除非目标也在空域)。
  4. 最佳实践
    • 优先使用 TopologySpreadConstraints 进行均匀分布。
    • 使用 preferred / ScheduleAnyway 保证服务可用性。
    • 确保所有节点都有统一的拓扑标签,避免空域带来的不确定性。