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 的排查顺序(照这个顺序查,别乱试)
kubectl describe pod的 Events:0/5 nodes are available: 3 Insufficient cpu, 2 node(s) had untolerated taint。- 资源:
requests是否超过任何单节点的可分配量(一个 8C 的 request 在 4C 节点上永远放不下)。 - 亲和性/反亲和性:required 类约束是否与现有节点矛盾。
- 污点:节点池是否有未容忍的 taint。
- PVC:
WaitForFirstConsumer的 StorageClass 需要先有 Pod 才绑定,但若 PV 供应失败会一直 Pending(见 7.7 存储卷与持久化)。 - 配额:命名空间的
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 弹性伸缩与自动扩缩容。