7.5 调度与亲和性

一句话

调度是 “过滤 + 打分” 的两段式:nodeSelector / affinity / taints 决定 “能不能放”,topologySpread / priority 决定 “放哪更好”;但调度只是初始放置,跑起来之后还可以被驱逐、抢占、OOM 推翻。

六种机制:谁在过滤,谁在打分

机制 阶段 强/软 典型用途
nodeSelector 过滤 强 “只用 GPU 节点”
nodeAffinity.required… 过滤 强 按可用区/机型/标签精确限定
nodeAffinity.preferred… 打分 软 “尽量放在 SSD 节点”
podAffinity / podAntiAffinity 过滤或打分 强/软(看后缀) 与某类 Pod 同置 / 分散
topologySpreadConstraints 打分(可升级为过滤) 软/强 副本均匀分布到 AZ / 节点
taints + tolerations 过滤(NoExecute 还会驱逐) 强 专用节点池、隔离故障节点

一个常被忽略的事实:调度只看 requests,不看 limits。requests 是 “我要预留多少”,节点按它算可分配量;limits 只在运行时生效。

亲和性:四个后缀决定行为

写法 调度时 运行中标签变了
requiredDuringSchedulingIgnoredDuringExecution 必须满足,否则 Pending 不驱逐(Ignored)
preferredDuringSchedulingIgnoredDuringExecution 尽量满足,不满足也能跑 不驱逐
requiredDuringSchedulingRequiredDuringExecution(部分版本未实现) 必须满足 会驱逐

这就是为什么 “节点打错了标签,Pod 还在跑”:IgnoredDuringExecution 只影响调度那一刻。

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - {key: topology.kubernetes.io/zone, operator: In, values: [az-a, az-b]}
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:   # 软:资源不足时允许同置
        - weight: 100
          podAffinityTerm:
            topologyKey: kubernetes.io/hostname
            labelSelector:
              matchLabels: {app: web}

分散副本:antiAffinity 还是 topologySpread

方案 表达 坑
podAntiAffinity(required) “不要和同类 Pod 同节点” 节点数少于副本数时,多出来的 Pod 永远 Pending
podAntiAffinity(preferred) “尽量不同节点” 资源紧张时静默挤在一起,你以为分开了
topologySpreadConstraints + whenUnsatisfiable: DoNotSchedule “每个 AZ 的副本数差不超过 maxSkew” maxSkew 设太大等于没约束;minDomains 不设时少一个 AZ 也能满足
topologySpreadConstraints + ScheduleAnyway 软约束,调度器给高分 极端情况下仍会偏斜

🔑 副本数与节点数的关系必须对上:required anti-affinity 下,副本数 ≤ 可用节点数,否则扩容就是在制造 Pending。

污点与容忍:专用节点池的标准做法

污点效果 对新 Pod 对已在跑的 Pod
NoSchedule 不调度(除非容忍) 不驱逐
PreferNoSchedule 尽量不调度 不驱逐
NoExecute 不调度 驱逐不容忍的 Pod(可按 tolerationSeconds 延迟)

典型组合:GPU 节点打 nvidia.com/gpu=present:NoSchedule,只有声明了对应容忍的推理服务能上去;节点故障时由 node controller 打 node.kubernetes.io/unreachable:NoExecute 触发驱逐(默认容忍 300 秒)。

Pending 的排查顺序(照这个顺序查,别乱试)

  1. kubectl describe pod 的 Events:0/5 nodes are available: 3 Insufficient cpu, 2 node(s) had untolerated taint。
  2. 资源:requests 是否超过任何单节点的可分配量(一个 8C 的 request 在 4C 节点上永远放不下)。
  3. 亲和性/反亲和性:required 类约束是否与现有节点矛盾。
  4. 污点:节点池是否有未容忍的 taint。
  5. PVC:WaitForFirstConsumer 的 StorageClass 需要先有 Pod 才绑定,但若 PV 供应失败会一直 Pending(见 7.7 存储卷与持久化)。
  6. 配额:命名空间的 ResourceQuota 是否已满(这时事件是 exceeded quota,不是 “资源不足”)。

反模式

反模式 后果
用 nodeSelector 实现 “高可用” 所有副本被钉在一个节点/一个 AZ,故障全灭
required anti-affinity + 副本数 > 节点数 永远有 Pod Pending,扩缩容卡住
给业务 Pod 加 NoExecute 容忍 3600 秒 节点故障后一小时才迁移,RTO 直接超标
只看 limits 不看 requests 调度按 0 算,节点超卖后集中 OOM
把 “节点标签” 当不可变事实 标签改了旧 Pod 不迁移(IgnoredDuringExecution)
忽略 topologySpreadConstraints 的 maxSkew 名义上分散,实际可以 3:0

三条铁律

🔑 调度只看 requests;limits 是运行时的事,不参与放置。

🔑 强约束要有 “放不下” 的预案:required 类约束 + 副本数超过可用域 = 永久 Pending。

🔑 分散副本优先用 topologySpread;antiAffinity 的 required 版本只在节点数确定够用时才用。

自检

Q1 一个 Deployment 有 5 个副本,集群 3 个节点,配了 required 的 podAntiAffinity(按 hostname)。会发生什么?

▸ 答案:2 个 Pod 永远 Pending。required anti-affinity 要求每个节点最多一个同类 Pod,3 个节点只能放 3 个;其余 2 个找不到满足条件的节点,事件里会写 “didn't match pod anti-affinity rules”。修法:改成 preferred,或用 topologySpreadConstraints 表达 “尽量均匀”,或增加节点。

Q2 节点打了新标签,Pod 没有迁移过去,是调度坏了吗?

▸ 答案:不是。绝大多数亲和性规则是 IgnoredDuringExecution:只在调度那一刻检查,运行中标签变化不会触发重调度。想让 Pod 跟随标签变化,需要 RequiredDuringExecution(多数版本未实现)或主动重建(kubectl rollout restart)。同理,删掉节点标签也不会驱逐已在跑的 Pod。

Q3 requests.cpu: 8 的 Pod 在 4 核节点集群里永远 Pending,为什么调 limits 没用?

▸ 答案:因为调度器按 requests 计算节点剩余可分配量:8 核 request 在任何 4 核节点上都放不下,即使 limits 写成 1 也一样。修法是降 requests(按实际 P95 用量),而不是动 limits。这也是 “requests 虚高导致扩容失败” 的典型成因。

Q4 什么时候该用 NoExecute 污点,而不是 NoSchedule?

▸ 答案:当你要驱逐已经在跑的 Pod 时。NoSchedule 只挡新 Pod,旧 Pod 会一直留着;NoExecute 会把不容忍的 Pod 赶走(可用于节点下线、专用化改造)。代价是驱逐会造成中断,必须配合 tolerationSeconds 与 PDB(PodDisruptionBudget)控制节奏。

Q5 一个服务要求 “三个副本分到三个 AZ”,你会怎么写,怎么验证?

▸ 答案:写 topologySpreadConstraints:maxSkew: 1、topologyKey: topology.kubernetes.io/zone、whenUnsatisfiable: DoNotSchedule,并设 minDomains: 3(否则只有 2 个 AZ 时也能满足)。验证:kubectl get pods -o wide 看 NODE 列,再对照节点所属 AZ;或用 kubectl get pod -o custom-columns 直接输出 zone 标签。只写 YAML 不抽查,等于没做。

跨篇相关:调度是控制器收敛的一部分见 7.1 声明式 API 与控制器模式;副本与身份语义见 7.2 K8s 对象模型 (Pod / ReplicaSet / Deployment / StatefulSet / DaemonSet / Job);资源与 QoS 决定谁能被驱逐见 7.9 资源限制与 QoS;PVC 与 WaitForFirstConsumer 见 7.7 存储卷与持久化;AZ 与故障域的取舍见 5.3 地域与可用区;弹性扩缩容与节点池见 5.4 弹性伸缩与自动扩缩容。