Kubernetes 调度 亲和性污点与容忍
樱下纸门
10 min read
Kubernetes 调度 亲和性污点与容忍
调度器的目标不是在“任意空闲节点”上塞下 Pod,而是把 Pod 放到满足约束、且资源账算得过来的节点上。亲和性控制“我想去哪 / 我不想和谁在一起”,污点容忍控制“哪些节点默认拒绝、谁有通行证”。两者方向相反但必须配合:只用污点会调度失衡,只用亲和会缺乏硬性隔离。调度是集群资源利用率和稳定性的总闸门,理解它才能解释“为什么这个 Pod 一直 Pending”,也才能在资源紧张时知道该调哪里而不是盲目加机器。
调度决策的三个输入
- 资源账:节点的 allocatable 减去已分配,必须 ≥ Pod 的 requests。
- 约束:nodeSelector、亲和性、污点容忍。
- 打分:满足硬约束后,按拓扑、镜像就近、资源碎片等打分选最优。
requests 决定“能不能调度”,limits 决定“能用到多少”。只设 limits 不设 requests,调度器按 0 算账,节点可能超卖到 OOM。经验上 requests 要贴近真实均值,limits 留峰值余量,别拍脑袋。requests 写太大,节点早早“满”了,利用率上不去;写太小,节点超卖,OOM 风险升。
调度器内部的四个阶段
调度不是一步到位,而是分阶段:filter(预选)淘汰不满足硬约束的节点;score(优选)给剩下的打分;reserve(保留)先占住资源防止并发冲突;bind(绑定)把结果写回 API。Pod 一直 Pending 时,先看 filter 阶段为何全被淘汰——kubectl describe pod 的 Events 通常会写明“节点不满足 cpu 请求”或“节点有未容忍的污点”,顺着这条信息改约束比盲目调参快得多。
最朴素的约束 nodeSelector
spec:
nodeSelector:
disktype: ssd
nodeSelector 是“精确键值匹配”的硬约束,简单但只能做相等判断,做不了“尽量”或“反着来”。复杂意图要交给亲和性。它适合“就这一类节点能跑”的强要求,不宜承载带偏好的场景,否则只能写一堆相等的 key。
节点亲和性:软硬两档
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: disktype
operator: In
values: ["ssd"]
required 是硬约束,不满足就 Pending;preferred 是软偏好,带 weight 参与打分。架构层面用 required 卡住架构(比如必须 amd64),性能层面用 preferred 引导(比如尽量 ssd),既稳又灵活。IgnoredDuringExecution 表示调度后即使节点标签变了也不驱逐,想要运行时也生效需要别的机制,否则节点标签漂移后约束就形同虚设。
Pod 亲和与反亲和
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: cache
topologyKey: kubernetes.io/hostname
反亲和常用在“同一服务的多副本不要挤在一个节点”上,topologyKey 指定打散维度,用 hostname 即按节点打散,用 zone 即按可用区打散。注意 required 反亲和在节点数少于副本数时会卡住调度,量级上副本数要 ≤ 可用节点数,否则改用 preferred,否则会出现“永远 Pending 的半数副本”,而且 describe 里还会显示 Unschedulable。
拓扑分布约束
除了反亲和,还有专门的 topologySpreadConstraints 控制打散更细:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: { app: web }
maxSkew 限制各拓扑域副本数差,whenUnsatisfiable: DoNotSchedule 是硬约束、ScheduleAnyway 是软约束。它比反亲和的语义更直观,适合大规模均匀铺开,也更容易和 HPA 配合。反亲和是“不要在一起”,拓扑约束是“尽量均匀”,两者视角不同,大集群更推荐拓扑约束。
污点与容忍:节点的否决权
给节点打污点,没有对应容忍的 Pod 不会被调度上去:
kubectl taint nodes node-1 dedicated=gpu:NoSchedule
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoExecute"
effect 三档:NoSchedule 不调度、PreferNoSchedule 尽量不调度、NoExecute 不但不调度,还会驱逐已运行但不容忍的 Pod。后者常用于节点维护:打了 NoExecute 污点,普通 Pod 被驱逐,带容忍的系统组件留下继续干活。把专用节点(GPU、大内存)用污点保护起来,是避免普通负载抢占稀缺资源的常规做法。
配额与约束对调度的限制
调度之上还有两道闸门:ResourceQuota 限制命名空间的总 requests/limits 用量,用尽后新 Pod 直接被拒;LimitRange 给没显式设 requests/limits 的 Pod 注入默认值。没设 requests 的 Pod 被 LimitRange 补上默认请求,会改变调度账;Quota 用尽则 Pod 卡在 FailedScheduling。排查“为什么排不进”时,别只盯节点,也要看命名空间配额和 LimitRange。
PodDisruptionBudget 与自愿驱逐
PDB 限制自愿驱逐(节点维护、滚动更新)时同时下线的副本数,保证最少可用副本。它和 NoExecute 污点配合:维护打污点驱逐,PDB 保证至少留几个,避免一刀切全杀导致服务不可用。PDB 管的是“自愿”驱逐,不影响节点宕机这种“非自愿”情况,别指望它挡硬件故障。
自定义调度器与扩展
默认调度器不够用时,可以写独立调度器或 extender 扩展打分/过滤逻辑,Pod 用 schedulerName 指定用哪个。多调度器场景下,各自的 reserve 互不知道,可能同时把 Pod 放到同一节点造成超卖,要特别小心。extender 通过网络调用扩展,延迟会拖慢整体调度吞吐,只在必要时引入。
一个调优示例
某服务设了 required 节点亲和只去 GPU 节点,但 GPU 节点被占满,副本全 Pending。放宽成 preferred 并扩充 GPU 节点池后恢复。这例子说明硬约束过窄会牺牲可用性:约束越硬,可落点越少,资源紧张时最先出问题。把“必须”降到“尽量”,往往就能解开死结。
资源碎片化与装箱
即便集群总资源够,若剩余资源被切成小块分散在多个节点,一个大 requests 的 Pod 仍可能无处可放,这叫碎片化。score 阶段的打分策略决定怎么填:偏好 MostAllocated 把节点尽量填满,能减少碎片、提高密度,但牺牲突发扩容余地;LeastAllocated 反之,留白多、弹性好但密度低。选哪个打分策略,取决于你更想要密度还是弹性,错误选择会一边浪费机器一边排不进 Pod。
踩坑清单
- 把 required 反亲和拓扑设成 zone,但集群只有一个 zone,副本全 Pending。拓扑维度要和实际规模匹配。
- NoExecute 污点忘了配容忍,节点维护时把本该留下的监控组件也驱逐了,观测出现盲区。
- 只设 limits 不设 requests,调度器按零计费,节点超卖触发整机 OOM,连带杀掉无关 Pod。
- 节点亲和 operator 用错 In / Exists,前者要列值后者只判键存在,写错直接不匹配。
- 污点 value 大小写和容忍不一致,容忍不生效,Pod 一直 Pending 又找不到原因。
- 优先级设太高且 preemptionPolicy 默认开启,批处理大任务反过来挤掉在线服务。
- 多个 nodeSelectorTerms 之间是“或”关系,多个 matchExpressions 之间才是“与”,逻辑写反会导致约束比预期宽松,调度到不该去的节点。
- 打分偏好 weight 总和不当,多个 preferred 互相抵消,实际落点和预期偏差大,要复核权重分配。
- 多调度器各自 reserve 互不可见,可能超卖,要谨慎设计。
调度约束越多,可落点越少,碎片越高。原则是把“必须”写成硬约束,把“最好”写成软偏好,给调度器留余地。
1 view
0 likes
0 comments