Kubernetes调度进阶:亲和性、反亲和与污点容忍实战排障 1. 调度器到底在忙什么如果你在 Kubernetes 集群里跑过一段时间业务一定见过这样的场景明明节点资源还有剩余Pod 却一直卡在 Pending 状态或者两个高负载的实例偏偏被调度到同一个节点上互相争抢 CPU。这种问题往往不是资源不够而是你对调度规则理解得不够。Kubernetes 的默认调度器虽然能解决“把 Pod 放到某个节点”这个基本问题但当集群规模变大、业务场景变复杂你会发现默认的“看资源、看标签”并不总能满足你的真实意图。调度器的职责简单说就是为一个待调度的 Pod从所有 Node 里挑一个最合适的。这个“合适”不是拍脑袋而是经过一套固定的流程——过滤Filtering和打分Scoring。过滤阶段把不满足硬性条件的节点排除掉比如资源不够、端口冲突、磁盘不满足或者有污点且没对应容忍度打分阶段则对剩下的节点按策略打分比如资源剩余越多分数越高最终选分数最高的节点。这套机制是 Kubernetes 默认的调度框架但只是“能用”谈不上“懂你”。为了让 Pod 按照业务需求去正确的位置我们通常要借助四类调度控制手段节点亲和/反亲和、Pod 亲和/反亲和、Taints 污点与 Tolerations 容忍度。很多人看文档时觉得每条都懂但真到排障时还是分不清为什么设置了 nodeAffinity 还是调度到了别的节点为什么 Pod 关联失败为什么加了污点后旧节点上的 Pod 没有立即被驱逐这篇文章我结合自己实际运维和排障的经验把这些机制拆开讲清楚并配上可直接复制的 YAML 和排查命令。如果你刚开始接触 Kubernetes建议先掌握默认调度器的工作方式再往下看。如果你已经有一定的集群运维经验可以直接跳到亲和和污点部分我踩过的坑也许你正在踩。2. 节点亲和与反亲和把 Pod 放到最该去的地方2.1 nodeSelector 与 nodeAffinity 的区别节点亲和nodeAffinity和 nodeSelector 都是让 Pod 找特定节点的工具但能力完全不同。nodeSelector 是 Kubernetes 最古老的“标签匹配”方式它只能做等值匹配比如spec: nodeSelector: disktype: ssd这意味着 Pod 只调度到带disktype: ssd标签的节点上。这种匹配方式简单粗暴但很难表达更复杂的约束比如“节点必须在 zone-a 并且在上面跑着 GPU 类型的负载”这种组合条件nodeSelector 就无能为力了。nodeAffinity 则提供了丰富的操作符In、NotIn、Exists、DoesNotExist、Gt、Lt并且把约束分成两类硬性要求requiredDuringSchedulingIgnoredDuringExecution和软性偏好preferredDuringSchedulingIgnoredDuringExecution。从命名上就能看出关键如果你希望节点必须满足条件否则 Pod 不调度那就用required如果只是“尽量满足”满足不了也可以调度到其他节点就用preferred。“IgnoredDuringExecution” 这段后缀也值得注意。它表示在调度时这个规则生效但如果节点的标签后来被修改或删除了已经运行的 Pod 不会因此被驱逐。这是有意为之避免因为标签变化导致集群里的 Pod 大面积搬迁移。新版 Kubernetes 也在实验requiredDuringSchedulingRequiredDuringExecution但目前生产中主流版本仍然以 Ignored 为主。2.2 硬性节点亲和配置先看一个硬性节点亲和的完整示例。假设我们有一批节点带有regioncn-east、zonezone-a、gputrue的标签现在要部署一个需要 GPU 的训练任务并且必须跑在 zone-aapiVersion: apps/v1 kind: Deployment metadata: name: gpu-trainer spec: replicas: 2 selector: matchLabels: app: gpu-trainer template: metadata: labels: app: gpu-trainer spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: region operator: In values: - cn-east - key: zone operator: In values: - zone-a - key: gpu operator: Exists containers: - name: trainer image: training-image:latest注意nodeSelectorTerms是一个列表里面的多个matchExpressions之间是“或”的关系只要满足任意一个 term整个节点就算满足。而在同一个 term 内部的matchExpressions之间是“与”的关系必须全部满足。这个层级关系非常容易搞错。我见过有人想表达“节点在 zone-a 或 zone-b”结果写在同一个 term 里导致 Pod 永远调度不上去。正确的写法是把 zone-a 和 zone-b 分别放到两个 term或者用operator: Invalues 写[zone-a, zone-b]。后者更简洁。2.3 软性节点亲和与权重软性亲和preferred和硬性亲和写法类似只是多了weight参数affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: zone operator: In values: - zone-a - weight: 20 preference: matchExpressions: - key: disktype operator: In values: - ssd调度器会为每个节点计算一个总分preferred的权重就是该节点满足条件的加分值。权重值范围是 1 到 100。上面例子里如果节点同时满足 zone-a 和 ssd会得 100 分只满足 zone-a 得 80 分。但最终能否被选中还要看调度器其他打分项综合的结果。这也是很多人困惑的地方明明设置了软性亲和 zone-aPod 却跑到了 zone-b。因为软性亲和只是“偏好”如果另一节点资源更充裕或更好分数可能反超。实际业务中我会把关键路径上的服务用硬性节点亲和把非关键服务用软性亲和配合资源请求和限制来平衡集群负载。2.4 节点反亲和的实际玩法节点反亲和nodeAntiAffinity在 Kubernetes 里没有单独的字段它是通过nodeAffinity里的NotIn或DoesNotExist操作符实现的。比如我们希望 Pod 不要调度到带networkfast标签的节点上affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: network operator: NotIn values: - fast反亲和的典型场景是隔离风险某个节点属于另一个团队独占不希望别人的 Pod 跑上来或者节点上有特殊的硬件需要避免某些普通业务混部。这里要注意NotIn的 values 为空是不合法的如果只想表达“节点不存在某个标签”用DoesNotExist更合适。还有一点节点亲和操作符中的Gt和Lt只支持整数值做数值比较时标签 value 必须是数字字符串否则调度器会报错。我曾经踩过坑把节点内存以64Gi这种字符串打标签然后用Gt比较结果调度器一直不满足条件。后来换成用 int 形式的标签比如memory-rank: 64才恢复正常。3. Pod 亲和与反亲和让 Pod 们“抱团”或“隔离”3.1 PodAffinity 解决的问题节点亲和解决的是“Pod 与 Node 的关系”Pod 亲和podAffinity解决的是“Pod 与 Pod 的关系”。什么场景需要关心 Pod 和 Pod 之间的关系最常见的是同 Zone 高可用和同节点数据本地性。比如某个分布式存储系统副本之间如果分布在同一节点节点宕机就会丢多个副本所以必须让副本分散到不同节点甚至不同可用区——这是 Pod 反亲和再比如某个缓存服务希望把 Pod 调度到有对应数据缓存节点的同一位置减少跨节点访问延迟——这是 Pod 亲和。Pod 亲和/反亲和的关键概念有三个topologyKey拓扑域、labelSelector选择目标 Pod、namespaces限定命名空间。topologyKey决定了“同域”的范围。比如topologyKey: kubernetes.io/hostname表示按节点维度判断topologyKey: topology.kubernetes.io/zone表示按可用区维度判断。同一拓扑域内的节点会被视为“在一起”。3.2 Pod 亲和示例和某个 Pod 同节点假设我们有一个服务 Anginx它提供本地缓存接口另一个服务 Bapi需要访问这个缓存最好让 B 的 Pod 和 A 的 Pod 落在同一节点上避免走网络。可以这样配affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - nginx topologyKey: kubernetes.io/hostname这段的意思目标节点上至少要有一个带有appnginx标签的 Pod并且该 Pod 与当前待调度 Pod 的hostname拓扑域一致。如果集群里不存在这样的节点Pod 就会一直 Pending。注意这里的“目标节点上有一个匹配 Pod”是在调度时判断的。如果集群中该节点上的匹配 Pod 后来被删了已经调度上去的 Pod 不会被自动重新调度因为同样是 IgnoredDuringExecution 语义。3.3 Pod 反亲和示例让副本尽量分散再看反亲和。我们希望一个 Deployment 的两个副本不要调度到同一个节点上可以用affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - web-server topologyKey: kubernetes.io/hostname上面是软性反亲和调度器会尽量把副本分散但没有硬性保证。如果只有两个节点而副本数是 3必然有同一个节点放两个副本。如果希望硬性保证把preferred改成required。但我建议谨慎使用硬性 Pod 反亲和因为一旦集群规模小于副本数多余的副本就永远 Pending。高可用场景中如果副本数可控硬性反亲和是有意义的如果副本数可能超过节点数建议用软性反亲和否则容灾效果没达到反而把服务搞挂了。3.4 topologyKey 的选择潜规则topologyKey是 Pod 亲和/反亲和最核心的维度选错会导致完全不同的行为。比如用kubernetes.io/hostname是只要求不同节点用topology.kubernetes.io/zone则要求不同可用区如果希望两个 Pod 在同一可用区就用 zone 作为 topologyKey。但要注意topologyKey 的值必须是一个节点标签的 key且节点上所有节点都必须有这个标签否则调度会异常。比如只给一部分节点打了rack标签另一部分没有然后你用topologyKey: rack那么没有该标签的节点永远无法满足“同一拓扑域”的条件Pod 调度会受阻。另一个容易踩的坑是Pod 亲和判断的拓扑域是从节点标签推导的但如果你有多个节点拥有相同的kubernetes.io/hostname标签虽然不应该但手动改标签时可能会发生那么拓扑域划分就会错乱。所以务必保持节点标签的唯一性和一致性。在多命名空间场景Pod 亲和默认只会在待调度 Pod 所在的命名空间内查找目标 Pod。如果需要跨命名空间必须在namespaceSelector或直接指定namespaces列表否则跨命名空间的目标 Pod 不会被匹配到。这个细节很容易让人对照文档时觉得“明明有 Pod 为什么匹配不上”。4. Taints 与 Tolerations给节点设置“准入规则”4.1 污点的三个要素Taints污点和节点亲和正好相反。节点亲和是“吸引”Pod 去某些节点污点是“排斥”Pod 靠近节点。一个污点由三个部分组成key、value、effect。通常在命令行里写作keyvalue:effect也可以在 YAML 中描述。effect 有三种NoSchedule、PreferNoSchedule、NoExecute。NoSchedule新的 Pod 除非容忍该污点否则不会调度到该节点。但节点上已有的 Pod 不受影响。PreferNoSchedule软性的 NoSchedule调度器会尽量避免但不保证。集群资源紧张时可能会把 Pod 调度上去。NoExecute不仅新的 Pod 不能调度上去节点上已有的 Pod 如果没有对应容忍会被立即驱逐Evicted。污点的典型用途包括给专用节点打污点让普通业务不能误调度给有问题或需要维护的节点打污点把异常 Pod 驱走给 GPU 节点打污点只有声明了需要 GPU 的工作负载才允许调度上去。4.2 用 kubectl 管理污点命令很简单常用的几个# 给 node1 添加污点effectNoSchedule kubectl taint nodes node1 dedicatedspecial:NoSchedule # 去掉污点在末尾加 - kubectl taint nodes node1 dedicatedspecial:NoSchedule- # 查看节点污点 kubectl describe node node1 | grep -i taint如果要添加 NoExecute 污点且希望已有 Pod 延迟驱逐可以给容忍度加tolerationSeconds控制 Pod 在节点上多待一段时间后自动被剔除。这个字段在需要“优雅迁移”的时候非常有用。4.3 Pod 的 Toleration 配置Pod 要调度到有污点的节点需要配置 tolerations。最基本的写法tolerations: - key: dedicated operator: Equal value: special effect: NoSchedule这里 operator 用Equal要求 key、value、effect 完全匹配。如果你只关心 key不关心 value可以用Existstolerations: - key: dedicated operator: Exists effect: NoScheduleExists 不需要写 value。另外如果 Pod 愿意容忍所有污点可以写成tolerations: - operator: Exists这表示容忍所有 key、value、effect 的污点一般不建议在生产环境这样用因为这会绕开所有节点的保护性污点。4.4 结合亲和与污点实现专用节点实际项目中我经常把“节点亲和 污点 容忍”组合起来使用。比如某公司有一批高性能计算节点标签是workloadhpc同时打了污点hpctrue:NoSchedule。普通业务没有容忍度调度器会直接避开这些节点。只有需要 HPC 的任务在 Deployment 里同时配置硬性节点亲和和对应容忍度才能使用这些资源。spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: workload operator: In values: - hpc tolerations: - key: hpc operator: Equal value: true effect: NoSchedule这样既保证了专用性又不影响普通业务正常调度。还有一个常见场景把控制面节点打上污点让业务 Pod 不调度到控制面节点上这其实是很多集群初始化工具的默认行为。但当控制面节点资源充足时你也许希望某个内部管理组件可以调度上去那就给这个组件加上对应的 tolerations 即可。4.5 NoExecute 驱逐的细节NoExecute 污点是最容易让人意外的。比如某个节点出现磁盘故障你不想手动一个一个地删 Pod可以给节点打上node.kubernetes.io/unreachable:NoExecuteKubernetes 集群自身也会自动给有问题的节点加类似的污点。这时节点上所有没有对应容忍度的 Pod 会被驱逐。Kubernetes 本身有一些内置的容忍度比如node.kubernetes.io/not-ready和node.kubernetes.io/unreachable默认容忍时间是 300 秒5分钟。这意味着即使节点出现故障Pod 也不会立刻被驱逐而是会等 5 分钟给 kubelet 恢复的时间。这个默认值写在 kube-controller-manager 的配置里可以通过--node-eviction-timeout调整。所以你在排查事件时看到某个 Pod “Evicted” 而找不到直接原因时先检查节点是否处于 NotReady。我自己吃过一次亏给节点配置了自动化修复脚本脚本会在节点失联后给节点打上NoExecute污点目的是快速驱逐 Pod 到其他节点。但忽略了控制面默认的 300 秒夯在那边最后整个服务中断了几分钟才恢复。后来我在脚本里直接触发kubectl drain并配合--disable-eviction选项才做到了可控迁移。5. 常见问题与排查实录5.1 Pod 一直 Pending如何判断是谁的问题遇到 Pod Pending我一般的排查顺序是先用kubectl describe pod pod-name看 Events最后几条会直接说明原因。常见的原因有资源不足事件提示Insufficient cpu/Insufficient memory这时候要去看节点容量和已分配资源而不是盲目加节点。节点有污点事件提示1 node(s) had taint {dedicated: special}, that the pod didnt tolerate。解决方式是修改 tolerations 或去掉污点。硬性节点亲和不满足事件提示0/3 nodes are available: 3 node(s) didnt match node selector说明节点标签和亲和条件不匹配。Pod 亲和不满足事件提示0/3 nodes are available: 3 node(s) didnt match pod affinity/anti-affinity可能目标 Pod 不在或者 topologyKey 选得不对。贴一个典型的输出Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 12s default-scheduler 0/3 nodes are available: 1 node(s) didnt match node selector, 2 node(s) had taint {dedicated: special}, that the pod didnt tolerate.这条信息把两个原因都列出来了。你可以针对性检查。5.2 软性亲和为什么不生效常有人反馈“设置了 preferredDuringScheduling 但一次都没生效”。首先要明确软性亲和只影响打分不影响过滤。如果一个节点完全不满足软性条件但只要它打分最高Pod 还是会调度过去。当集群节点很少或者目标和当前调度 Pod 所在节点重合时不生效是正常的。要验证软性亲和是否真的没生效可以在多个节点之间加个标签差异比如 zone-a 和 zone-b然后观察大量调度后分布是否倾斜。如果完全没有倾斜检查配置里的weight是否写成了 0。weight 最小是 1如果填 0调度器会忽略该偏好。另外如果你同时在 Deployment 里设置了podAntiAffinity和nodeAffinity两者的约束是同时生效的不会互相覆盖。调试时不要只盯着其中一项。5.3 污点已经去掉Pod 还在 Pending有一种情况是我明明执行了kubectl taint nodes node1 dedicated-但 Pod 还是 Pending。此时去describe node看 Taints 字段是否已为空。如果确实为空再看 Pod 的事件。很多时候事件里已经没有污点相关的错误了而是显示节点 CPU 或内存不足。因为在你去掉污点的同时该节点可能已经被其他等待的 Pod 填满了。这要求你不仅关注污点还要关注整体资源水位。还有一种隐蔽情况Pod 有硬性 nodeAffinity 要求恰好节点也有污点你给 Pod 加了 toleration但节点亲和又不满足。这时候事件只会提示“didnt match node selector”。所以排查时要多个维度同时看不能只盯一个。5.4 如何优雅地清空一个节点如果要对节点做计划内维护正确操作顺序是给节点打上 NoExecute 污点或执行kubectl cordon阻止新 Pod 调度上来。使用kubectl drain驱逐节点上的 Pod。命令大概是kubectl drain node1 --ignore-daemonsets --delete-emptydir-data--ignore-daemonsets是因为 DaemonSet 的 Pod 由控制器直接管理驱逐后会被重新调度到同一节点drain 会卡住。--delete-emptydir-data则是允许删除使用 emptyDir 的 Pod如果业务对本地临时数据有要求需要谨慎评估。维护完成后执行kubectl uncordon node1让节点重新接受调度。drain 的过程中如果节点上有未通过 PodDisruptionBudget 的 Poddrain 会阻塞等待。这是保护机制确保大规模驱逐不会破坏服务可用性。我之前排障时发现 drain 命令一直卡住就是因为某个服务的 PDB 设置了minAvailable: 1而集群副本数只剩下 1 个驱逐请求永远不满足。这时候要临时调低副本数或更新 PDB 后再操作。5.5 常见疑惑速查表我整理了一张表把几种容易混淆的配置和处理方式放一起方便你平时快速定位场景使用方式注意事项仅按节点标签选择nodeSelector只支持等值匹配复杂标签条件硬性选择nodeAffinity required多个 term 或关系term 内表达式与关系尽量偏置到某类节点nodeAffinity preferred结合 weight 调整权重避免 Pod 与特定 Pod 同节点podAntiAffinitytopologyKey 决定拓扑域粒度强制 Pod 与特定 Pod 同拓扑域podAffinity注意 namespace 限制阻止普通 Pod 调度到专用节点Taint NoSchedule需配合业务 Pod 的 toleration驱逐已有异常 PodTaint NoExecute无 tolerationSeconds 会立即驱逐只容忍某个 key 的多种 valueToleration Exists不需要写 value容忍所有污点operator: Exists谨慎使用会绕过所有污点6. 实操心得和一些建议我自己维护的集群经历过几次“调度事故”总结下来最有价值的经验是在写调度策略之前先画一张业务拓扑图。哪个服务要同节点、哪个服务要跨节点、哪个节点是专用资源、哪些业务可以容忍延迟这些需求只有在图上明确了才能转换成正确的 affinity 和 taint 组合。不要一上来就写 YAML。另外所有的调度策略都应该通过 GitOps 方式管理把亲和、污点、容忍度当成普通代码一样 review。因为调度策略是分布式系统的隐式约束一旦配错影响的不是一个 Pod而是整个集群的稳定性。我亲身经历过一次为了缩短访问延迟给某个中间件加了硬性 Pod 亲和结果中间件升级时节点故障新副本找不到同节点目标服务崩溃。最后加上preferred和topologyKey改用 zone 粒度才平衡了延迟和可用性。最后想提醒的是调度器和 kubelet 之间并不是实时同步的。修改节点标签后可能需要等待几十秒才能被调度器感知同样给节点加污点后Pod 的调度也会有一段传播延迟。如果你在生产环境做应急变更先观察 1-2 分钟不要因为响应不及时就重复操作。这些知识看起来零碎但真正决定你能否稳得住集群的往往是这些细节。希望这篇文章能帮你少踩一些调度相关的坑。