Kubernetes调度亲和度与容忍度:从原理到避坑实践

发布时间:2026/10/16 11:56:48
Kubernetes调度亲和度与容忍度:从原理到避坑实践 Kubernetes的默认调度器其实只会办一件事在资源够用的节点里挑一个分数最高的。至于你的业务想和谁待在一起、不想挨着谁、哪台机器是专属的、哪些流量不能上去它一概不关心。解决这些问题的就是调度体系里的亲和度affinity和容忍度toleration。这两块看起来都在讲“Pod往哪放”但作用维度完全不一样——亲和度表达的是“我想去哪”容忍度表达的是“节点不让我去我还能不能去”。今天我不打算照着官方文档念一遍而是把这些年配 affinity、写 tolerations 踩过的坑、验证过的写法、复杂场景下的取舍拿出来聊透可以说清楚概念也可以当配置速查。1. 默认调度器的短板为什么单靠资源打分不够用不少刚接触Kubernetes的同学以为调度就是把Pod放到一台空闲的机器上简单得很。真到生产环境跑几个月你会发现单纯按资源打分远远不够甚至会在关键时刻坑你一把。1.1 默认调度器都在干什么Kubernetes自带的调度器工作过程可以粗略分成两步。第一步是过滤Filter把所有不满足硬性条件的节点筛掉比如CPU内存不够、端口被占用、节点处于不可调度状态等第二步是打分Score把剩余节点按资源富余程度、镜像是否已存在、污点容忍情况等因素打个分分数最高的那个节点被选中。这套机制本身的“视野”是资源视角。它不关心你的业务有多个副本且这些副本最好别在一台机器上也不关心某个计算任务必须跑在特定硬件上更不关心哪些节点属于某个团队专用。默认情况下两个数据库实例被调度到同一台物理机是完全正常的节点一宕机高可用就变成了笑话。1.2 生产环境里最常出现的三个诉求我总结了一下业务方提得最多的调度诉求基本就三类。第一类是打散。希望多副本尽量分布在不同节点、不同可用区把故障域摊开。典型场景是核心数据库、网关、消息队列等有状态服务哪怕只有一个节点挂了也不能让整个服务一起挂。第二类是专用。某些节点配了GPU、大内存、高速SSD或者特定的网卡不希望所有Pod都挤上来。典型场景是机器学习训练任务占用的GPU机器、日志采集和监控用的高性能节点。第三类是靠近或隔离。有的Pod和服务之间有强依赖最好放在同一个节点甚至同一个可用区以减少网络延迟而有的Pod互相之间必须离远一点避免资源争抢或故障传导。这三类诉求单靠默认调度器都无法直接表达。于是Kubernetes提供了两套互补的机制亲和度负责表达“我想去哪/想和谁近/想离谁远”容忍度负责在节点有“专属标记”污点时让特定Pod获得“入场券”。下面分开讲透。2. 节点亲和把Pod送到符合硬件、地域、归属的节点上节点亲和nodeAffinity解决的核心问题是“Pod应该落在什么样的节点上”。它能根据节点的标签做精确匹配既有硬性条件也有软性偏好。2.1 为什么有了 nodeSelector 还要 nodeAffinity很多老开发者第一次接触节点选择时用的是 nodeSelector它确实能解决一部分问题。比如目标节点打了disktypessd的标签Pod里写nodeSelector: {disktype: ssd}就能把Pod调度到SSD节点上。但nodeSelector的表达能力太弱。它只支持“key等于某个value”这一种关系无法表达“节点不在某个污点域里”“节点属于A或B组”“只要节点存在某个标签”。而nodeAffinity支持更丰富的操作符In、NotIn、Exists、DoesNotExist、Gt、Lt还可以叠加多条规则支持硬性和软性两种模式。从实用性上说nodeAffinity基本可以全面替代nodeSelector而且更推荐在新配置里直接用nodeAffinity。2.2 required 和 preferred硬性与软性的差别nodeAffinity里最核心的两个字段是requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution。名字又长又拗口但拆开看就好理解了。前半段required/preferred是说这条规则是“必须满足”还是“尽量满足”。后半段IgnoredDuringExecution的意思是这条规则只在调度那一刻生效节点标签在Pod运行后无论怎么变系统都不会重新调度这个Pod。这一点很关键——很多人误以为给节点改了标签已经跑在上面的Pod会跟着走其实不会。用一段YAML来演示最常见的写法affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd - key: node-role.k8s.io/database operator: Exists preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - cn-east-1a这段配置的含义是硬性条件必须是SSD节点且存在node-role.k8s.io/database标签软性偏好是尽量在cn-east-1a这个可用区。注意nodeSelectorTerms里的多个matchExpressions之间是逻辑与的关系必须全部满足而如果你的配置里写了多个nodeSelectorTerms多个nodeSelectorTerms之间是逻辑或的关系满足任意一组即可。这个细节写错会导致调度结果完全不符合预期。2.3 给节点打标签和验证节点亲和依赖节点标签标签没打好配置写得再对也没用。常用的打标签命令kubectl label node node-01 disktypessd kubectl label node node-01 node-role.k8s.io/database查看某个节点是否带指定标签或者直接列出集群里所有节点及其标签kubectl get nodes --show-labels kubectl get nodes -l disktypessdPod调度上去之后验证方式也很简单kubectl get pods -o wide kubectl describe pod pod-namedescribe输出里会有Node-Selectors、Tolerations等字段Events区域会写清楚节点过滤和打分的过程排错基本从这看起。2.4 节点亲和使用中的两个高频坑第一个坑是硬性条件范围写得过死。比如只允许调度到某个特定节点kubernetes.io/hostnamenode-01扩容时集群里就这一台机器满足条件其他Pod只能Pending。更合理的做法是选一个足够大的标签集合比如按可用区、按机型、按磁盘类型来描述节点池而不是按单台主机名。第二个坑是标签拼写不一致。我见过不少案例节点上实际标签是小写的disktypessdPod里因为某个同事习惯写了DiskType结果硬性条件永远不满足Pod一直Pending还排查了半天。关键是在root权限下先跑一遍kubectl get nodes --show-labels核对标签再写配置。3. Pod亲和与反亲和控制Pod之间的“关系距离”节点亲和管的是“Pod与节点的关系”Pod亲和podAffinity和Pod反亲和podAntiAffinity管的则是“Pod与Pod的关系”。这在多副本高可用和依赖服务就近部署时特别好用。3.1 topologyKey 是核心机制Pod亲和和反亲和的写法里都有一个关键字段topologyKey——它定义了“在哪个拓扑维度上判断Pod关系”。最常用的是三个层级kubernetes.io/hostname单节点维度同一节点内算“同一个拓扑域”topology.kubernetes.io/zone可用区维度同一个可用区内算“同一个拓扑域”topology.kubernetes.io/region地域维度同一个地域内算“同一个拓扑域”调度器在做亲和判断时会先看目标节点上已运行的Pod是否满足labelSelector条件然后根据topologyKey判断这些Pod是否和当前要调度的Pod在同一个拓扑域内。如果是反亲和那么同一个拓扑域内已经存在匹配的Pod当前Pod就不能再调度进去。3.2 用反亲和实现多副本打散假设有一个Deployment跑三个Web副本要求它们不能都在同一台节点上。配置可以这样写affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - web topologyKey: kubernetes.io/hostname这里用的是preferred软性意思是调度器优先把副本分散到不同节点但如果节点数不够比如集群只有两台第三副本还是可以被调度到已有副本的节点上Pod不会卡死。如果业务强依赖多副本必须跨节点那就得用requiredDuringSchedulingIgnoredDuringExecutionaffinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - web topologyKey: kubernetes.io/hostname硬性反亲和的代价是节点数小于副本数时后面的副本会一直Pending。而且这里有个容易被忽略的细节如果某个节点上还有一个处于Failed或Pending状态的旧Pod也带着appweb标签新Pod依然会被“算作冲突”明明节点空着也调度不进去。遇到这种问题先清理残留Pod。3.3 用Pod亲和实现就近部署与反亲和相反Pod亲和用于把相关Pod放到同一个拓扑域内。最典型的场景业务Pod和Redis缓存Pod最好在同一个可用区内减少跨区访问的网络延迟和费用。affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 60 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - redis-cache topologyKey: topology.kubernetes.io/zone这段配置会指引调度器优先选择那些已经部署了redis-cachePod的可用区。注意这里也是preferred因为如果用required一旦缓存服务所在节点全部拉满或异常业务Pod也会跟着无处可去。3.4 打散和就近混合使用时的克制有些团队一上来就把亲和、反亲和的necessary全部堆上结果整个集群的Pod调度像走迷宫任何一个节点标签调整都会引起连锁Pending。我的经验是反亲和这种约束最怕“硬”加“细”。拓扑粒度越细约束越严格副本数越多无解的概率越大。能用preferred尽量用preferred硬性反亲和只留给真正“一挂全挂”的核心组件。而Pod亲和最好配合节点亲和一起用否则调度器只知道“要和缓存放一起”不知道“应该去哪个可用区找缓存”反而容易绕圈子。4. 污点与容忍度节点有权利拒绝你不想要的Pod前面讲的亲和度是Pod主动提出诉求节点只能被动接受。污点taint和容忍度toleration则把主动权交还给了节点——节点可以“歧视”某些PodPod只有声明了对应的容忍度才能被允许调度上来。4.1 污点的三种效果给节点打污点的方式非常简单kubectl taint nodes node-01 gpupresent:NoSchedule污点由三部分组成key、value和effect。key是必须的value可以省略effect决定了排斥的强度。effect有三种NoSchedule没有对应容忍度的Pod不会被调度到该节点已运行的Pod不受影响PreferNoSchedule软性拒绝尽量避免把Pod调度上来但不保证绝对不调度NoExecute不仅新Pod调度不上去节点上已有的且没有对应容忍度的Pod还会被立刻驱逐这个区别极其重要。生产环境里经常有人图省事直接打NoExecute结果节点上一堆现有Pod瞬间“裸奔”引发大面积重启。4.2 容忍度的写法Equal 还是 ExistsPod里声明容忍度的方式有两种概念和节点亲和的表达式类似。第一种是精确匹配valuetolerations: - key: gpu operator: Equal value: present effect: NoSchedule第二种是只关心key不管value是什么tolerations: - key: gpu operator: Exists effect: NoScheduleExists写法适合只关心“这个污点是否存在”的场景比如只要节点不是GPU污点专供某个监控Pod就能上去。Equal则更适合指定具体instance。多写少写一个value容忍就可能不生效这是最常见的低级错误。4.3 系统自带的污点和默认容忍Kubernetes集群中有几个系统级污点不需要大家手动打但需要知道它们在干什么。比如node.kubernetes.io/not-ready表示节点未就绪node.kubernetes.io/unreachable表示节点不可达这些污点默认带NoExecute效果。普通Pod默认对这些污点有300秒的容忍时长也就是说节点失联5分钟后节点上的Pod开始被驱逐。这是Kubernetes主动驱逐故障节点Pod的机制。另一个常见的系统污点在mastercontrol-plane节点上一般是node-role.kubernetes.io/control-plane:NoSchedule所以普通业务Pod不会调度到master节点上这也是为了控制面稳定。4.4 专用节点的经典组合拳污点和容忍度单独用的价值有限真正经典的方案是污点 标签 节点亲和 容忍度四件套。以GPU节点为例给节点打标签区分资源类型kubectl label node gpu-node-01 gpurtx3080给节点打污点防止无关Pod占用kubectl taint nodes gpu-node-01 gpurtx3080:NoSchedule业务Pod里同时声明节点亲和要求gpu标签和容忍容忍gpurtx3080:NoSchedule这样普通流量不会误入GPU节点而训练任务可以精确落到指定GPU机型上。没有亲和度只加容忍度Pod确实能上到节点但调度器可能把你放到GPU节点也可能放到普通节点没有容忍度只加亲和度Pod满足了标签条件但被污点拒之门外依然无法调度。两者必须成对出现。5. 调度器内部是怎么消费这些规则的理解了配置项之后很有必要从调度器的视角走一遍流程。很多人配置写得看起来正确却总不生效就是因为不清楚这些规则是在哪个环节被评估的。5.1 一级过滤硬性条件一票否决一个Pod进入调度流程后调度器会先对所有节点做过滤。这个阶段类似面试初筛——只要有一条硬性条件不满足节点直接出局后面所有的打分都跟你无关。过滤阶段主要检查这些内容节点是否可调度unschedulable资源是否足够是否满足nodeSelector和nodeAffinity的required条件是否满足podAffinity/podAntiAffinity的required条件是否容忍节点的污点端口、存储卷等约束你可以看到污点容忍、亲和度都在这个阶段起作用。这个阶段是逻辑与关系所有条件必须同时满足。如果一个节点因为磁盘类型不匹配被筛掉了后面给它加再高的软性偏好分也不会有结果。5.2 二级打分软性偏好是加法不是乘法通过过滤的节点进入打分环节。这个阶段里preferredDuringSchedulingIgnoredDuringExecution的节点亲和、软性Pod反亲和、资源富余程度、镜像是否已缓存等都会按权重加分。比如配置里写了两个软性偏好SSD节点权重80可用区cn-east-1a权重60。一个节点同时满足两个偏好加140分另一个只满足SSD加80分。调度器大概率选择140分的节点但不绝对——如果140分的节点带宽资源已经极度紧张而另一个节点空闲资源充裕资源打分项可能反超。这符合我的经验软性亲和只负责“引导”不负责“保证”。5.3 为什么有的规则“看着没用”这类问题在答疑群里出现过无数次配置明明写了亲和度Pod还是调度到了不符合条件的节点上。最常见的原因是混淆了required和preferred。硬性约束会出现在过滤阶段效果是“不符合就不给调度”软性约束只参与打分效果是“尽量选但实在不行也能凑合”。因此如果你用的是preferred就不能期望它产生硬性效果。还有一种情况是虽然写了required的节点亲和但Pod同时还有其他硬性约束比如资源要求特别高导致满足亲和条件的节点全因资源不够被筛选出去了最终整个集群没有可用节点Pod一直Pending。这时describe pod的Events里会明确写出卡在哪一步比如0/4 nodes are available: 1 node(s) didnt match node affinity, 3 node(s) insufficient memory。看到这种报错思路就清晰了要么调大节点资源要么放宽亲和范围要么降低资源请求。6. 排查实录亲和度和容忍度失效的常见现场理论讲再多不如记录几个真实排查过程。下面四个case是生产环境里最高频的亲和度/容忍度事故现场。6.1 Case 1Pod一直PendingEvents提示“didnt match node affinity”现象新扩容的Pod状态一直是Pendingdescribe出来类似这样Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 95s default-scheduler 0/3 nodes are available: 3 node(s) didnt match node affinity定位步骤先看节点上有哪些标签再看Pod的affinity配置两者对齐。多数情况是节点标签根本没打或者打到了另一台机器上。修复就是补齐标签或者修改Pod的affinity配置降低硬性约束。这里有个有效的小技巧启动多个Pod副本时别一个个看直接kubectl get pods -o wide | grep Pending看分布再用kubectl describe挑一个看详细报错。6.2 Case 2反亲和把Pod卡死节点明明有空位现象为了保证高可用对3个副本使用了requiredDuringSchedulingIgnoredDuringExecution的podAntiAffinitytopologyKey是kubernetes.io/hostname。集群有3台节点每个节点都空着但第三副本就是Pending。定位过程逐个describe节点节点和Pod发现前两个副本被调度到了node-01和node-02第三个副本找不到合适的节点。再一看node-03上有一个旧的appweb标签的Pod处于Unknown状态虽然Pod已经不提供服务但调度器在做反亲和统计时会把它算进去。清理掉这个残留Pod第三个副本立刻调度成功。从这个案例得到的教训是用硬性反亲和之前一定先确保集群里没有大量状态异常的残留Pod。日常维护时可以定时清理垃圾Pod降低这种“明明有空位却调度不动”的概率。6.3 Case 3tolerations写错导致Pod进不了专用节点现象节点有gpupresent:NoSchedule污点Pod配了toleration但仍然Pending。describe时报错0/4 nodes are available: 2 node(s) had taint {gpupresent: NoSchedule}, that the pod didnt tolerate这就奇怪了明明写了容忍为什么还说不容忍检查YAML发现Pod里toleration写的是tolerations: - key: gpu operator: Exists effect: NoExecute问题出在effect写成了NoExecute而节点污点是NoSchedule。容忍度只在effect完全匹配时才生效。把effect改成NoSchedule后Pod顺利调度。这个case提醒大家key、value、effect三要素必须和污点完全一致Exists操作符也要求effect匹配除非不指定effect此时会匹配所有effect但在容忍度里不指定effect其实会更宽松一般不建议这样用。6.4 Case 4随手打了NoExecute污点整个节点被清空现象给某台大内存节点打了一个maintenanceyes:NoExecute污点本意是想把新Pod挡住结果节点上所有没有对应容忍度的Pod在几秒钟内全部被驱逐业务大面积波动。直接原因就是NoExecute的驱逐语义。正确做法是如果只是想防止新Pod调度上来用NoSchedule如果确实想排空节点上的Pod应该用kubectl drain而不是手动打NoExecute污点。即使确实需要驱逐也建议给核心Pod加上tolerationSeconds缓冲时间tolerations: - key: maintenance operator: Equal value: yes effect: NoExecute tolerationSeconds: 3600这样既有驱逐动作又给了业务1小时的过渡时间不至于瞬间全军覆没。7. 综合配置多可用区高可用加专用节点混布的完整模板最后给一套可以直接套用的综合配置把前面所有知识串起来。场景是一个对外API服务两个可用区部署多副本尽量打散到不同节点同时希望这个服务优先落在有SSD磁盘的高性能节点上而且该节点池通过污点做了隔离只有这个服务能上。apiVersion: apps/v1 kind: Deployment metadata: name: saas-api spec: replicas: 4 selector: matchLabels: app: saas-api template: metadata: labels: app: saas-api spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - cn-east-1a - cn-east-1b - key: node-pool operator: In values: - high-perf preferredDuringSchedulingIgnoredDuringExecution: - weight: 70 preference: matchExpressions: - key: disktype operator: In values: - ssd podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - saas-api topologyKey: kubernetes.io/hostname podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 50 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - saas-cache topologyKey: topology.kubernetes.io/zone tolerations: - key: node-pool operator: Equal value: high-perf effect: NoSchedule containers: - name: api image: registry.example.com/saas/api:1.4.2 resources: requests: cpu: 0.5 memory: 512Mi limits: cpu: 2 memory: 2Gi逐段拆解这套配置的设计逻辑节点硬性亲和部分限制了只能调度到cn-east-1a和cn-east-1b两个可用区且必须属于high-perf节点池。这是最顶层的硬性范围保证业务不会乱跑到其他可用区或者普通节点池。节点软性亲和部分为disktypessd的节点加了70分权重。SSD节点会被优先选择但即使全集群一台SSD节点都没有Pod也不会因此Pending只是会落在普通节点上。Pod反亲和部分用100分权重引导4个副本尽量打散到不同节点。没加required是为了避免集群节点数少于4时出现无解Pending。实际上如果只有3台节点第4个副本会和已有副本挤在同一台但它依然能启动。Pod亲和部分用50分权重引导同一可用区内靠近缓存服务。这个权重设得比反亲和低是因为暴露在公网的API服务更看重可用性打散其次才是降低延迟。容忍部分让Pod可以调度到打了node-poolhigh-perf:NoSchedule污点的节点池。整个混布的关键在于节点池通过污点把普通流量挡在外面只有带了对应容忍度的服务才能进来同时节点标签又保证带有容忍度的服务不会跑到普通节点池去。这两者必须同时配置缺一不可。验证这套配置是否生效最直接的方式是kubectl get pods -l appsaas-api -o wide检查每个Pod分布在哪些节点是否分散、是否在预期可用区。还可以看节点上的Pod列表kubectl describe node node-name该命令会列出节点上的所有Pod、标签和污点一眼就能确认有没有非预期Pod混进专用节点。这套模板在生产环境跑了一段时间后我还根据经验加了一条所有多副本服务至少加一条preferred的podAntiAffinity到hostname维度成本几乎为零但能挡住不少“一挂挂一片”的串联故障。至于污点这层我的习惯是“标签决定来源污点决定门槛”——只打标签不给污点的专用节点等于没锁门的机房谁都能进监控告警都拦不住。