Kubernetes 拓扑调度完全实战
—— TopologyKey 分类、节点打标、亲和性/反亲和性配置与空域调度深度解析
在 Kubernetes 中,topologyKey是控制 Pod 分布的核心杠杆。它决定了调度器如何划分“故障域”,以及如何在这些域之间实现高可用(分散)、高性能(集中/亲和)或均匀分布。
本教程将系统性地讲解:
- TopologyKey 的分类与选择逻辑。
- 不同场景下的节点打标最佳实践及配置方法。
- 核心应用:**Pod 亲和性(Affinity)与反亲和性(Anti-Affinity)**如何配合
topologyKey使用。 - 核心难点:当节点缺少标签(即处于“空”拓扑域)时,Pod 能否被调度?
- 完整配置模板与避坑指南。
第一部分: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-b2. 验证
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调度逻辑:
- 调度器找到所有运行着
app=redis-cache的节点。 - 获取这些节点的
topology.kubernetes.io/zone值(例如az-1)。 - 优先将
web-app调度到同样拥有zone=az-1标签的其他节点上。 - 如果
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调度逻辑:
- 调度器检查集群中所有已存在的
app=prometheus-managerPod。 - 获取它们所在节点的
kubernetes.io/hostname。 - 禁止将新 Pod 调度到这些 hostname 对应的节点上。
- 如果只有 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远离某些服务:
- 如果目标在有标签节点,无标签节点是安全的(可以调度)。
- 如果目标也在无标签节点,无标签节点是危险的(互斥)。
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>总结
- TopologyKey 是基础:它定义了“域”。
- Affinity/Anti-Affinity 是动作:
- Affinity + TopologyKey=聚拢(靠近目标域)。
- Anti-Affinity + TopologyKey=分散(远离目标域)。
- 空域行为需谨慎:
- 亲和性:硬模式下,空域不可用。
- 反亲和性:硬模式下,空域可用(除非目标也在空域)。
- 最佳实践:
- 优先使用
TopologySpreadConstraints进行均匀分布。 - 使用
preferred/ScheduleAnyway保证服务可用性。 - 确保所有节点都有统一的拓扑标签,避免空域带来的不确定性。
- 优先使用
