高优先级任务等待超过30分钟的情况在共享集群里并不罕见。一个查询任务申请8个executor,但集群里跑着几十个低优先级的定时批处理,每个都占着计算槽位不释放。此时如果调度器只做排队,高优任务可能永远轮不到资源。抢占式调度的价值就在这里,它允许调度器根据优先级差和资源缺口主动挑选低优任务终止,让高优任务尽快进入执行状态。但抢占不是简单地把进程杀掉,它涉及队列配额、优先级比较、资源回收和任务重启成本等多层判断。

一、触发抢占的判定条件
抢占调度通常不会因为某个任务优先级高就无条件触发。多数调度器会在资源总量不足且高优任务无法调度时,再去检查是否可以从低优任务手上回收资源。以 Kubernetes 为例,Pod 的 priorityClassName 决定任务优先级,preemptionPolicy 字段可以设置为 PreemptLowerPriority 或 Never。高优 Pod 处于 Pending 状态后,调度器会遍历节点上的低优 Pod,按照优先级从低到高选出足以满足资源请求的牺牲者集合。这个筛选过程不仅看 CPU 和内存,还会考虑节点亲和性、拓扑分布约束以及 Pod 的终止宽限期。
一个常见的误区是只盯着瞬时资源水位。比如某个节点显示内存剩余 4GB,高优任务刚好需要 4GB,但调度器仍然可能不抢占,因为资源碎片和预留规则会导致这 4GB 无法分配。更可靠的做法是结合 requests 与 limits 的差异,以及节点上已经预留但未实际使用的资源。YARN 的 Capacity Scheduler 和 Apache Mesos 的 revocation 机制也采用类似思路,它们会计算一个资源缺口,再反向选择受害任务,目标是释放尽量少的资源、影响尽量少的正在运行任务。
下面是一段简化后的受害者选择逻辑,核心是从低优先级开始累计释放量,直到满足高优任务需求。真实实现还会加入已运行时长保护、最大可抢占比例和队列边界等限制。
def select_victims(candidates, needed_resources, preemptor_priority):
candidates.sort(key=lambda t: t.priority)
victims = []
freed = Resource(0, 0)
for task in candidates:
if task.priority >= preemptor_priority:
continue
victims.append(task)
freed.add(task.allocated)
if freed.satisfies(needed_resources):
break
return victims
二、任务排队模型与优先级反转治理
排队治理的目标不是让所有任务都尽快运行,而是让不同优先级的任务在合理时间内获得资源,同时避免低优任务被无限期饿死。一个直观的做法是将集群划分成多个队列,每个队列配置独立容量、最大容量和最大运行任务数。高优队列可以借用低优队列的空闲资源,但低优队列不能反向占用高优队列的保证资源。这种设计能减少跨队列抢占的发生频率,也方便从租户视角做成本核算。
优先级反转在任务调度里的典型表现是:低优任务持有一个高优任务需要的本地缓存或共享文件句柄,高优任务排队等待,而中优任务却不断通过抢占获得资源,最终导致高优任务被中优任务长期压制。治理这个问题需要在队列层面设置严格的优先级顺序,在抢占选择时排除正在执行检查点或持有临界资源的任务,并为低优任务设置最小运行时长保护。比如 Spark 的动态分配机制中,executor 释放前会有 idle timeout,避免任务刚启动就被回收。
实际配置中可以通过 PriorityClass 定义多个优先层级,并为每个层级设置不同策略。例如下面的配置创建了高优先级等级,并允许抢占低优先级任务,同时设置了 Pod 的终止宽限期。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
---
apiVersion: v1
kind: Pod
metadata:
name: batch-job
spec:
priorityClassName: high-priority
terminationGracePeriodSeconds: 30
containers:
- name: app
image: demo-app:latest
队列配额方面,建议把队列的 maximum-capacity 设置得比 capacity 高一些,允许短时弹性借用,但不要让单个队列长期占满全部资源。这样在高优任务突发时可以通过抢占快速腾挪,而在低峰期又能保持较高资源利用率。
三、抢占执行与资源回收的细节
抢占动作不是瞬间完成。被选中的任务会先收到终止信号,调度器根据宽限期等待进程保存状态、释放连接和清理临时文件,超过宽限期才会强制终止。在 Kubernetes 中,terminationGracePeriodSeconds 控制这个等待时间。对于无状态批处理任务,通常设置 10 到 30 秒即可;对于数据库或流处理任务,需要给更长时间完成 checkpoint 和 offset 提交。设置过短会导致数据丢失,设置过长又会让高优任务等待更久,所以需要结合任务类型做分层配置。
资源回收环节同样容易出问题。进程被终止后,节点上的内存可能不会立刻完全释放,本地磁盘缓存和网络连接也需要时间清理。如果调度器在资源尚未完全回收时就把高优任务调度上来,可能出现启动失败或性能抖动。一些平台会在抢占后等待一个冷却窗口,确认节点资源可用后再进行下一轮调度。Spark 的 executor 退出时,Driver 会收到通知并重新申请资源,如果集群压力大,这个过程可能反复发生,造成任务长时间无法恢复。
为降低这类风险,可以限制单轮抢占的任务数量,比如一次最多终止 20% 的低优任务,并且对同一个任务设置最小运行时间保护。还可以配置调度器监控周期,让连续两次抢占之间保持一定间隔。下面的伪代码表示一个带冷却时间的抢占控制循环,避免系统在高负载下过度抖动。
def preemption_loop(scheduler_state):
if not scheduler_state.need_preemption():
return None
if scheduler_state.cooldown_remaining > 0:
return None
victims = select_victims(
scheduler_state.low_priority_tasks,
scheduler_state.resource_gap,
scheduler_state.high_priority
)
for task in victims:
task.send_stop_signal(grace_period=20)
scheduler_state.cooldown_remaining = 60
return victims
四、可落地的分层治理建议
集群规模不同,抢占策略的激进程度也要有所区别。几十个节点的共享集群,最好先通过多级队列和延迟调度来缓解压力,只有在高优任务等待时间超过阈值时才启用抢占。上百节点的大型平台可以用更细粒度的优先级和自动伸缩,但要避免不同租户之间互相抢占导致账单和审计难以对齐。无论规模如何,都应该把抢占看作最后手段,而不是常规调度选项。
从落地顺序看,可以先治理队列命名和配额,明确每个队列的容量、最大容量和提交用户限制。接着给任务打上优先级标签,按业务线或负载类型划分层级。然后开启调度器监控,观察任务等待时间、被抢占任务数和资源利用率的变化。最后根据数据调整抢占阈值,比如把高优等待时间从 5 分钟放宽到 10 分钟,或者把单轮抢占比例从 30% 降低到 15%。
常见的关键参数包括监控周期、抢占间隔、队列最大容量和任务重试次数。这些参数之间会相互影响,比如监控周期太短会增加调度器开销,太长又来不及响应突发任务。建议先在测试集群模拟高优任务到达曲线,再逐步上生产。最终的目标不是消灭排队,而是让高优任务能在可接受的时间内得到资源,同时把被抢占任务的损耗控制在较低水平。