☰
Kubernetes调度亲和度与容忍度:从原理到避坑实践
2026/10/16 11:56:41 网站建设 项目流程

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,它确实能解决一部分问题。比如目标节点打了disktype=ssd的标签,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 disktype=ssd kubectl label node node-01 node-role.k8s.io/database=

查看某个节点是否带指定标签,或者直接列出集群里所有节点及其标签:

kubectl get nodes --show-labels kubectl get nodes -l disktype=ssd

Pod调度上去之后,验证方式也很简单:

kubectl get pods -o wide kubectl describe pod <pod-name>

describe输出里会有Node-Selectors、Tolerations等字段,Events区域会写清楚节点过滤和打分的过程,排错基本从这看起。

2.4 节点亲和使用中的两个高频坑

第一个坑是硬性条件范围写得过死。比如只允许调度到某个特定节点kubernetes.io/hostname=node-01,扩容时集群里就这一台机器满足条件,其他Pod只能Pending。更合理的做法是选一个足够大的标签集合,比如按可用区、按机型、按磁盘类型来描述节点池,而不是按单台主机名。

第二个坑是标签拼写不一致。我见过不少案例,节点上实际标签是小写的disktype=ssd,Pod里因为某个同事习惯写了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不会卡死。

如果业务强依赖多副本必须跨节点,那就得用requiredDuringSchedulingIgnoredDuringExecution:

affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - web topologyKey: kubernetes.io/hostname

硬性反亲和的代价是,节点数小于副本数时,后面的副本会一直Pending。而且这里有个容易被忽略的细节:如果某个节点上还有一个处于Failed或Pending状态的旧Pod也带着app=web标签,新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)则把主动权交还给了节点——节点可以“歧视”某些Pod,Pod只有声明了对应的容忍度才能被允许调度上来。

4.1 污点的三种效果

给节点打污点的方式非常简单:

kubectl taint nodes node-01 gpu=present:NoSchedule

污点由三部分组成:key、value和effect。key是必须的,value可以省略,effect决定了排斥的强度。effect有三种:

  • NoSchedule:没有对应容忍度的Pod不会被调度到该节点,已运行的Pod不受影响
  • PreferNoSchedule:软性拒绝,尽量避免把Pod调度上来,但不保证绝对不调度
  • NoExecute:不仅新Pod调度不上去,节点上已有的且没有对应容忍度的Pod还会被立刻驱逐

这个区别极其重要。生产环境里经常有人图省事直接打NoExecute,结果节点上一堆现有Pod瞬间“裸奔”,引发大面积重启。

4.2 容忍度的写法:Equal 还是 Exists

Pod里声明容忍度的方式有两种,概念和节点亲和的表达式类似。第一种是精确匹配value:

tolerations: - key: gpu operator: Equal value: "present" effect: NoSchedule

第二种是只关心key,不管value是什么:

tolerations: - key: gpu operator: Exists effect: NoSchedule

Exists写法适合只关心“这个污点是否存在”的场景,比如只要节点不是GPU污点专供,某个监控Pod就能上去。Equal则更适合指定具体instance。多写少写一个value,容忍就可能不生效,这是最常见的低级错误。

4.3 系统自带的污点和默认容忍

Kubernetes集群中有几个系统级污点不需要大家手动打,但需要知道它们在干什么。比如node.kubernetes.io/not-ready表示节点未就绪,node.kubernetes.io/unreachable表示节点不可达,这些污点默认带NoExecute效果。普通Pod默认对这些污点有300秒的容忍时长,也就是说节点失联5分钟后,节点上的Pod开始被驱逐。这是Kubernetes主动驱逐故障节点Pod的机制。

另一个常见的系统污点在master(control-plane)节点上,一般是node-role.kubernetes.io/control-plane:NoSchedule,所以普通业务Pod不会调度到master节点上,这也是为了控制面稳定。

4.4 专用节点的经典组合拳

污点和容忍度单独用的价值有限,真正经典的方案是污点 + 标签 + 节点亲和 + 容忍度四件套。以GPU节点为例:

  1. 给节点打标签,区分资源类型:kubectl label node gpu-node-01 gpu=rtx3080
  2. 给节点打污点,防止无关Pod占用:kubectl taint nodes gpu-node-01 gpu=rtx3080:NoSchedule
  3. 业务Pod里同时声明节点亲和(要求gpu标签)和容忍(容忍gpu=rtx3080: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) didn't match node affinity, 3 node(s) insufficient memory。看到这种报错,思路就清晰了:要么调大节点资源,要么放宽亲和范围,要么降低资源请求。

6. 排查实录:亲和度和容忍度失效的常见现场

理论讲再多,不如记录几个真实排查过程。下面四个case是生产环境里最高频的亲和度/容忍度事故现场。

6.1 Case 1:Pod一直Pending,Events提示“didn't match node affinity”

现象:新扩容的Pod状态一直是Pending,describe出来类似这样:

Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 95s default-scheduler 0/3 nodes are available: 3 node(s) didn't match node affinity

定位步骤:先看节点上有哪些标签,再看Pod的affinity配置,两者对齐。多数情况是节点标签根本没打,或者打到了另一台机器上。修复就是补齐标签,或者修改Pod的affinity配置降低硬性约束。这里有个有效的小技巧:启动多个Pod副本时,别一个个看,直接kubectl get pods -o wide | grep Pending看分布,再用kubectl describe挑一个看详细报错。

6.2 Case 2:反亲和把Pod卡死,节点明明有空位

现象:为了保证高可用,对3个副本使用了requiredDuringSchedulingIgnoredDuringExecution的podAntiAffinity,topologyKey是kubernetes.io/hostname。集群有3台节点,每个节点都空着,但第三副本就是Pending。

定位过程:逐个describe节点节点和Pod,发现前两个副本被调度到了node-01和node-02,第三个副本找不到合适的节点。再一看,node-03上有一个旧的app=web标签的Pod处于Unknown状态,虽然Pod已经不提供服务,但调度器在做反亲和统计时会把它算进去。清理掉这个残留Pod,第三个副本立刻调度成功。

从这个案例得到的教训是:用硬性反亲和之前,一定先确保集群里没有大量状态异常的残留Pod。日常维护时可以定时清理垃圾Pod,降低这种“明明有空位却调度不动”的概率。

6.3 Case 3:tolerations写错导致Pod进不了专用节点

现象:节点有gpu=present:NoSchedule污点,Pod配了toleration但仍然Pending。describe时报错:

0/4 nodes are available: 2 node(s) had taint {gpu=present: NoSchedule}, that the pod didn't 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污点,整个节点被清空

现象:给某台大内存节点打了一个maintenance=yes: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节点池。这是最顶层的硬性范围,保证业务不会乱跑到其他可用区或者普通节点池。

节点软性亲和部分为disktype=ssd的节点加了70分权重。SSD节点会被优先选择,但即使全集群一台SSD节点都没有,Pod也不会因此Pending,只是会落在普通节点上。

Pod反亲和部分用100分权重引导4个副本尽量打散到不同节点。没加required,是为了避免集群节点数少于4时出现无解Pending。实际上如果只有3台节点,第4个副本会和已有副本挤在同一台,但它依然能启动。

Pod亲和部分用50分权重引导同一可用区内靠近缓存服务。这个权重设得比反亲和低,是因为暴露在公网的API服务更看重可用性打散,其次才是降低延迟。

容忍部分让Pod可以调度到打了node-pool=high-perf:NoSchedule污点的节点池。整个混布的关键在于:节点池通过污点把普通流量挡在外面,只有带了对应容忍度的服务才能进来;同时节点标签又保证带有容忍度的服务不会跑到普通节点池去。这两者必须同时配置,缺一不可。

验证这套配置是否生效,最直接的方式是:

kubectl get pods -l app=saas-api -o wide

检查每个Pod分布在哪些节点,是否分散、是否在预期可用区。还可以看节点上的Pod列表:

kubectl describe node <node-name>

该命令会列出节点上的所有Pod、标签和污点,一眼就能确认有没有非预期Pod混进专用节点。

这套模板在生产环境跑了一段时间后,我还根据经验加了一条:所有多副本服务,至少加一条preferred的podAntiAffinity到hostname维度,成本几乎为零,但能挡住不少“一挂挂一片”的串联故障。至于污点这层,我的习惯是“标签决定来源,污点决定门槛”——只打标签不给污点的专用节点,等于没锁门的机房,谁都能进,监控告警都拦不住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询