1. K8s调度入门:先把“把Pod放到哪台机器”这件事想清楚
做Kubernetes的同学,早晚都会撞上调度这个话题。刚接触时你可能觉得调度不过就是“把Pod放到一个Node上跑起来”,但真上了生产环境,你会发现事情远没那么简单——节点标签怎么打、Pod该不该往某类机器上放、如何避免两个副本挤在同一台物理机、某个节点要下线维护怎么办,这些全是调度要管的事。
我最早踩坑是在一次压测环境里。当时业务方要求把核心服务均匀打散到三台高配机器上,我没做任何亲和性配置,只靠ReplicaSet默认调度。结果Kubernetes调度器把三个副本全部塞进了一台机器,另外两台空转,那台机器CPU直接打满,服务延迟飙到不可接受。从那以后我就明白,调度这事不能靠运气,必须把节点亲和、Pod亲和、污点容忍这些机制真正吃透,并且清楚它们各自管什么、怎么组合用。
这个标题里提到的内容,其实是三条完全不同的调度控制路径:节点亲和和反亲和解决的是“Pod和Node之间”的选择关系,Pod亲和和反亲和解决的是“Pod和Pod之间”的部署关系,而Taints污点和Tolerations容忍解决的是“Node主动拒绝哪些Pod”的问题。它们三个层面互相独立,但又经常需要配合使用。这篇文章就围绕这三条路径,把原理、配置、实操和排障一次讲透。
适合谁来读?如果你已经在用Kubernetes部署应用,但总觉得调度不可控、Pod落在哪全靠运气;或者你准备设计多可用区、多机型混部的集群,想把Pod的分布规则表达清楚;又或者你只是被“亲和”“污点”这些概念绕得头晕——这篇文章就是给你写的。
2. 先建立一个整体认知:调度的几种手段分别管什么
2.1 调度器到底在做什么
Kubernetes默认的调度器是kube-scheduler,它干的事情可以简化成一句话:为每个待调度的Pod找到一个最合适的Node。这个过程分两步,先过滤(Filtering),后打分(Scoring)。
过滤阶段,调度器会把完全不能满足Pod要求的Node剔除掉。比如Pod声明了要2个CPU,那CPU不足的Node直接被淘汰;Pod挂了一个节点亲和规则,指定必须跑在带有disk=ssd标签的节点上,那不带这个标签的Node全部出局。这个阶段是“硬约束”,不满足就拉倒。
打分阶段,调度器对剩下的Node逐一打分,分数高的优先。默认打分规则很多,包括资源余量、Pod分布情况、节点上已有的Pod数量等等。你配置的亲和规则如果是preferred类型(软性偏好),就是在打分阶段加分,而不是在过滤阶段强制筛选。
明白这两个阶段非常重要。很多人配置亲和性时搞不清楚为什么有的规则没生效,原因往往就是他把“软规则”当成了“硬规则”来用。硬规则是required,不满足就不调度;软规则是preferred,不满足最多是分数低,Pod照样能调度上去。
2.2 三种调度控制路径的关系
NodeSelector、节点亲和、Pod亲和/反亲和、污点容忍,这几种机制不是替代关系,而是互补关系:
| 控制路径 | 作用对象 | 表达的问题 | 硬/软 |
|---|---|---|---|
| nodeSelector | Pod → Node | 我只想去带某标签的机器 | 硬 |
| 节点亲和(nodeAffinity) | Pod → Node | 我倾向于去某类机器,也可以限定必须去 | 硬/软都支持 |
| Pod亲和(podAffinity) | Pod → Pod | 我要和某类Pod待在同一个拓扑域 | 硬/软都支持 |
| Pod反亲和(podAntiAffinity) | Pod → Pod | 我不要和某类Pod待在同一个拓扑域 | 硬/软都支持 |
| Taints + Tolerations | Node 主动 → Pod | 某些Pod不准上来,除非你有“通行证” | 硬(默认) |
举一个实际场景来串起这几种手段:你有一套机器学习训练集群,gpu节点打上了gpu=tesla的标签,普通业务节点没打。训练任务Pod用节点亲和强制要求跑到gpu=tesla的节点上;为了保证训练稳定性,再用Pod反亲和让同一个训练任务的不同副本不要挤在同一台GPU机器上;同时,给GPU节点打一个污点,防止那些没声明容忍的普通Pod“蹭机”被调度上来。三者配合,才算把调度规则表达完整。
这个例子你应该能看出来,节点亲和和污点做的事情并不重复。节点亲和是Pod主动选择Node,污点是Node主动拒绝Pod。即便你给Pod写了required节点亲和,只要Node上有污点而Pod没容忍,Pod依然进不去。两者相遇时,污点的优先级更高。
3. 节点亲和与反亲和:把Pod精准导航到目标机器
3.1 从nodeSelector说起,为什么还要有nodeAffinity
Kubernetes最早提供的节点选择能力是nodeSelector。它非常简单——Pod里声明一个map,比如gpu: tesla,调度器就只把这个Pod放到带gpu=tesla标签的Node上。简单、直观,但也非常死板,表达能力极其有限。
它有几个硬伤。第一,只支持精确匹配key=value,想表达“这个标签存在就行”或者“这个标签的值不等于某个值”就不行。第二,只能写多个条件的AND,没法表达“满足A或B任一即可”。第三,只有硬约束,没有软约束——没法表达“我优先想跑在SSD机器上,但如果没有,普通机器也行”。
节点亲和(nodeAffinity)就是来补这些短板的。它是nodeSelector的进阶版,表达能力强大得多,并且在设计上完全兼容了原有场景。
为了平滑升级,Kubernetes后来在Node上增加了nodeAffinity字段之后,nodeSelector依然被保留。在现在的API里,你可以仍然用nodeSelector,也可以用nodeAffinity,或者两个一起用。如果两个都写了,那调度时会同时满足两者的条件。
3.2 nodeAffinity的两种类型和四种匹配操作符
节点亲和配置里最关键的是schedulingIgnoredDuringExecution这个概念。你可以把它理解成Kubernetes对亲和规则的一种“软硬分离”设计。字段全称很长,拆开来看:
requiredDuringSchedulingIgnoredDuringExecution:调度时必须满足,不满足就调度失败。但Pod已经运行之后,即使Node标签发生变化导致规则不再满足,已经运行的Pod不会被驱逐。preferredDuringSchedulingIgnoredDuringExecution:调度时尽量满足,不满足也能调度,只是优先级低。运行期的行为同上。
后面那个IgnoredDuringExecution其实是执行期忽略的意思,也就是Kubernetes不会因为标签变化去主动把Pod挪走。目前还有requiredDuringExecution之类的计划特性,但到这篇文章写作时,你只需要关注上面两种。
表达匹配条件时,nodeAffinity支持的匹配符有四个:
In:值必须在给定列表中,等效等于nodeSelector的精确匹配NotIn:值不能在给定列表中Exists:标签键存在即可,不关心值DoesNotExist:标签键不存在
另外还有一个Gt和Lt,用于数值型标签的比较大小。这两个操作符适用于标签值是整数形式的场景,比如memory=64这种。
下面是一个完整的示例。假设集群里有三种节点:SSD高IO节点、GPU训练节点、普通节点,对应的标签分别是disk-type=ssd、gpu=true、disk-type=hdd。我们要部署一个数据库Pod,它必须跑在SSD节点上,同时希望尽量跑在CPU主频更高的节点上(假设打了cpu-fast=true标签):
apiVersion: v1 kind: Pod metadata: name: db-pod spec: containers: - name: db image: mysql:8.0 affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disk-type operator: In values: - ssd preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: cpu-fast operator: Exists注意nodeSelectorTerms是个列表,列表里的多个term之间是OR关系。也就是说,只要满足其中一个term就算通过过滤。而单个term内部的多个matchExpressions是AND关系,必须全部满足。
这点非常容易写错。你如果有多个条件需要“同时满足”,一定要把它们放在同一个matchExpressions里。如果你图省事拆成了两个term,那调度器理解的是“满足其中一个就行”,结果可能完全不是你要的。
3.3 weighted权重到底怎么影响调度结果
preferredDuringSchedulingIgnoredDuringExecution里的weight字段,很多人不理解它的作用。简单说:调度器打分时,每满足一条preferred规则,就给节点加上相应的权重分。
多个preferred规则可以各设权重,最终调度器会把所有加分累加到节点总分里。但需要注意,weight值本身没有绝对意义,只有相对意义。你设了80和60,跟设了8和6相比,对最终排序的影响是完全一样的——因为其他打分项的默认权重可能高达几十甚至上百,你的120分只是整体的一个加分项,不能决定一切。
所以有个实操经验:权重值的数量级要根据场景选择。如果想让某条偏好规则真正对调度结果有显著影响,建议权重设在50以上,甚至100;如果只是“聊胜于无”的偏好,可以设10左右。但别指望权重能产生“硬约束”的效果,它永远只是加分项。
3.4 节点反亲和到底存在吗:NotIn和DoesNotExist就是反亲和
严格来说,Kubernetes并没有一个独立叫“节点反亲和”的字段。反亲和的表达能力是通过NotIn和DoesNotExist这两个操作符实现的。你只需要把匹配条件从“我要去”改成“我不要去”就行。
举个例子,某类节点打上了role=spot的标签,表示这些是竞价实例,随时可能被回收。生产环境的Web服务不希望跑在上面:
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: role operator: NotIn values: - spot这一段表达的意思是:role标签的值不能是spot。注意一个细节:如果某个节点压根没有role这个标签,那么NotIn的判断会直接通过——因为NotIn只关心标签存在且值匹配的情况。这跟DoesNotExist是完全不同的语义:前者允许标签存在但值不匹配,也允许标签不存在;后者严格要求标签键不能存在。
这在某些场景下会埋坑。比如你的节点上如果有些机器根本没有打role标签,那NotIn: spot是拦不住这些无标签节点的。如果你希望“只要机器打上了这个标签且值是spot就不能去”,那没问题;如果你希望“没打标签的也不许去”,那你得再加一条DoesNotExist或单独控制标签的覆盖范围。
4. Pod亲和与反亲和:让Pod和Pod之间“抱团”还是“散开”
4.1 为什么要关心Pod和Pod之间的位置关系
节点亲和已经把Pod和Node之间的关系管住了,但很多时候你要表达的需求是“Pod与Pod之间”的位置。
最典型的两个场景:
第一是抱团。比如你有一个Redis缓存和一个业务服务,业务服务对缓存的访问延迟极其敏感,你希望业务Pod和Redis Pod尽量落在同一台机器上,走本机回环网络,而不是跨节点走一遍物理网络。这时候需要Pod亲和。
第二是散开。比如一个无状态服务的多个副本,如果全部堆在同一台机器上,这台机器一挂,整个服务的所有副本全部不可用。为了保证高可用,你需要让这些副本尽量分布到不同的机器上。这时候需要Pod反亲和。
Kubernetes的Pod亲和和反亲和就是专门处理这两种需求的。它跟节点亲和的本质区别在于,Pod亲和的条件不是“Node具备什么标签”,而是“某个Node上已经运行了哪些Pod”。
4.2 topologyKey:Pod亲和里必须弄懂的核心概念
配置Pod亲和时,topologyKey是一个绕不开的字段。它决定的是“同一个拓扑域”到底按什么尺度来划分。
这个字段的值是Node上的一个标签键。Kubernetes用这个标签键来给Node分组。比如:
kubernetes.io/hostname:以机器维度分组,每个Node是一个独立拓扑域,这是最常用的粒度topology.kubernetes.io/zone:以可用区为维度分组,同一个可用区内的Node属于同一个拓扑域topology.kubernetes.io/region:以地域为维度分组
举个例子,如果配置了Pod亲和,要求“和某类Pod处于同一个拓扑域”,topologyKey用的是kubernetes.io/hostname,那调度器会把目标Pod所在的Node作为参照,然后只把新Pod调度到同一台Node上。如果topologyKey换成topology.kubernetes.io/zone,那只要在同一个可用区内,哪怕不同机器也行。
这个字段是必填的。你不写,配置直接校验失败。为什么强制要求?因为Kubernetes需要一个明确的尺度来判定亲和与反亲和,没有尺度,语义就无法成立。
特别注意:Pod反亲和里,topologyKey还不能是空的,而且通常不能用kubernetes.io/hostname以外的超大粒度——当然这不是强制的,只是从高可用角度建议大家认真想清楚。如果你把反亲和的topologyKey设成topology.kubernetes.io/zone,那同一个可用区内就可以同时跑多个副本,这在高可用上的保护力度会弱很多。
4.3 Pod亲和/反亲和的配置实例
下面看一个实际配置。假设你有两个服务,一个是cache,已经通过Deployment部署好了,Pod带app=cache标签;另一个是api服务,希望跟cache的Pod跑到同一台机器上,减少访问延迟:
apiVersion: apps/v1 kind: Deployment metadata: name: api spec: replicas: 3 selector: matchLabels: app: api template: metadata: labels: app: api spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - cache topologyKey: kubernetes.io/hostname containers: - name: api image: myapp/api:v1注意这里有个隐含条件:api和cache必须部署在同一个命名空间内。如果不在同一个命名空间,需要额外加namespaces字段指明要匹配的命名空间列表。
再看反亲和的例子。同一个服务有5个副本,希望它们不要落在同一台机器上:
spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - api topologyKey: kubernetes.io/hostname这段配置的语义是:在调度每个副本时,调度器会检查目标Node上是否已经存在app=api的Pod。如果存在,就拒绝把新的Pod调度上去。这样5个副本就会被尽量分散到5台不同的机器上。
4.4 踩坑实录:Pod反亲和的“自己人”问题
上面这段配置有一个非常经典的坑:反亲和的labelSelector匹配到了自己。因为Deployment的Pod模板本身带有app=api这个标签,所以每次新调度一个Pod时,它都算作“已经存在的目标Pod”的同类。这就意味着第一个Pod调度成功后,第二个Pod不能跟第一个在同一台机器;第三个不能跟第一个、第二个在同一台机器。如果集群里机器数量少于副本数,后面的Pod会一直Pending。
这不是配置错误,而是语义使然。很多人第一次写Pod反亲和时都会遇到“怎么有副本一直Pending不调度”的问题,原因就是这个。解决办法有两个:
- 如果你就是想严格保证每个副本分散在不同机器上,那这个行为是合理的,你需要保证集群有足够多的Node。
- 如果你只是希望避免和别的服务混部,那labelSelector里要选一个能区分“自己副本”和“对方副本”的标签,不要把自身标签圈进去。
另外一个常见做法是软反亲和。为了高可用,你可能并不需要“绝对不许同机”,只是希望调度器“尽量别让我和副本们挤在一起”。这时用preferredDuringSchedulingIgnoredDuringExecution更合适。
spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - api topologyKey: kubernetes.io/hostname软反亲和不会导致调度失败,只是降低分数。集群机器不足时,多个副本也能挤在一台机器上跑起来,只是调度器会优先尝试分散。这个在实际生产里更常用,因为硬反亲和太容易把调度卡死。
5. Taints污点与Tolerations容忍:给Node装上“门禁”
5.1 污点的本质:Node主动把Pod拒之门外
节点亲和、Pod亲和都是从Pod视角出发的:Pod说我想到哪去、想跟谁在一起。但实际运维里,Node自己往往也有诉求。比如某台机器要做内核升级,暂时不想接收新Pod;某台机器是GPU专用机器,不希望普通业务Pod跑上来;某个节点磁盘异常,希望上面的Pod尽快搬走。
这些诉求靠亲和规则是表达不了的。这时候就要用污点(Taints)。
污点是打在Node上的一个标记。一个污点由三部分组成:key、value和effect。调度器在调度Pod时,会检查Node上的所有污点,然后看Pod是否有对应的容忍(Tolerations)。如果Pod不容忍某个污点,同时该污点的effect是NoSchedule级别的,这个Pod就不会被调度到该Node上。
一个很生活化的理解:Node是一间VIP休息室,污点就是门口挂的牌子写着“非请勿入”。Pod想进去,必须有门禁卡,也就是对应的容忍。亲和规则是Pod主动选择Node,污点则是Node定义准入规则。
5.2 三种污点effect分别是什么意思
effect字段决定污点的严格程度,一共有三种:
NoSchedule:新的Pod不调度到该节点上,但已经运行在该节点上的Pod不受影响,可以继续跑。PreferNoSchedule:软性的NoSchedule,调度器会尽量避免把Pod调度到这个节点上,但实在没地方去时也可以调度过来。NoExecute:最严格。不仅新Pod不能调度上来,连已经运行在这个节点上的Pod,如果没有对应的容忍,也会被驱逐。
前两种主要影响“未来”的调度决策,NoExecute会影响“现在”正在运行的Pod。这个区别非常重要,因为做节点维护时,如果你希望把节点上的Pod全部赶走,用NoExecute,而不是手动一个个删除Pod。
5.3 污点管理的实操命令
污点的管理主要通过kubectl命令行完成。常见的操作有三个:
给节点打污点:
kubectl taint nodes node1 key1=value1:NoSchedule这里的key1=value1是污点的键值对,NoSchedule是effect。如果只关心键不关心值,也可以写成key1:NoSchedule。
去掉污点:
kubectl taint nodes node1 key1=value1:NoSchedule-注意末尾的减号,没有它这条命令就是在打污点而不是去掉污点。这是很多人会犯的低级错误。去掉污点时不要求value完全一样,只要key和effect匹配就能移除。
查看节点的污点:
kubectl describe node node1输出信息中会有Taints字段,列出了这个节点上的所有污点。排查调度问题时,第一步就是先看这个字段。
5.4 Pod容忍配置的写法与细节
Pod要容忍某个污点,需要在Pod的spec里加上tolerations字段。最基本的写法:
tolerations: - key: "key1" operator: "Equal" value: "value1" effect: "NoSchedule"这段的意思是:这个Pod容忍key1=value1且effect为NoSchedule的污点。当Node上有这个污点时,Pod可以被调度上去。
还有两种更灵活的写法。
第一种是只关心key,不关心value:
tolerations: - key: "key1" operator: "Exists" effect: "NoSchedule"operator: Exists表示只要污点的key存在、effect匹配,不管value是什么,都能容忍。
第二种是容忍所有污点:
tolerations: - operator: "Exists"这种写法没有指定key和effect,表示无条件容忍任意污点。一般只在特殊场景下用,比如系统组件DaemonSet的Pod,或者你确定这个Pod放到任何节点上都不会有问题。
5.5 NoExecute里还有一个时间参数
在NoExecute污点的容忍配置里,tolerationSeconds是一个非常有用的字段。它表示“我能忍受这个污点多长时间”,超时之后如果污点还在,Pod就会被驱逐。
tolerations: - key: "key1" operator: "Equal" value: "value1" effect: "NoExecute" tolerationSeconds: 60这个配置的意思是:Node上有key1=value1且effect为NoExecute的污点时,Pod会在这个节点上继续运行最多60秒,之后被驱逐。
这个字段的用途很多。比如节点要关机维护,你先给节点打一个NoExecute污点,但允许某些关键Pod多留几分钟做优雅退出。时间一到,Pod自然被驱离,比你手动数秒去删Pod优雅多了。
5.6 Kubernetes自动添加的系统级污点
生产环境中,很多节点上的污点并不是人打的,而是Kubernetes节点控制器自动添加的。常见的有:
node.kubernetes.io/not-ready:节点未就绪,effect为NoExecutenode.kubernetes.io/unreachable:节点不可达,effect为NoExecutenode.kubernetes.io/disk-pressure:节点磁盘压力过大node.kubernetes.io/memory-pressure:节点内存压力过大node.kubernetes.io/network-unavailable:节点网络不可用
默认情况下,Pod对这些系统级污点是没有任何容忍的。所以当一个节点突然变成NotReady状态时,Kubernetes会触发Pod驱逐流程,把Pod从该节点上移走,重新调度到其他健康节点。
但有一个细节:在节点宕机或不可达的情况下,Pod并不是立即被驱逐,而是等待一个默认的容忍时间(通常是300秒)。这个时间由Pod的tolerationSeconds控制,默认值由集群控制器设置。要通过这个机制,快速感知Pod失联并把工作负载迁走,可以给核心Pod配置更短的tolerationSeconds,增加故障响应速度。比如:
tolerations: - key: "node.kubernetes.io/unreachable" operator: "Exists" effect: "NoExecute" tolerationSeconds: 30这样节点不可达后,这个Pod只会等待30秒就会被调度到别的节点上,服务恢复速度会快很多。
6. 综合实战:把亲和、反亲和、污点组合起来用
6.1 一个完整的调度方案长什么样
前面分别介绍了三种机制,但真实生产环境里它们往往是组合出场的。我拿一个模拟项目场景来演示完整配置,这个项目是一个典型的多层Web服务,包含前端Nginx、后端API、数据库MySQL,部署在三类节点上:
node-group=web:普通Web节点,数量多,配置均衡node-group=db:数据库专用节点,带SSD和较高内存,打了污点db=dedicated:NoSchedulenode-group=gpu:GPU节点,给了机器学习任务专用
部署需求:
- 前端Nginx副本要分散在不同机器上,高可用优先。
- 后端API要跟Redis跑到同一台机器,降低延迟。
- 数据库MySQL只跑到数据库专用节点,并且不能让别的Pod蹭上来。
- GPU节点不允许普通业务Pod进入。
完整配置思路如下。
数据库节点的污点提前打好:
kubectl taint nodes db-node-1 db=dedicated:NoScheduleMySQL的Deployment配置:
apiVersion: apps/v1 kind: Deployment metadata: name: mysql spec: replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: tolerations: - key: "db" operator: "Equal" value: "dedicated" effect: "NoSchedule" affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-group operator: In values: - db containers: - name: mysql image: mysql:8.0这个配置的两层含义:第一层,MySQL有db=dedicated污点的容忍,能进入数据库专用节点;第二层,节点亲和强制要求node-group=db,确保它只落在数据库节点上。两个条件叠加,别的节点它不去,专用节点上它进得去。
再给数据库节点打一个反向保护:不允许非数据库Pod跑上来。这个可以用污点实现,但要注意,如果只是打了污点,那些带有db容忍的Pod都能进来。如果还要限制得更死,需要配合Pod反亲和或准入策略。生产上通常会结合命名空间隔离来管理。
API服务的配置:
apiVersion: apps/v1 kind: Deployment metadata: name: api spec: replicas: 3 selector: matchLabels: app: api template: metadata: labels: app: api spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - redis topologyKey: kubernetes.io/hostname podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - api topologyKey: kubernetes.io/hostname containers: - name: api image: myapp/api:v1这里有一个值得注意的问题:API Pod要求跟Redis同机,但同时有3个副本,如果集群里Redis只部署了1个副本,那3个API Pod会全部挤到Redis所在的那台机器上。虽然反亲和是preferred只能尽量分散,但同机要求是required,实际上3个API Pod依然会被塞到同一台机器。所以部署之前要评估清楚:如果API是多副本的,Redis也应该做成多副本,或者把Pod亲和改成preferred而不是required。否则亲和规则会跟副本数打架,反而制造热点。
6.2 多种机制叠加时的调度判断顺序
当节点亲和、Pod亲和、污点同时存在时,调度器是怎么判断的?
在过滤阶段,调度器会校验所有硬性条件:
- 先看Node上的污点,Pod有没有对应的容忍?没有容忍且effect是NoSchedule或NoExecute,直接淘汰。
- 再看节点亲和里的
requiredDuringScheduling,Node标签是否匹配?不匹配直接淘汰。 - 再看Pod亲和/反亲和里的
requiredDuringScheduling,目标Node上的Pod是否满足条件?不满足直接淘汰。
过滤完之后,进入打分阶段。此时preferredDuringSchedulingIgnoredDuringExecution的各项规则开始发挥作用,加上权重分,最终选出一个得分最高的Node。
这个顺序是Kubernetes调度器内置的逻辑,你不需要去改它,但理解它很重要。因为排查调度问题时,你得知道哪个环节卡住了:是污点不让进?还是亲和条件不满足?还是只是分数不够高、被别的Node比下去了?判断方法就是挨个看,先看Taints,再看亲和条件,最后看调度结果里有没有Pending的Pod以及事件信息。
6.3 Operator的Equal和Exists到底怎么选
配置容忍时,operator字段有两种取值:Equal和Exists。它们的使用场景不同:
Equal:需要同时匹配key、value和effect,精确控制。适用于只有一个特定污点要被容忍的场景。Exists:只匹配key和effect,不关心value。适用于污点的value会变化,或者你根本不关心value的场景。
一个常见的组合是系统组件DaemonSet,比如日志收集器、监控Agent。它们要跑遍集群所有节点,包括那些打了各种污点的节点。这个时候用operator: Exists加不指定effect,就可以容忍所有污点:
tolerations: - operator: "Exists"我见过不少同学在这个地方纠结半天,总觉得“不写全是不是不严谨”?实际上,对于需要全节点覆盖的系统组件,这种写法恰恰是官方推荐的方式。容忍本来就是“放行”的意思,表达“我不在乎这个Node上有什么限制”是最直接的理解。
但如果你的Pod要部署到生产环境,且对安全性有要求,我建议还是尽量把容忍范围收紧,只容忍确切的key和effect。因为容忍了所有污点意味着,将来如果有人给某台有故障隐患的节点打了NoExecute污点想驱逐Pod,你的Pod也会无条件留下或者被驱逐,失去了一层保护。
7. 调度排障:Pod一直Pending时,怎么定位是哪一环节卡住了
7.1 排障第一步:看事件
Pod一直Pending不调度,是实际运维中最常见的问题。定位的第一步永远是看Pod的事件:
kubectl describe pod <pod-name>如果调度器拒绝了调度,事件区域会给出非常明确的提示。常见的错误消息无非这几种:
0/5 nodes are available: 5 node(s) didn't match Pod's node affinity——节点亲和条件不满足0/5 nodes are available: 1 node(s) had taint {db: dedicated}, that the pod didn't tolerate——节点上有污点,Pod没容忍0/5 nodes are available: 2 node(s) didn't match pod anti-affinity rules——Pod反亲和不满足
这些消息基本上把方向指得很清楚了。你只需要根据提示去检查对应的配置。
有一个技巧:如果你不确定到底是哪台节点上有什么样的污点,可以一次性看所有节点的污点标签:
kubectl get nodes -o custom-columns='NAME:.metadata.name,TAINTS:.spec.taints[*].key,TAINTS_VALUE:.spec.taints[*].value,TAINTS_EFFECT:.spec.taints[*].effect'这个命令会以表格形式把所有节点的污点列出来,比一个个describe方便得多。
7.2 排障第二步:核对标签和拓扑域
如果事件提示亲和条件不满足,你得核对几个信息:
查看节点标签:
kubectl get nodes --show-labels检查某个特定节点上的标签:
kubectl describe node <node-name> | grep Labels然后跟Pod里的nodeSelectorTerms逐条对照。最容易踩的坑是:标签名称里带了空格、大小写不一致、或者namespace没对上导致Pod亲和匹配不到目标Pod。
还有一点,Pod亲和匹配的是“目标Pod所在的节点”,而不是“目标Pod本身”。如果你的目标Pod因为某种原因还没调度成功,那等它的Pod亲和根节点也会一直Pending——一个等一个,形成死锁。排查时要看看被依赖的Pod是不是已经正常运行了。
7.3 排查调度时一个容易忽略的问题:节点资源不足
有些时候,事件里没有任何亲和或污点相关的错误,Pod还是Pending,这时候百分之九十九是资源不足。调度器给出的错误一般是:
Insufficient cpuInsufficient memoryInsufficient ephemeral-storage
这个问题经常跟亲和规则一起出现。比如你配置了节点亲和到某个特定标签的节点,而这些节点的资源已经被占满了,那Pod就会一直Pending。不要以为是亲和配置写错了,先看看目标节点的资源余量。
kubectl describe node <node-name> | grep -A5 "Allocated resources"这里能看到已分配资源量和实际可分配量。如果发现节点上CPU或内存已经满了,要么扩容,要么调整亲和规则让Pod能去别的节点。
7.4 一些我自己记住的实用排查路径
我平时排查调度问题时,习惯按这个顺序走:
- 先看Pod事件,区分是调度失败还是镜像拉取失败。
- 如果是调度失败,看错误消息是资源、污点还是亲和。
- 如果是污点,列出所有节点的污点情况,对照Pod的容忍配置。
- 如果是亲和,核对节点标签、Pod标签、topologyKey、namespace。
- 如果一切配置都正确但仍然一直Pending,把Pod的YAML拉出来重新读一遍,经常能发现是
nodeSelectorTerms的OR/AND逻辑理解错了。 - 最后看一眼节点的
Allocated resources,排除资源问题。
这套流程基本能解决九成以上的调度排障问题。
8. 关于调度的几个理念和我的体会
做Kubernetes调度配置做得多了,我有一个很深的感受:调度规则的设计本质上是把“部署意图”从“人的大脑”里翻译成“机器能理解的配置”。而这个过程最大的困难,不是学会某个字段怎么填,而是想清楚你到底要什么。
比如“让API服务和Redis待在一起”这句话,听起来很明确,但细想问题很多:是所有API副本都要跟Redis在一起吗?如果Redis有多副本,是任意一个副本同机就行,还是必须跟某个特定副本同机?如果Redis所在的机器满了,API是愿意等还是愿意跑远一点?这些问题没有想清楚,配置写出来一定会有隐藏问题。
我还有一个经验:能用软规则的时候,尽量别用硬规则。硬规则写起来很爽,但一旦集群环境发生变化,比如某类节点下线、某些机器标签变更,硬规则会让Pod找不到去处,直接Pending。而软规则虽然“有时候不按你想的来”,但至少不会把调度彻底卡死。在高可用优先的设计里,我倾向于用软亲和、软反亲和,把硬规则留给那些真正需要强保证的场景。
最后说一个小技巧。调试亲和配置时不一定要动用真实的业务Pod,可以用一个一次性测试Pod快速验证调度规则:
kubectl run test-pod --image=busybox --restart=Never --command -- sleep 3600然后kubectl edit pod test-pod临时加上亲和配置保存,观察它是否被调度到你预期的节点,确认后再把配置复制到正式的Deployment里。这个方法试错成本极低,比反复修改Deployment再重发要高效得多。
Kubernetes的调度体系内容很深,这篇文章覆盖的是最核心的三个维度:节点亲和/反亲和、Pod亲和/反亲和、污点与容忍。把这三块吃透,再去看调度器的高级特性——优先级、抢占、拓扑分布约束——就会顺畅很多。因为它们本质上遵循的是同一套思想:明确意图,用标签描述世界,用规则约束行为,然后让调度器去执行。