1. 调度这件事,为什么值得花时间搞明白
资源调度这块,说实话是Kubernetes里最值得花时间啃的部分之一。很多团队用Kubernetes,一开始就是kubectl apply一把梭,Pod跑起来了就觉得万事大吉,结果过段时间开始出幺蛾子:明明集群还有一堆空闲节点,新起来的Pod却全部挤在两三台上;某个业务流量一上来,宿主机CPU被打满,邻居业务跟着一起遭殃;节点维护要重启,结果关键服务被调度到了正在下线的机器上。这些问题的根子,基本都是没搞懂调度器是怎么做决策的,也没在设计之初就把调度规则想清楚。
Kubernetes的调度器(kube-scheduler)本质上干的事情就一件:为每一个待调度的Pod挑选一个最合适的节点。听起来简单,但仔细想想就知道,这个“最合适”背后牵扯到资源水位、亲和性约束、污点容忍、拓扑分布、优先级抢占等一大堆机制。你配置对了,集群的利用率和稳定性都能上一个台阶;配置错了,轻则资源浪费,重则线上事故。
这篇文章我打算直接从实战角度出发,把资源调度这块的主要知识点和操作经验串一遍。内容会覆盖调度器的工作机制、资源请求与限制的正确姿势、节点亲和性与反亲和性、污点与容忍、Pod拓扑分布约束,以及优先级抢占这些核心话题。每个部分都会有我做项目时实际踩过的坑和验证过的操作方式,不是那种抄文档式的罗列。
适合谁看呢?一种是已经用了一段时间Kubernetes,想深入理解调度原理来优化集群的开发者或运维工程师;另一种是正在设计多环境部署方案,希望搞清楚怎么让Pod落位更合理的人。看完之后,你至少能回答这几个问题:我的Pod为什么被调度到了那台机器?怎么让某些应用固定跑到特定的节点池?怎么避免资源竞争互相拖垮?以及高优先级任务来了,怎么挤掉低优先级任务。
2. 调度器内部到底怎么干活
2.1 从Pod创建到调度的完整链路
先梳理一下一条Pod从创建到运行的完整链路,明白调度器在整个链路中的位置和作用。
当你执行kubectl create或者通过Deployment创建Pod时,请求会先打到API Server。API Server把Pod对象写入etcd,然后这个Pod会处于Pending状态。注意,这时候Pod还没有被分配到任何节点上,spec.nodeName字段是空的。kube-scheduler通过Watch机制监听API Server,发现有新的、未被调度的Pod(nodeName为空)出现,就会进入调度流程。
调度流程分两步走:过滤(Filtering)和打分(Scoring),这也是业内常说的Predicates和Priorities策略(新版本已经改成调度框架的Extension Points,但核心思想一脉相承)。
过滤阶段,调度器会把所有节点过一遍,剔除那些不满足硬性条件的节点。比如节点资源不够(CPU/内存请求超出可分配量)、节点上有污点而Pod不满足容忍条件、Pod有节点亲和性要求但节点标签不匹配、Pod声明的端口与节点上已有Pod冲突等。这些条件是硬约束,不满足就直接淘汰。
打分阶段则是在通过过滤的节点里排序,给每个节点算出一个分值,然后选分最高的那个节点。打分考虑的因素很多,比如节点剩余资源的多寡(资源越富裕分越高)、Pod之间亲和性反亲和性的满足程度、节点上已存在Pod的分布情况(尽量分散或尽量聚合)等。
这两步做完之后,kube-scheduler会把选定节点的名字写回Pod对象(通过API Server更新spec.nodeName字段),然后节点上的kubelet监听到这个更新,就会拉取镜像、创建容器,把Pod真正跑起来。
我在实际项目里遇到过一种情况:调度器不做过滤,只做打分,Pod被调到了一个资源基本耗尽的节点上。排查下来发现,是节点资源回收不及时加上有DaemonSet类的Pod占了大量资源,导致调度器拿到的节点状态是旧的。这提醒我一个很重要的点:调度决策基于的是调度器本地缓存的节点状态快照,不是实时向每个节点查询。如果节点状态同步有延迟,或者调度器缓存和实际不一致,就会出现“调度成功但节点起不来”的问题。
2.2 调度框架和扩展点的作用
Kubernetes从1.15版本之后逐渐引入了调度框架(Scheduling Framework),把原来写死的过滤、打分流程改造成了一系列可插拔的扩展点。这个改造的意义在于,你要自定义调度逻辑,不需要再去fork kube-scheduler改源码了,而是可以编写插件挂载到对应的扩展点上。
调度框架里有几个重要的扩展点:QueueSort(排序待调度Pod的队列,决定了先调度哪个Pod)、PreFilter和Filter(对应过滤阶段,可以增加自定义的硬性过滤条件)、PreScore和Score(对应打分阶段,可以增加自定义的评分策略)、Reserve(预占资源,在最终确定节点前锁定资源)、Permit(允许或延迟调度决策,常用于多调度器协同场景)、PreBind和Bind(绑定阶段,写入nodeName之前和之后的钩子)。
我自己没写过太复杂的调度插件,但用过一个场景:在Filter阶段检查节点的磁盘类型是不是SSD,不是就过滤掉。当时是给一个数据库中间件集群做调度,数据目录必须落在SSD盘上,而集群里混有机械盘节点。虽然也可以用节点标签加节点亲和性来做,但用调度插件可以做得更细,比如结合PV的存储类和你自己的内部元数据做联合判断。
对绝大多数使用者来说,自定义调度插件不是日常操作,但理解这个框架能帮你明白两件事:第一,你在YAML里写的nodeSelector、affinity、tolerations实际是在哪个环节起作用的;第二,如果集群里装了第三方调度器组件(比如用来做GPU共享调度的),它们和你写的调度策略之间是什么关系,出了问题可以往哪个方向排查。
3. 资源请求与限制,调度决策的地基
3.1 requests和limits的关系必须搞清
调度器做资源判断时,看的不是节点的总容量,而是节点上所有Pod的requests之和与节点可分配量之间的剩余关系。这个点特别关键,因为很多人把requests和limits混为一谈。
先明确定义:requests是调度器用来做“能不能放得下”的判断基准,也是CPU分享和内存保障的水位线;limits是运行时对容器资源的硬限制,超了会被限流或杀掉。在调度阶段,Pod要的requests如果超过节点剩余的可分配资源,这个节点就会被过滤掉。而limits即使设得很高,调度时也不会因为所有Pod的limits之和超了节点容量而拒绝调度,因为limits超标是允许的(过载状态),requests超标则不允许。
举个例子,一个节点有4核CPU和8Gi内存,可分配量假设是3.8核和7.5Gi(扣掉系统预留和kubelet预留)。上面已经跑了两个应用:A请求了1核2Gi,B请求了2核3Gi。这时候节点剩余可分配量是0.8核和2.5Gi。如果你想新起一个requests为1核2Gi的Pod,调度器会认为这个节点放不下(CPU不够),直接过滤掉。但如果你把limits设置得很大,比如limits为8核16Gi,只要requests仍为1核2Gi,调度器照样会接受它,因为调度器只看requests。
这里就引出了生产中常被吐槽的一个问题:很多团队只写limits不写requests,或者干脆两个都不写。不写requests,调度器默认它是0,那调度阶段等于什么也不判断,所有节点都放得下,结果就是Pod全被塞到一台机器上,真正运行时要多少给多少,CPU争抢、内存OOM全来了。我在优化过的集群里见过最夸张的情况:20多个Pod挤在3个节点上,另外十几个节点空闲,原因就是所有Pod都没写requests,调度器失去了资源依据,打分阶段只能靠节点标签、Pod数量这些维度瞎猜。
3.2 CPU和内存怎么设才合理
requests和limits的设定,不能拍脑袋,要结合实际负载和历史监控数据来调。
CPU这块,特点是可压缩资源,线程被限流时会排队等待,不会杀掉容器。所以CPU的requests可以按实际平均使用量来设,limits可以设到平均值的2到3倍作为突发峰值缓冲。但注意,如果你设了CPU limits,容器在超过这个限制时会被throttle,表现就是响应变慢、延迟升高。我在排查一次接口超时问题时,发现服务的CPU使用率远没有打满节点,但客户端就是偶发超时,最后用kubectl top配合容器指标一看,是CPU throttle次数非常高,就是这个服务自己设了很紧的CPU limits导致的。
内存是另一种性质:不可压缩资源,超过limits会直接OOMKilled。所以内存的requests建议设得比实际均值稍微高一点,给GC和其他临时分配留空间,limits设成你能接受的上限——一般来说,达到这个值被OOM杀掉是可以接受的。我在生产环境踩过坑,把内存requests设得和limits一样,结果Java服务一到高峰期就触顶被杀,后来把limits调整到requests的1.3倍左右,同时让JVM堆参数适配容器内存限制,才稳定下来。
一个小建议:内存的requests至少要保证Pod运行所需的最低内存,不然节点上内存确实够,但容器启动就OOM,调度得再准也没意义。
3.3 用ResourceQuota和LimitRange做兜底
只靠开发自觉写requests和limits,在团队大了之后基本不可靠。我见过不止一次:应用YAML写得花里胡哨,亲和性、反亲和性、拓扑分布约束全都有,但requests和limits就是没写。所以要在命名空间层面做兜底。
LimitRange可以给命名空间里未显式设置requests/limits的Pod自动注入默认值。比如你可以给某个命名空间设置默认的CPU requests为100m、内存requests为256Mi,这样凡是没写的Pod都会被自动填上,调度器就有了判断依据。
ResourceQuota则是对整个命名空间的总资源用量做上限,比如CPU请求总量不能超过20核、内存总量不能超过40Gi,或者PVC总数不能超过10个。这个能防止某个团队把共享集群的资源全部抢走。
两种机制配合使用效果最好:LimitRange管单个Pod的默认值,ResourceQuota管命名空间的总盘子。我在给一个多团队共享集群做配置时,就是按照团队维度划命名空间,每个命名空间设了ResourceQuota,同时用LimitRange兜底默认requests。折腾完再去查看调度效果,资源分布明显均匀了,节点空闲率也降下来了。
4. 节点选择与亲和性,让Pod去它该去的地方
4.1 nodeSelector和节点亲和性的适用边界
节点选择最简单的方式是nodeSelector,要求Pod必须调度到带指定标签的节点上。这个方式在节点分组明确的场景下很好用,比如集群里有GPU节点池和普通CPU节点池,GPU任务加一个nodeSelector: gpu: "true"就行了,配置量小,排错也方便。
但nodeSelector有个明显的短板:它只能表达硬性约束,表达不了优先级。比如你希望Pod优先往某个节点池跑,但那个池满了也可以接受其他池,nodeSelector就做不到。这种情况下要上节点亲和性nodeAffinity。
节点亲和性支持两种语义:requiredDuringSchedulingIgnoredDuringExecution对应硬性要求,效果类似nodeSelector但语法更灵活;preferredDuringSchedulingIgnoredDuringExecution对应软性偏好,调度器会尽量满足,但实在满足不了也会调度到其他节点。生产环境里,软性亲和用得很多,比如让某些服务尽量调度到和它依赖的中间件同机的节点上,以减少网络开销,但不强制。
这里有个容易踩的坑:required硬性亲和配置完了之后,如果节点标签拼错了或者节点池缩容把标签对应的节点都删了,Pod会一直Pending。调度器不会帮你判断标签是否存在,它只看条件是否满足,不满足就过滤掉。所以在改标签或者缩容节点池之前,最好先确认一下现在集群里有哪些Pod依赖这些标签。
4.2 Pod间的亲和性与反亲和性
节点亲和性解决的是Pod和节点之间的分布关系,Pod亲和性和反亲和性解决的则是Pod和Pod之间的分布关系。这个在实际生产中用处很大。
一个典型场景:两个服务之间通信非常频繁,比如业务网关和认证服务,希望把它们调度到同一个节点甚至同一个机架,以降低网络延迟。用podAffinity可以做到,通过topologyKey指定从哪个维度看“同一位置”(比如kubernetes.io/hostname表示同一节点)。另一个典型场景:两个服务相互争抢资源(比如一个高CPU的应用和一个高内存的应用),不希望它们跑到同一台机器上,用podAntiAffinity配合kubernetes.io/hostname,可以让它们分散到不同节点。
反亲和性在部署有状态服务时的价值更突出。比如你部署一个三副本的Redis集群,如果三个副本都落在同一台机器上,那这台机器宕掉整个集群就挂了。给Pod加上反亲和性,要求同一组Pod尽量散落到不同节点,就能在物理层面提升容灾能力。
不过反亲和性的代价也不小。首选方案是requiredDuringScheduling,加上之后调度器要花费更多计算来判断Pod分布,同时在节点数量有限的集群里,可能因为找不到足够的、不冲突的节点而导致Pod长时间Pending。我实际经历过一次:集群一共6个节点,服务副本数设了5,反亲和性要求每节点最多一个Pod,结果有一个节点因为内存水位不足被过滤掉,剩下4个节点放不下5个副本,调度直接卡住。后来在配置里加了一个topologyKey维度更宽的兜底策略(比如从hostname改为zone级别),才解决问题。
4.3 topologySpreadConstraints,让Pod分布更均匀
亲和性和反亲和性处理的是“要和谁在一起”和“不要和谁在一起”,而拓扑分布约束topologySpreadConstraints解决的是“尽量均匀地铺开”的问题。它允许你定义Pod在一组拓扑中的分布偏好,比如让同一Deployment的副本尽量平均分布在每个节点上或每个可用区里。
这个特性在跨可用区高可用场景下非常关键。比如你在云上创建了一个三可用区的集群,希望业务Pod三个副本分别落在三个可用区,任何一个可用区故障都不会影响全部副本。拓扑分布约束可以直接做到这一点,比手动用反亲和性加zone标签去写要优雅得多。
配置拓扑分布约束时,核心参数有maxSkew(允许的最大分布倾斜度,即最忙和最闲的拓扑域之间允许差几个Pod)、topologyKey(按什么维度划分拓扑域,通常用topology.kubernetes.io/zone或者kubernetes.io/hostname)、whenUnsatisfiable(不满足约束时是DoNotSchedule硬性要求还是ScheduleAnyway软性偏好)。
我在一个跨可用区集群里配置过一条约束:同一个服务的3个副本,按可用区维度分散,maxSkew设为1,whenUnsatisfiable设置为DoNotSchedule。结果发现当某个可用区资源紧张时,Pod就一直Pending,调度器虽然在打分阶段尽量平衡,但硬约束让它不敢把两个Pod放到同一个区。后来把whenUnsatisfiable改成了ScheduleAnyway,同时把maxSkew放宽到2,Pod就不再卡住了,分布也还算均衡。这里面的取舍,其实就是高可用要求和资源利用率之间的权衡,没有标准答案,得看你的场景优先级。
5. 污点与容忍,节点和Pod之间的“通行协议”
5.1 污点容忍的基本逻辑
污点(Taint)和容忍(Toleration)可以用来阻止Pod被调度到某些节点上。和nodeSelector/亲和性不同,污点是节点主动“嫌弃”Pod的一种机制:节点打了污点,没有对应容忍的Pod就不允许调度上来。注意,这个“不允许”是在过滤阶段生效的,与打分无关。
污点有三个组成部分:key、value和effect。effect有三种取值:NoSchedule表示不调度新的Pod上来(已运行的Pod不受影响);PreferNoSchedule表示尽量不调度上来,是软性的;NoExecute表示不仅不调度新Pod,已经运行在上面的Pod如果不容忍会被驱逐。
生产环境里最常见的几种污点用法:
- 节点专门用于某种工作负载,比如GPU机器打上
gpu=true:NoSchedule,只有声明容忍gpu=true的Pod才能上去; - 节点在做维护,准备下线或重启,打上
maintenance=true:NoExecute,让上面的Pod被驱逐到其他节点; - 系统组件(比如监控、日志采集)调度到所有节点,各个节点上的DaemonSet Pod通过对应的容忍来绕过这个限制。
我自己在集群维护过程中,给准备重启的节点打NoExecute污点是常做的一件事。操作顺序是:先打污点,等待业务Pod被逐出,然后cordon节点,再执行节点重启。这样能确保业务Pod全部在节点重启前迁移走,不会出现节点重启期间Pod还挂着导致服务不可用的情况。
5.2 容忍度别乱开,不然污点形同虚设
容忍的配置有个很容易被忽略的点:operator为Exists且不带key时,表示容忍所有污点。很多人在排障的时候图省事,给业务Pod加上tolerations: - operator: Exists,结果就是Pod可以往任何节点上跑,之前辛辛苦苦打的污点全部失效。
我在一个共享集群里遇到过这问题。基础设施团队给一批旧机器打了NoSchedule的污点,打算慢慢腾空后下线。但某个业务的Pod还是时不时被调度到这些机器上,排查下来发现是这个业务团队的Deployment模板里有一条无限制的容忍,直接绕过了基础设施团队设计的隔离逻辑。后来把容忍改成了只针对特定key且effect为NoSchedule的精确容忍,这个问题才算解决。
所以容忍的配置原则是:越精确越好。知道自己的Pod必须容忍哪个污点,就只写那个key、value和effect,不要写通配。
5.3 节点维护的标准操作流程
结合污点和cordon,我这里写一下维护单台节点的标准流程,这是我多轮操作下来总结出的比较稳的一套顺序:
- 先给节点打NoExecute污点,让不容忍的Pod自动被驱逐到其他节点。这里要注意,不是所有Pod都能顺利被驱逐,比如使用了emptyDir且需要数据持久化的Pod,被逐出后数据就没了,所以操作前最好过一遍上面跑的业务,确认可接受。
- 等Pod迁移完成,用
kubectl cordon <node>把节点标记为不可调度,防止新Pod调度上来。 - 节点排空操作(
kubectl drain),把剩余DaemonSet之外的Pod优雅地驱逐掉。 - 重启或维修节点之后,
kubectl uncordon <node>解除限制,此时新Pod才能重新调度上来。
这套流程颠来倒去我做过很多次,核心思路就一句话:让Pod自己走,别硬赶。硬删Pod的话,控制器会立刻重新创建一个新Pod,可能还是调度回这台机器,白忙活。
6. 优先级与抢占,重要任务如何插队
6.1 PriorityClass怎么用
默认情况下,所有Pod的调度优先级是一样的,调度队列的排序方式也比较简单。但如果集群里同时运行着在线业务和离线任务(比如数据处理、批量训练),你可能希望在线业务始终比离线任务先调度、且在资源紧张时能挤掉离线任务。
这时候要用到PriorityClass。它可以定义不同的优先级级别,Pod通过priorityClassName引用某个PriorityClass,调度器在排序待调度Pod时会优先处理高优先级的Pod。
PriorityClass的定义很简单,核心是value字段,数字越大优先级越高。比如我维护的一个集群里定义了三档优先级:high-priority(value=1000000)给核心支付链路和用户端实时接口;medium-priority(value=100000)给一般业务服务;low-priority(value=10000)给离线数据处理任务。这个value选取没有硬性要求,只要拉开差距、便于理解就行。但有一点要注意,Kubernetes的优先级值范围是-2147483648到1000000000,超过10亿的数值会预留出来给系统组件用,比如kube-system里的一些关键Pod。你在自定义的时候不要去占用系统保留的范围,不然可能和集群内置组件产生意想不到的冲突。
6.2 抢占调度有什么代价
高优先级Pod来了,如果集群资源不够,调度器会尝试抢占(Preemption):把低优先级的Pod驱逐掉,腾出资源给高优先级Pod。这个机制能保证重要任务不被资源不足卡住,但代价也很明显——被抢占的Pod会被强制终止,如果它没有做优雅退出处理,可能产生数据不一致。
我经历过一次印象很深的抢占事故。一个定时数据同步任务Pods被高优先级业务Pod抢占杀掉,这个同步任务没有做断点续传,重启后需要从头再来,导致本来半小时能跑完的任务拖了好几个小时。从那之后,我对核心的、有状态的数据任务做了一个约定:要么它们自己也声明高优先级(以防止被低优先级任务抢占),要么给它们加一个PDB(PodDisruptionBudget),限制被驱逐后最多允许同时中断几个Pod。PDB配合主动驱逐操作能生效,但对抢占调度也有类似的保护作用,我实测下来它确实能在一定程度上限制抢占导致的击穿范围。
6.3 警惕优先级饥饿问题
抢占机制还会引入一个经典问题:低优先级任务可能长时间得不到调度。因为高优先级任务不断到来,调度器一直优先处理它们,低优先级任务只能排在队列后面吃灰。在离线计算场景,比如深度学习训练任务,如果它们的优先级设得很低且资源池长期被在线业务占据,就会出现“永远Pending”的状态。
解决饥饿问题的常见思路是:给不同优先级的Pod设置对应的资源配额(ResourceQuota),每个优先级级别使用独立的命名空间或配额,让低优先级任务在自己的配额里也能获得一定的调度机会;或者在业务层面控制高优先级任务的并发度,避免预期外的优先级倒挂。
这个机制用得好的话,能明显提升集群的吞吐能力和稳定性。但它是把双刃剑,建议在没有十足把握之前,先在测试集群里模拟一下高优先级任务到来时的行为,再部署到生产。
7. 常见调度问题与排查思路速查
最后整理一份我在排障过程中反复用到的排查清单和思路。先看Pod状态,如果长时间Pending,最优先怀疑的就是调度阶段出了问题。
Pod一直Pending,怎么排查
第一步,用kubectl describe pod <pod-name>查看Events,调度器会把过滤掉的原因写在这里,比如0/6 nodes are available,后面会跟具体的0个节点不可用的原因列表。常见的有:Insufficient cpu、Insufficient memory、node(s) had taint、node(s) didn't match node selector、node(s) didn't match pod affinity/anti-affinity。看到这些信息,基本就知道是资源不够、污点不匹配,还是亲和性约束不满足。
第二步,如果Events里没有明确提示,查看集群里有没有调度器异常。kubectl get pods -n kube-system | grep scheduler,确认kube-scheduler在正常运行。多控制平面节点的情况下,还要注意是否有leader选举问题,导致多个scheduler同时接收任务或者都没有接管。
第三步,确认Pod是否设置了nodeName字段。有时候是人工强制指定了nodeName,调度器会直接跳过这台Pod,而如果这个节点不存在或者kubelet状态异常,Pod会一直卡在Pending。这种问题描述里看起来像是调度问题,实际是绑定了错误节点导致的。
节点资源明明够,但Pod还是调度不上去
这种情况我见过好几次,典型原因是节点上某一类资源被耗尽。比如节点的可分配内存还剩1Gi,但Pod要求的requests是1.5Gi,那自然放不下。还有一种情况是节点层面支持扩展资源(比如GPU显存、FPGA资源),这种资源不是通过标准CPU内存来衡量的,如果你声明了nvidia.com/gpu这类扩展资源,调度器会校验节点的资源余量,不够也会过滤掉。
另外,节点存储资源也可能成为限制因素。临时存储(ephemeral-storage)如果被大量占用,Pod调度时也会被过滤掉。所以节点上不仅有CPU和内存,还有存储这个维度,平时监控不要只看前两个。
Pod被调度到了一个不合适的节点
如果Pod已经运行了,但你觉得它跑错了地方,需要先确认调度决策是基于什么信息做出的。kubectl get pod -o yaml里可以看到调度器写入的nodeName、Pod的调度相关配置。再对比一下这个节点的标签和资源情况,基本就能还原调度逻辑。
有时候问题出在节点标签过期。比如节点池缩容后,新的节点没有打上老节点上的自定义标签,导致nodeSelector匹配不上,Pod全堆在了其他几个打了标签的节点上。这种问题用kubectl get nodes --show-labels看一下就能发现,但难的是发现时机——我建议在节点池变更之后做一次标签核对,不要等线上出问题再去查。
如果把上面这些点都过完了还是没头绪,可以打开kube-scheduler的日志,搜索这个Pod的名字,会看到调度器的完整决策过程,包括每一步过滤的节点和原因。虽然日志量大,但定位调度问题的时候确实是最直接的证据。
8. 我在资源调度上的一些体会
做了几年Kubernetes,给我最大的感受是:调度配置不是一次性的事情,而是一个持续调优的过程。集群规模小的时候,随便写写requests和limits也能跑得不错;但一旦进入几十个节点、上百个应用、多团队共用的阶段,资源调度的规划和治理就必须提上日程。
我的建议是,先打好基础:保证所有Pod都有合理的requests和limits,这是调度器发挥作用的前提。然后逐步引入节点亲和性和污点容忍来划分集群的资源域,比如在线业务域、离线任务域、GPU任务域。再进一步,对关键的有状态服务配置Pod反亲和性或拓扑分布约束,提升容灾能力。等这些都稳定了,再考虑优先级、抢占和自定义调度,去解决更精细的资源竞争问题。
不要一上来就把所有调度特性都用上。调度约束加得越多,调度器做决策的复杂度越高,出问题的概率也越大。我见过一个团队的Pod YAML里同时写了节点亲和性、Pod反亲和性、拓扑分布约束和一堆容忍,结果是调度器花了大量时间做计算,Pod分布非常分散,资源碎片化严重,反而比简单用nodeSelector的时候更难管理。
最后想重点强调一个安全相关的点:资源调度涉及的生产系统变更,操作前一定要在测试环境验证。特别是涉及节点驱逐、Pod抢占、污点修改这类直接影响运行实例的操作,宁可多花半小时确认,也不要抱着“应该没事”的心态直接上生产。我自己就犯过因为没确认容忍匹配范围就把节点打污点,导致一批Pod全部被驱逐的错。那次的教训是:任何对调度器的改动,先对照节点标签和Pod配置,把影响范围写在纸上,再执行操作。
这段内容是基于我个人的实践经验整理的,不同版本的Kubernetes在细节实现上可能有些差异,但大的思路和排查方向基本一致。如果你们团队也在做资源调度相关的优化,欢迎在评论区聊聊你们的场景和方案,我看到了会尽量回复。