tags: [Kubernetes, K8s系列, DevOps, 调度, Node Affinity, Pod Affinity, Pod拓扑分布, 资源管理]
K8s 系列 | 第 16 天:Node Affinity 与 Pod Affinity:精细化调度策略实战
第 16/30 天
引言
在上一篇文章中,我们学习了污点(Taint)与容忍度(Toleration)如何通过”排斥-容忍”机制控制 Pod 调度。但污点和容忍度是一种被动式的调度策略——节点通过打污点来排斥 Pod,Pod 通过容忍度来表明自己能容忍哪些节点。那么,有没有一种主动式的调度策略,让 Pod 能够明确表达”我想被调度到什么样的节点上”呢?
答案是 Node Affinity(节点亲和性)。更进一步,Kubernetes 还提供了 Pod Affinity(Pod 亲和性/反亲和性) 和 Pod 拓扑分布约束(Pod Topology Spread Constraints),让 Pod 可以表达”我想和哪些 Pod 运行在一起””我不想和哪些 Pod 待在同一台机器上”或”我希望 Pod 在集群中均匀分布”。
本文将从基础概念到生产实战,全面解析这三类精细化调度策略。
一、核心概念
1.1 什么是 Node Affinity?
Node Affinity 是 nodeSelector 的升级版,它允许 Pod 通过节点标签(Label)来表达对节点的偏好。与 nodeSelector 的简单相等匹配不同,Node Affinity 提供了更丰富的匹配语法:
requiredDuringSchedulingIgnoredDuringExecution(硬亲和):Pod 必须调度到满足条件的节点上,否则无法调度。相当于强化的nodeSelector。preferredDuringSchedulingIgnoredDuringExecution(软亲和):调度器会尽量满足条件,但如果找不到符合条件的节点,Pod 仍然可以被调度到其他节点上。
名称中的
IgnoredDuringExecution表示:即使节点标签在 Pod 运行期间发生变化,已运行的 Pod 也不会被驱逐。Kubernetes 正在开发requiredDuringSchedulingRequiredDuringExecution模式,届时节点标签变化会导致 Pod 被重新调度。
1.2 什么是 Pod Affinity / Anti-Affinity?
Pod 亲和性(Affinity)和反亲和性(Anti-Affinity)让 Pod 根据集群中其他 Pod 的标签来决定调度位置:
- Pod Affinity:Pod 倾向于调度到”有某类 Pod”的拓扑域(节点、可用区等)中。
- Pod Anti-Affinity:Pod 倾向于避免与”某类 Pod”处于同一拓扑域。
同样分为 requiredDuringSchedulingIgnoredDuringExecution(硬性)和 preferredDuringSchedulingIgnoredDuringExecution(软性)两种。
关键参数 topologyKey 决定了”拓扑域”的粒度——可以是 kubernetes.io/hostname(节点级别)、topology.kubernetes.io/zone(可用区级别)或自定义标签。
1.3 什么是 Pod Topology Spread Constraints?
拓扑分布约束(Topology Spread Constraints)将 Pod 在集群中均匀分布到不同的拓扑域中,避免单点故障和资源倾斜。它通过 maxSkew(最大倾斜度)参数控制分布的不均衡程度。
1.4 三种调度策略的关系
| 策略 | 作用对象 | 表达方式 | 典型场景 |
|---|---|---|---|
| Node Affinity | 节点标签 | “我要什么样的节点” | GPU 节点、SSD 节点、特定区域 |
| Pod Affinity | 其他 Pod 标签 | “我要和谁在一起” | 缓存亲和、低延迟通信 |
| Pod Anti-Affinity | 其他 Pod 标签 | “我不要和谁在一起” | 高可用分散、故障隔离 |
| Topology Spread | 拓扑域分布 | “我要均匀分布” | 多可用区部署、资源均衡 |
二、实战步骤
2.1 Node Affinity 基础示例
首先,给节点打上一些标签,模拟不同的节点特征:
# 标记 GPU 节点
kubectl label nodes node-01 gpu-type=nvidia-a100
# 标记 SSD 节点
kubectl label nodes node-01 disk-type=ssd
kubectl label nodes node-02 disk-type=ssd
kubectl label nodes node-03 disk-type=hdd
# 标记可用区
kubectl label nodes node-01 topology.kubernetes.io/zone=zone-a
kubectl label nodes node-02 topology.kubernetes.io/zone=zone-a
kubectl label nodes node-03 topology.kubernetes.io/zone=zone-b
接下来,创建一个使用 Node Affinity 的 Deployment,要求 Pod 必须运行在 SSD 节点上,且优先选择 zone-a:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ml-training
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: ml-training
template:
metadata:
labels:
app: ml-training
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disk-type
operator: In
values:
- ssd
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
- weight: 20
preference:
matchExpressions:
- key: gpu-type
operator: In
values:
- nvidia-a100
containers:
- name: trainer
image: nvidia/cuda:12.2-base
command: ["sleep", "3600"]
说明:
– requiredDuringScheduling...:Pod 必须调度到 disk-type=ssd 的节点上
– preferredDuringScheduling... 有两个条目:
– weight=80:强烈偏好 zone-a(权重高)
– weight=20:如果 zone-a 的 SSD 节点资源不足,次选有 GPU 的节点
– 调度器会计算每个节点的总权重得分,选择得分最高的节点
2.2 Node Affinity 支持的匹配操作符
Node Affinity 支持多种 operator,比 nodeSelector 丰富得多:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
# In: 标签值在列表中
- key: disk-type
operator: In
values: [ssd, nvme]
# NotIn: 标签值不在列表中
- key: disk-type
operator: NotIn
values: [hdd]
# Exists: 节点存在该标签(无论值是什么)
- key: gpu-type
operator: Exists
# DoesNotExist: 节点不存在该标签
- key: spot-instance
operator: DoesNotExist
# Gt / Lt: 数值比较(大于/小于)
- key: node-memory-gb
operator: Gt
values: ["64"]
注意:
nodeSelectorTerms中的多个matchExpressions是与(AND)关系——所有条件必须同时满足。但nodeSelectorTerms数组中的多个条目是或(OR)关系——满足任意一个条目即可。
2.3 Pod Affinity:让缓存和计算紧密耦合
假设我们有一个 Redis 缓存 Pod 和一个计算 Pod,希望计算 Pod 尽量与缓存 Pod 在同一节点(减少网络延迟):
先部署 Redis Pod:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: redis-cache
labels:
app: cache
component: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
EOF
然后部署计算 Pod,使用 Pod Affinity 靠近 Redis:
apiVersion: apps/v1
kind: Deployment
metadata:
name: compute-worker
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: compute
template:
metadata:
labels:
app: compute
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname
containers:
- name: worker
image: alpine:3.19
command: ["sleep", "3600"]
这告诉调度器:计算 Pod 尽量(软性)与标签 app=cache 的 Pod 位于同一节点上。
2.4 Pod Anti-Affinity:实现高可用分散部署
对于 Web 前端等高可用应用,我们希望 Pod 分散在不同节点上,避免单点故障:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-frontend
namespace: default
spec:
replicas: 5
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
使用 podAntiAffinity + topologyKey: kubernetes.io/hostname,调度器会尽量将每个 app=web 的 Pod 分布到不同节点上。当 replicas=5 但只有 3 个节点时,前 3 个 Pod 各占一个节点,后 2 个 Pod 回退到已有 Pod 的节点上(因为是软亲和)。
如果需要严格保证(硬性),改为 requiredDuringSchedulingIgnoredDuringExecution——但这时如果 replicas > 节点数,多余的 Pod 会处于 Pending 状态,5 副本在 3 节点集群中只能调度 3 个。
2.5 Pod Topology Spread Constraints:多可用区均匀分布
在生产环境中,集群通常跨多个可用区(Availability Zone)。我们希望关键应用在每个可用区均匀分布,提升容灾能力:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 6
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-server
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api-server
containers:
- name: api
image: nginx:1.25-alpine
参数说明:
| 参数 | 含义 | 示例值 |
|---|---|---|
maxSkew |
任意两个拓扑域之间的最大 Pod 数量差异 | 1 表示尽量完全平均 |
topologyKey |
按哪个标签划分拓扑域 | 可用区/节点/自定义标签 |
whenUnsatisfiable |
无法满足约束时的行为 | DoNotSchedule(阻塞)/ ScheduleAnyway(尽力而为) |
labelSelector |
计数哪些 Pod 参与均匀分布 | 通常匹配应用本身的标签 |
上例中:
– 第一层约束(zone 级):maxSkew=1, DoNotSchedule—严格确保每个可用区 Pod 数相差不超过 1,3 个可用区 × 各 2 个副本 = 6 副本
– 第二层约束(node 级):maxSkew=1, ScheduleAnyway—在每个可用区内尽量分散到不同节点
2.6 综合实战:生产级微服务部署
结合所有调度策略,部署一个生产级的微服务:
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: production
spec:
replicas: 4
selector:
matchLabels:
app: payment
template:
metadata:
labels:
app: payment
tier: backend
spec:
affinity:
# 1. Node Affinity:只能调度到生产环境的 SSD 节点
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: environment
operator: In
values:
- production
- key: disk-type
operator: In
values:
- ssd
# 2. Pod Anti-Affinity:支付服务副本分散到不同节点(高可用)
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- payment
topologyKey: kubernetes.io/hostname
# 3. Pod Affinity:与数据库尽量同区域
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- payment-db
topologyKey: topology.kubernetes.io/zone
# 4. Topology Spread:跨可用区均匀分布
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: payment
containers:
- name: payment
image: myregistry/payment-service:v2.1
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
这个配置同时使用了全部四种调度策略,是典型的生产级配置。调度器的决策流程如下:
- 过滤(Filtering):只保留
environment=production且disk-type=ssd的节点 - 打分(Scoring):计算每个候选节点的得分
- 尽量与
app=payment的 Pod 不在同一节点(+100 分) - 尽量与
app=payment-db在同一可用区(+80 分) - 尽量跨可用区均匀分布
- 选择:选择总分最高的节点
三、常见问题
3.1 Required Pod Anti-Affinity 导致 Pod 无法调度怎么办?
这是最常见的坑。假设 3 节点集群、replicas=6、硬反亲和 topologyKey: kubernetes.io/hostname,调度器只能调度 3 个 Pod(每个节点一个),剩下 3 个永远 Pending。
解决方案:
1. 改用 preferredDuringScheduling(软性)——能分散就分散,不能也不阻塞
2. 减少 replicas 到 ≤ 节点数
3. 增加节点数
4. 结合拓扑分布约束(Topology Spread Constraints),不同拓扑域粒度更有弹性
3.2 Node Affinity 和 nodeSelector 能一起用吗?
可以。如果同时指定了 nodeSelector 和 nodeAffinity.requiredDuringScheduling...,调度器会要求两者都满足(AND 关系)。但实践中推荐统一使用 Node Affinity,因为它的表达能力更强。
3.3 Pod Affinity 的性能影响
Pod Affinity/Anti-Affinity 的调度计算开销比 Node Affinity 大得多,因为调度器需要为每个候选节点查询该节点上运行的所有 Pod 的标签。对于大规模集群(1000+ 节点),这可能显著延长调度时间。
优化建议:
– 优先使用 Node Affinity(基于节点标签,O(1) 查询)
– Pod Affinity 尽量使用 preferredDuringScheduling
– 避免在大型 Deployment 上使用 requiredDuringScheduling 的 Pod Anti-Affinity
3.4 topologyKey 必须是什么?
topologyKey 理论上可以使用任何节点标签,但以下 topologyKey 是标准的:
| topologyKey | 语义 | 典型值 |
|---|---|---|
kubernetes.io/hostname |
节点级别 | node-01, node-02 |
topology.kubernetes.io/zone |
可用区 | us-east-1a, us-east-1b |
topology.kubernetes.io/region |
地域 | us-east-1, cn-north-1 |
⚠️
podAntiAffinity使用requiredDuringScheduling+topologyKey: kubernetes.io/hostname在大型集群中可能遇到调度器性能瓶颈。Kubernetes v1.27+ 引入了matchLabelKeys来优化匹配效率。
四、生产最佳实践
4.1 调度策略的选择优先级
在决定使用哪种调度策略时,建议遵循以下优先级:
- Taint & Toleration:防御性策略——阻止不兼容的 Pod 调度到特殊节点(GPU、专用硬件)
- Node Affinity:主动选择——Pod 明确声明需要的节点特征
- Topology Spread Constraints:分布策略——确保跨拓扑域均匀分布
- Pod Anti-Affinity:隔离策略——避免关键服务集中
- Pod Affinity:耦合策略——让互相依赖的服务靠近
4.2 调试调度决策
当 Pod 无法调度时,使用以下命令调试:
# 查看调度事件
kubectl describe pod <pod-name> | grep -A 10 Events
# 查看节点标签
kubectl get nodes --show-labels
# 查看节点上的 Pod 分布
kubectl get pods -o wide --all-namespaces | grep $(kubectl get nodes -o name | head -1)
# 模拟调度(kube-scheduler 的日志级别)
# 在 scheduler 启动参数中添加 --v=5
4.3 与 Cluster Autoscaler 的配合
当使用 Cluster Autoscaler(集群自动扩缩)时,Node Affinity 会影响扩容决策:
requiredDuringScheduling约束会传递给 Cluster Autoscaler,使其创建带相应标签的新节点preferredDuringScheduling约束通常被忽略,因为它是软性要求- Pod Anti-Affinity 在节点扩容时可能导致 Cluster Autoscaler 过度扩容(每个新节点只调度一个 Pod 后有剩余空间),建议配合 Pod Disruption Budget 使用
4.4 多租户场景下的调度隔离
在多租户集群中,可以使用命名空间加调度策略实现软隔离:
# 为租户 A 的 Pod 添加调度约束
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: tenant
operator: In
values:
- tenant-a
结合 Namespace ResourceQuota 和 LimitRange,实现从调度到资源的多层隔离。
总结
本文深入解析了 Kubernetes 的三大精细化调度策略:
- Node Affinity:让 Pod 主动选择”我要什么样的节点”,支持硬约束和软约束,是
nodeSelector的强力升级版 - Pod Affinity / Anti-Affinity:让 Pod 根据其他 Pod 的位置做调度决策,实现靠近(低延迟)或远离(高可用)
- Pod Topology Spread Constraints:确保 Pod 在集群中均匀分布,是跨可用区部署的利器
与之前学习的污点和容忍度相比,Node Affinity 是”拉”的策略(Pod 选择节点),而污点和容忍度是”推”的策略(节点筛选 Pod)。两者结合使用,可以实现从简单到复杂的全部调度需求。
在生产环境中,建议组合使用多种调度策略,并牢记”能不用硬约束就不用硬约束”的原则——preferredDuringScheduling 比 requiredDuringScheduling 更灵活,能避免因拓扑约束过严导致的 Pod 无法调度问题。
📖 系列目录
- 第 1 天:Kubernetes 是什么?核心概念与架构全景解析
- 第 2 天:手把手搭建你的第一个 K8s 集群(kubeadm 实战)
- 第 3 天:Pod 详解:K8s 最小的调度单元与生命周期管理
- 第 4 天:Deployment 与 ReplicaSet:声明式应用管理
- 第 5 天:Service 与网络基础:ClusterIP、NodePort、LoadBalancer 详解
- 第 6 天:Namespace 与资源配额:多租户隔离基础
- 第 7 天:ConfigMap 与 Secret:配置管理最佳实践
- 第 8 天:Volume 与 PersistentVolume:存储抽象层的核心机制
- 第 9 天:StorageClass 与动态存储供给实战
- 第 10 天:StatefulSet:有状态应用的部署与管理
- 第 11 天:Ingress 与 Ingress Controller:外部流量接入全攻略
- 第 12 天:NetworkPolicy:K8s 网络安全策略与微隔离
- 第 13 天:Headless Service 与服务发现机制深度解析
- 第 14 天:CSI 存储插件与生产存储选型指南
- 第 15 天:污点与容忍度:掌控 Pod 调度
- 第 16 天:Node Affinity 与 Pod Affinity:精细化调度策略实战 ← 本篇
- 第 17 天:HPA 水平自动扩缩:基于 CPU/内存/自定义指标的弹性伸缩 🔜
- 第 18 天:VPA 与 Cluster Autoscaler:资源与集群层的自动扩缩 🔜
- 第 19 天:Job 与 CronJob:批处理与定时任务实战 🔜
- 第 20 天:PriorityClass 与抢占式调度机制 🔜
- 第 21 天:资源配额与 LimitRange:多租户资源管控 🔜
- 第 22 天:监控体系搭建:Prometheus + Grafana + Kube-state-metrics 🔜
- 第 23 天:日志收集实战:EFK/ELK 栈在 K8s 中部署 🔜
- 第 24 天:RBAC 权限控制:ServiceAccount、Role、ClusterRole 深度解析 🔜
- 第 25 天:Helm Charts:包管理器使用与 Chart 开发实战 🔜
- 第 26 天:集群备份与恢复:etcd 快照 + Velero 方案 🔜
- 第 27 天:滚动更新与回滚策略:实现零停机发布 🔜
- 第 28 天:集群升级最佳实践:Control Plane 与 Node 升级步骤 🔜
- 第 29 天:多集群管理:Federation / Cluster API / 多集群服务网格 🔜
- 第 30 天:K8s 生产环境踩坑实录:性能调优、故障排查与最佳实践 🔜
下期预告
第 17 天:HPA 水平自动扩缩:基于 CPU/内存/自定义指标的弹性伸缩
在学习了 Pod 如何被调度到节点后,下一个关键问题是:当负载变化时,Pod 的数量如何自动调整?第 17 天我们将深入 Kubernetes 水平自动扩缩(HPA),从 Metrics Server 部署到基于 CPU、内存和自定义指标的自动伸缩策略,帮你打造弹性的生产集群。















暂无评论内容