K8s 系列 | 第 16 天:Node Affinity 与 Pod Affinity:精细化调度策略实战


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"

这个配置同时使用了全部四种调度策略,是典型的生产级配置。调度器的决策流程如下:

  1. 过滤(Filtering):只保留 environment=productiondisk-type=ssd 的节点
  2. 打分(Scoring):计算每个候选节点的得分
  3. 尽量与 app=payment 的 Pod 不在同一节点(+100 分)
  4. 尽量与 app=payment-db 在同一可用区(+80 分)
  5. 尽量跨可用区均匀分布
  6. 选择:选择总分最高的节点

三、常见问题

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 能一起用吗?

可以。如果同时指定了 nodeSelectornodeAffinity.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 调度策略的选择优先级

在决定使用哪种调度策略时,建议遵循以下优先级:

  1. Taint & Toleration:防御性策略——阻止不兼容的 Pod 调度到特殊节点(GPU、专用硬件)
  2. Node Affinity:主动选择——Pod 明确声明需要的节点特征
  3. Topology Spread Constraints:分布策略——确保跨拓扑域均匀分布
  4. Pod Anti-Affinity:隔离策略——避免关键服务集中
  5. 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)。两者结合使用,可以实现从简单到复杂的全部调度需求。

在生产环境中,建议组合使用多种调度策略,并牢记”能不用硬约束就不用硬约束”的原则——preferredDuringSchedulingrequiredDuringScheduling 更灵活,能避免因拓扑约束过严导致的 Pod 无法调度问题。

📖 系列目录

下期预告

第 17 天:HPA 水平自动扩缩:基于 CPU/内存/自定义指标的弹性伸缩

在学习了 Pod 如何被调度到节点后,下一个关键问题是:当负载变化时,Pod 的数量如何自动调整?第 17 天我们将深入 Kubernetes 水平自动扩缩(HPA),从 Metrics Server 部署到基于 CPU、内存和自定义指标的自动伸缩策略,帮你打造弹性的生产集群。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容