资讯动态

K8S默认调度器抢占机制全解析:三段式流程与实战排查

发布时间:2026/9/8 5:40:28 来源:尧图企业网站定制
生产环境里最让人头疼的一类K8S故障莫过于一个高优先级任务提交上去结果卡在Pending好几个小时节点资源全被低优先级Pod占满了调度器就是“无动于衷”。上周我们线上就遇到过类似情况排查到最后问题出在很多人对默认调度器里的DefaultPreemption抢占逻辑理解太浅——以为设置了高优先级就一定会抢占实际上这个抢占从触发到生效中间隔着一整个“三段式”流程。这篇文章我就把这块彻底拆开从调度器的视角讲清楚抢占是怎么从无到有发生的、内部分几个阶段、每一步在干什么再给一个能在测试集群里直接复现的完整实验过程最后附上我这些年踩过的坑。顺便回应一个很多人纠结的问题K8S和Docker到底什么关系。简单说Docker只是解决“单机怎么跑容器”K8S的核心价值在“跨节点怎么调度容器”。而调度器里最难、最容易引发事故的逻辑恰恰就是本文要讲的抢占机制。这篇内容适合三类人看正在被Pod抢占不生效折磨的SRE、刚学调度器想搞懂PostFilter扩展点的开发者、以及准备K8S面试想把这个点讲成加分项的同学。1. 抢占机制的逻辑起点调度器为什么需要“动手抢”1.1 默认调度器的过滤逻辑与Pod卡住的常见现象先还原一下默认调度器的工作方式。kube-scheduler拿到一个待调度Pod后先进入内部调度队列然后走“过滤Filter—打分Score—选定节点Reserve—绑定Bind”这条链路。其中过滤环节最核心调度器会把集群里所有节点逐一比对该Pod的亲和性、污点容忍、端口冲突、资源请求等硬性条件任何一个不满足就直接排除。当集群资源充足时过滤器很快会留下一批可用节点调度器从里面打分挑一个最优解。可问题在于如果你的请求值requests在节点上根本凑不齐那么所有节点都会在过滤阶段被刷成失败。这时候Pod会留在调度队列里一直重试表现就是Pods一直Pendingevents里面反复出现类似FailedScheduling的记录原因是Insufficient cpu / memory。资源不足时的出路其实只有三条扩容节点、等待现有Pod释放资源、主动把低优先级Pod“请走”。前两条是被动的只有第三条是调度器主动发起的这就是DefaultPreemption要做的事。严格说DefaultPreemption并不是一个独立的可插拔组件而是调度框架里PostFilter扩展点上的默认实现。当Filter阶段全军覆没时调度器会进入PostFilter阶段试着通过驱逐一些低优先级Pod来腾出空间让当前这个高优先级Pod能塞进去。1.2 优先级体系抢占的资格从哪来想触发抢占Pod光“看起来重要”没用它得有真实的优先级数值。K8S里的优先级靠PriorityClass定义Pod再通过priorityClassName引用。一个最小化的PriorityClass长这样apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: 高优先级任务使用这里面的value就是抢占的依据数值越大优先级越高。调度队列里默认运行着一个叫PrioritySort的queueSort插件它保证队列中Pod按照优先级从高到低排序高优先级Pod永远排在前面。所以高优Pod并不需要“越过”低优Pod它天然就在队列前面真正的障碍是低优Pod把节点资源占了高优Pod到了节点面前却进不去。优先级只是资格抢占还要有“策略”允许。PriorityClass里可以再加一行preemptionPolicy: Never把“抢占”这个行为关掉。设置成Never后高优先级Pod不会驱逐任何人只会安静地等在队列里直到资源释放。这个字段是很多人在排查时忽略的点如果高优Pod的PriorityClass显式禁用了抢占那后面所有调度器行为都不会发生。还有一点需要注意优先级比较是逐Pod比较的。抢占者只会去驱逐优先级比自己低的Pod遇到和自己同优先级或者更高优先级的Pod调度器不会碰。如果某个节点上全是同优先级Pod那这个节点在高优Pod眼里依然不可用即使节点负载很低也不行。所以设计PriorityClass时数值区分度要留够别把生产和测试的任务混成同一档。2. DefaultPreemption 内部实现拆解三段式“抢”的完整链路2.1 第一段在过滤失败的节点里筛选候选节点当高优Pod在所有节点上Filter失败后PostFilter阶段的DefaultPreemption逻辑开始执行。它的第一步不是直接挑一个“资源最紧张”的节点去驱逐而是要把之前Filter失败的节点全拿出来重新“模拟调度”一遍。为什么要重新模拟因为Filter失败的原因可能各不相同。资源不足是最常见的但也可能是Pod自带的反亲和性导致节点不可用或者nodeSelector根本不匹配任何节点。如果是后两种原因你把节点上低优Pod全杀光也没用高优Pod还是进不去。所以DefaultPreemption会先给每个失败节点做一次“预判”看这个节点在排除了低优Pod的干扰之后到底还有没有机会容纳当前Pod。具体做法是拿当前NodeInfo节点元数据、已分配资源、已调度Pod列表在内存里做一次“打扫”把这些候选Pod从资源占用列表里摘掉再用高优Pod的Filter插件跑一遍模拟调度。只有模拟通过、证明“抢完就能进来”的节点才有资格进入下一轮。这也是为什么有些场景下你的高优先级Pod明明有亲和性匹配的节点却迟迟不生效——因为它可能被一个反亲和规则挡在门外而调度器觉得“这个节点抢了也白抢”。2.2 第二段选出victim不是随便挑有了候选节点之后第二步是确定每个节点上具体要驱逐哪些Pod。这一段的专业名词叫选取受害者victims。DefaultPreemption在选victim的时候有一条非常明确的原则先挑优先级最低的并且在满足高优Pod需求的前提下数量越少越好。它不会把节点上所有低优Pod一次全杀掉而是按优先级从低到高排序挨个“试删”每删掉一个就用剩余资源重新跑一次模拟调度直到高优Pod的请求资源能被满足为止。这一步决定了被牺牲的是谁、保留的是谁是整条抢占链路里最容易出意外的地方。因为如果你只杀掉一个Pod资源够了那就只杀一个如果需要三个那三个都得牺牲。这里还有两个隐藏得很深的过滤条件。第一个是PodDisruptionBudgetPDB。如果节点上有Pod被打上了PDB标签并且允许中断的数量已经降到最低限制那这些Pod会被调度器从候选victim列表里剔除。也就是说即使它们是低优先级只要PDB保护着抢占也不允许把保护底线击穿。第二个过滤条件是状态过滤像已经处于Terminating、已经不在节点上实际运行的Pod不会进候选列表。PDB这个点特别值得展开一下。假设一个数据库实例有三个副本你给这组Pod配了minAvailable: 2的PDB现在三个副本都在正常运行。如果你再提交一个高优Pod这个节点上能被打断的副本数只有1个调度器优先选择这个可中断的副本作为victim另外两个即使优先级更低也不会同时被驱逐。反过来如果PDB设置成minAvailable: 3那这组Pod完全不可抢占它们会直接从小黑屋里放出来。2.3 第三段提名不是立即执行而是“软驱逐”流程选定victim并不是终点真正让抢占生效至少还要走完“提名调度删除Pod”这三步。为了不让一个高优Pod在自己等待期间又被其他中优Pod插队K8S引入了一个机制叫提名节点NominatedNodeName。调度器会把高优Pod的status.nominatedNodeName字段设置为目标节点名相当于告诉集群“我预定这个位置了你们不要来抢。”这个字段不是摆设后续调度器面对其他Pod做资源分配时会把提名中的Pod所需的资源也计算进节点已分配量里同等条件下尽量避让。这样一来被抢占的低优Pod被删除后空出来的资源首先保证的是这个提名Pod能落上去。但提名归提名真正清理资源还得靠调用Kubernetes API把victim Pod删掉。kube-scheduler会收到低优Pod的删除信号Pod进入Terminating状态接下来就是kubelet执行优雅终止graceful termination流程。这里有个常见的认知误区很多人以为“驱逐资源立刻释放”实际上Pod进入Terminating状态后容器可能还在运行、还在占用内存和CPU取决于优雅终止时间和容器是否响应SIGTERM。如果应用的优雅停止要30秒甚至更久那么从高优Pod被提名到它真正绑定节点中间可能有一个不小的空窗期。这也解释了为什么有时候你看到低优Pod已经被杀了高优Pod却依然Pending。总结成一句话DefaultPreemption的三段式链路是——先在失败节点里挑出“抢完能成功”的节点再在节点上挑出“最该被牺牲”的Pod最后通过提名和删除完成真正的资源腾挪。理解了这个链路后面所有排查和调优都顺了。3. 动手实践在真实集群里复现一次抢占3.1 搭建最小验证环境理论看十遍不如亲手复现一次。我们用一个临时K8S集群做实验生产环境要求高的话可以换成kubeadm或云厂商托管集群原理完全一样。我习惯用kind快速起一个三节点集群1个控制平面节点加2个worker节点这样有足够空间观察节点间的调度差异。kind create cluster --config - EOF kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: worker EOF集群起来后先创建两个PriorityClass。一个叫low-priorityvalue设成100一个叫high-priorityvalue设成1000000。差距越大越容易观察抢占行为实际生产中数值具体取多少要看你的分级规范但不能全部挤在同一档。apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: low-priority value: 100 globalDefault: false description: 低优先级测试任务 --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: 高优先级测试任务接下来是关键的一步在两个worker节点上分别部署占资源的低优先级Pod。这里强烈建议用裸Pod而不要用Deployment或者其他控制器管理。为什么后面我会专门讲Deployment会把被驱逐的低优Pod不断重建出来导致抢占实验根本没法稳定观察。每个worker上放两个请求1核CPU、1Gi内存的Pod把节点资源吃紧。为了方便控制位置可以直接给裸Pod指定nodeName这样干扰项最少。apiVersion: v1 kind: Pod metadata: name: low-priority-pod-1 spec: priorityClassName: low-priority nodeName: kind-worker containers: - name: hog image: busybox:1.36 command: [sh, -c, while true; do sleep 3600; done] resources: requests: cpu: 1 memory: 1Gi limits: cpu: 1 memory: 1Gi类似的Pod在kind-worker、kind-worker2上各创建两个。等全部Running后用kubectl describe node kind-worker | grep -A 8 Allocated可以确认资源情况正常情况下两个worker节点的CPU和内存余量都会非常紧张高优Pod想找一个空闲节点几乎不可能。3.2 提交高优先级Pod观察触发链路硬件条件备齐后提交一个高优先级Pod请求资源设成2核CPU、2Gi内存。这个大小刚好多于单个worker节点的剩余资源又不超过整个集群的总量非常适合触发抢占。apiVersion: v1 kind: Pod metadata: name: high-priority-pod spec: priorityClassName: high-priority containers: - name: nginx image: nginx:1.25 resources: requests: cpu: 2 memory: 2Gi limits: cpu: 2 memory: 2Gi提交之后用命令行观察状态变化我列一下我实验时的完整链路。# 第一步观察Pending和事件 kubectl get pod high-priority-pod -w kubectl describe pod high-priority-pod | tail -20 # 第二步观察提名节点 kubectl get pod high-priority-pod -o jsonpath{.status.nominatedNodeName} ; echo # 第三步看低优Pod是否被删 kubectl get pods -o wide | grep low-priority正常流程里你会先看到高优Pod进入Pending然后事件里出现PostFilter: preemption相关记录再接下来某个低优Pod变成Terminating并最终被删除最后高优Pod的NominatedNodeName字段从空值变成某个worker节点名随后Pod调度到该节点Running。整个过程如果优雅终止时间默认30秒高优Pod真正Running可能要等30秒以上这非常正常。要看到更细的内部决策可以给kube-scheduler日志加verbosity。在kind环境里直接进入控制面容器抓日志docker exec -it kind-control-plane crictl logs $(docker exec kind-control-plane crictl ps | grep kube-scheduler | awk {print $1}) | grep -i preempt从日志里能看到Preparing for preemption到Start preempting再到Selected node这类关键字对照上面的三段式链路能很清楚看到调度器在每个阶段干了什么。3.3 边界条件一起测PDB、Never策略和控制器重建复现成功之后我强烈建议把几个边界条件也顺手测掉理解会更深面试也不虚。第一个是PDB的影响。给低优Pod加一个PDB保护要求至少保留1个副本可用再重复上面的实验。此时调度器在选victim时会受到限制如果被保护Pod是唯一可牺牲的抢占依然会成功但如果PDBminAvailable已经达到上限你会发现高优Pod无论如何都不触发驱逐哪怕节点完全挤不下它。这是生产环境中常见的“高优Pod迟迟Pending却又查不出错”的元凶之一。第二个是preemptionPolicy: Never。把高优Pod的priorityClassName换成一个设置preemptionPolicy: Never的PriorityClass再重复实验。这次低优Pod不会离开高优Pod只能一直Pending。这个办法适合那些“可以等但不能打断别人”的任务比如批处理任务在低谷期利用弹性资源。第三个是控制器重建问题。把低优裸Pod换成Deployment管理的Pod资源请求不变再跑一次抢占。你会发现被抢占的Pod被杀掉后Deployment立刻把它重新建出来而且新Pod大概率又会被调度回同一个节点于是你看到的现象就是高优Pod要12秒才Running但低优Pod刚被杀又起来节点资源依然紧张甚至可能又触发一轮抢占。这就是为什么生产环境中开启抢占后要考虑给低优工作负载配合适的配额、HPA或弹性资源否则抢占只会变成无休止的“驱逐—重建”循环。4. 常见问题与排查技巧实录4.1 抢占不生效一张速查表定位问题我平时排查“抢占不生效”的情况基本靠一套固定方向。下面是整理出来的速查表按优先级顺序检查基本不会漏。常见现象可能原因排查/解决方式高优Pod一直Pending低优Pod纹丝不动高优Pod未配置PriorityClass或优先级不够高kubectl get pod -o yaml看priorityClassName与priority数值高优Pod配置了PriorityClass但preemptionPolicy是Never检查PriorityClass定义去掉preemptionPolicy: Never节点上都是同优先级或更高优先级Pod重新规划PriorityClass数值档位低优Pod被杀掉后立刻重建资源恢复不了低优Pod由Deployment/StatefulSet等控制器管理对工作负载合理设置副本数、配额或接受循环本身Pod被提名了字段有nominatedNodeName但始终不调度优雅终止时间太长victim还没完全释放资源查看victim Pod的Terminating时长调短优雅终止时间有PDB保护但不知道为何不能抢占PDB的minAvailable已经到阈值kubectl get pdb -n xxx看Allowed Disruptions节点明明有空闲资源却没触发抢占节点亲和/反亲和/污点条件不满足dry-run失败查看scheduler日志的失败节点原因调度器事件里没有Preemption相关记录集群版本较旧或scheduler配置禁用了PostFilter查看kube-scheduler配置与版本这里最值得单独拎出来说的坑是“看事件不要只看高优Pod还要看低优Pod”。因为你提交的高优Pod事件通常只会写FailedScheduling真正包含victim信息的记录在scheduler日志和低优Pod的事件里。低优Pod会有类似Deleted by kube-scheduler due to preemption的说明这是判断抢占是否发生的铁证。相反地如果高优Pod一直Pending且低优Pod没有收到任何删除事件几乎可以断定抢占过程压根没启动或者被某个前置条件卡住了。这时候回到速查表从PriorityClass和preemptionPolicy开始查效率最高。4.2 生产环境里容易踩的坑和应对思路第一件事认清抢占是“兜底手段”而不是“常态策略”。如果你发现集群日常调度频繁依赖抢占说明资源规划和容量预留出了问题需要从提高资源利用率、调整request/limit、增加HPA弹性等方向解决而不是靠调大PriorityClass数值硬撑。抢占是有代价的victim Pod被杀后控制器会重建重建期间服务可能抖动数据库类或有状态服务甚至可能出现更严重的问题。第二件事给关键workload加PDB而且要真正确认PDB在工作。很多团队写了PDB却因为selector写错或者minAvailable设置不正确导致压根没生效。建议在配置完成后用kubectl get pdb -A看一眼ALLOWED DISRUPTIONS列这个数字表示当前允许被打断的Pod数。如果显示0且你的服务副本数大于1PDB配置基本有问题。第三件事注意优雅终止与就绪探测的配合。我在实际维护中最常见的“抢占后二次故障”场景是低优Pod被驱逐高优Pod很快调度上去但高优Pod的镜像拉取很慢启动后流量还没就绪而节点上的其他Pod已经被挤得OOM了。因此高优工作负载最好设置合理的terminationGracePeriodSeconds、startupProbe和资源预留别把抢占当成“万能钥匙”它只是把问题从资源分配层推到了调度和启动最快的路径上。第四件事多节点环境下要主动使用节点亲和性和反亲和性约束缩小抢占范围。默认情况下调度器选择在哪个节点执行抢占会综合考虑最少驱逐数量和最低优先级优先驱逐优先级最低、数量最少的节点。如果你有资源充足但被反亲和性排除在外的节点抢占就不会发生。这时可以给高优任务加上明确的nodeSelector或者在workload之间用PodTopologySpread让负载更均匀。曾经有个核心任务是AI训练任务要求跨节点有GPU资源但因为反亲和把节点排除了大半抢占了半天还是Pending这类问题在日志里看很久才定位到。最后说一个调度器调试的实操技巧。遇到“不知道哪一步没走通”的情况先给kube-scheduler加-v4或-v5日志级别然后重点看三个关键字FailedScheduling过滤失败原因、PostFilter是否进入抢占流程、preemptvictim选择与提名结果。日志里如果出现Pod does not fit on node但不带preemption前缀说明抢占逻辑没有对该节点执行如果出现了Found victims on node说明victim已经被选中接下来就看Pod的删除和调度行为。这一套组合拳打下来90%的抢占问题都能定位到具体环节。再说回开头那个高优任务Pending的问题。那次最终查明原因其实是低优服务运行在多个节点上节点全部打了LargeNode的污点高优Pod没容忍这些污点Filter阶段全失败调度器dry-run发现即使把所有低优Pod都杀了也无法满足污点容忍于是干脆不抢占。最后不是靠调优先级解决的而是给高优Pod补了正确的tolerations才顺利调度。这件事给我最大的教训是抢占不是万能的它只是PostFilter阶段的一次“尽力而为”前置的Filter规则如果本身就排斥这个Pod那后置的抢占逻辑再努力也没用。我个人的习惯是在定义PriorityClass时就把抢占策略想清楚高价值任务默认开启抢占但配上PDB保护核心依赖批处理任务设置preemptionPolicy: Never让它们在余量资源中“佛系”运行每个PriorityClass的description字段写清楚适用范围防止后人误用。实验里那个“低优Deployment被杀后重建循环”的问题其实生产中也经常出现所以低优工作负载如果能承载中断最好配合HPA和优雅退出逻辑让重建过程不至于引发雪崩。最后再留一个小技巧如果你后续想给集群里的某些Pod做“永不让出”的约束除了PDB之外还可以把它的PriorityClass设为跟高优任务同一档甚至更高并配上明确的注释说明抢占只会发生在严格低优先级Pod身上这是硬规则摸透了之后所有“谁给谁让路”的问题都能一眼看穿。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价