4、资源调度策略:FIFO调度、优先级调度、公平调度、容量调度、抢占式调度

资源调度,说白了就是决定「下一个轮到谁干活」。

我刚开始做调度系统时,觉得这玩意儿不就是个队列嘛。后来线上出了几次事故,才发现调度策略选不对,整个集群都能给你搞崩掉。今天咱们就把五种主流调度策略掰开揉碎聊清楚。

核心观点:没有银弹。每种调度策略都有自己的适用场景,选错了就是灾难。

资源调度策略全景图 资源调度器 FIFO 调度 优先级调度 公平调度 容量调度 抢占式 先来先服务 简单公平 无优先级 按优先级排序 高优先级先执行 可能饿死低优先级 资源均分 动态调整 避免资源闲置 队列容量保证 多租户隔离 弹性共享 资源回收 强制终止 保障SLA 选择建议 简单场景 → FIFO | 需要优先级 → 优先级调度 | 多租户 → 容量调度 资源均衡 → 公平调度 | 保障SLA → 抢占式调度

4.1 FIFO 调度:最简单的排队策略

FIFO(First In First Out),先来先服务。就像银行排队,谁先到谁先办。

实现起来极其简单,一个队列就搞定。我早期做的一个小项目就用这个,代码量不到 50 行。

// FIFO 调度伪代码
Queue<Job> jobQueue = new LinkedList<>();

void submitJob(Job job) {
    jobQueue.add(job);
}

Job schedule() {
    return jobQueue.poll(); // 队头出队
}

适用场景:任务执行时间相近、对响应时间不敏感的场景。比如批量数据导入、日志归档。

避坑:我曾经在一个实时流处理系统里用了 FIFO,结果一个耗时 10 分钟的大任务堵住了后面所有秒级任务。线上告警直接炸了。FIFO 不适合长短任务混跑的场景。

4.2 优先级调度:给任务分三六九等

优先级调度,就是给每个任务打一个优先级标签。调度器永远选优先级最高的任务执行。

嗯,这里要注意:优先级怎么定?我见过三种做法:

  • 静态优先级:提交时指定,运行期间不变。简单粗暴。
  • 动态优先级:根据等待时间、资源消耗动态调整。实现复杂,但更公平。
  • 多级反馈队列:多个优先级队列,任务在不同队列间升降级。Linux 进程调度就用这个。
// 优先级调度伪代码
PriorityQueue<Job> jobQueue = new PriorityQueue<>(
    (a, b) -> b.priority - a.priority
);

void submitJob(Job job) {
    jobQueue.add(job);
}

Job schedule() {
    return jobQueue.poll(); // 最高优先级出队
}

核心问题:优先级反转和饥饿。低优先级任务可能永远得不到执行。我遇到过生产环境里一个低优先级的监控任务被饿死,结果集群挂了都没人知道。

解决方案:引入优先级老化机制。任务等待时间越长,优先级自动提升。这样低优先级任务最终也能被执行。

4.3 公平调度:让每个人都能分到蛋糕

公平调度(Fair Scheduling)的目标是让所有任务或用户获得大致相等的资源份额。

说白了,就是「见者有份」。Hadoop 的 Fair Scheduler 就是典型代表。

特性 FIFO 优先级调度 公平调度
资源分配 按到达顺序 按优先级 按权重均分
公平性 中等 差(低优先级可能饿死) 好
实现复杂度 低 中 高
适用场景 简单批处理 有明确优先级需求 多用户共享集群

公平调度有个关键机制叫 min share 和 fair share。每个队列保证最低资源(min share),超出部分按权重公平分配。

我曾经踩过的坑:公平调度默认会抢占资源。有一次没配好抢占策略,导致正在运行的任务被频繁打断,吞吐量反而下降了。后来我加了抢占延迟和最小抢占资源阈值才解决。

4.4 容量调度:多租户场景的标配

容量调度(Capacity Scheduling)是 Hadoop YARN 的默认调度器。它把集群资源划分成多个队列,每个队列有固定的容量保证。

你想想看,公司里不同部门共用一套大数据集群。A 部门说「我每天要跑 100 个任务」,B 部门说「我要 200 个」。容量调度就是给每个部门划好地盘。

// 容量调度配置示例(YARN)
<configuration>
  <property>
    <name>yarn.scheduler.capacity.root.queues</name>
    <value>engineering,marketing</value>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.engineering.capacity</name>
    <value>60</value> <!-- 工程部 60% 容量 -->
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.marketing.capacity</name>
    <value>40</value> <!-- 市场部 40% 容量 -->
  </property>
</configuration>

容量调度 vs 公平调度:容量调度强调「硬性容量保证」,公平调度强调「动态公平分配」。容量调度更适合有明确 SLA 的多租户场景。

4.5 抢占式调度:必要时的暴力手段

抢占式调度(Preemptive Scheduling),就是调度器有权强制终止正在运行的任务,把资源让给更高优先级的任务。

说实话,这招有点暴力。但有时候不得不这么做。

抢占式调度的核心流程:

  1. 监控:持续检查各队列资源使用情况
  2. 判断:发现高优先级队列资源不足,低优先级队列超量使用
  3. 选择:从低优先级队列中选出可被抢占的任务
  4. 执行:发送 kill 信号,回收资源
  5. 重分配:将回收的资源分配给高优先级队列

重要提醒:抢占不是免费的。被 kill 的任务可能丢失中间状态,需要重新计算。我建议设置抢占超时时间,比如任务运行超过 5 分钟就不允许被抢占,避免浪费大量计算。

// 抢占策略配置示例
preemption:
  enabled: true
  threshold: 0.8          # 队列使用率超过 80% 触发抢占
  minimum_preempt: 2       # 最少抢占 2 个容器
  wait_before_kill: 30s    # 发送 kill 前等待 30 秒

个人经验:抢占式调度一定要配合 优雅退出 机制。任务收到 SIGTERM 后,先保存 checkpoint,再退出。这样下次启动可以从断点继续,而不是从头跑。

4.6 如何选择调度策略?

我个人的选择原则很简单:

  • 单用户、任务类型单一:FIFO 就够了,别折腾
  • 有明确的优先级需求:优先级调度,但记得加老化机制
  • 多用户共享集群:容量调度或公平调度,二选一
  • 需要保障核心业务 SLA:抢占式调度,但要做好容错

实际生产环境中,往往是多种策略组合使用。比如 YARN 的容量调度内部也支持优先级和抢占。没有最好的策略,只有最适合你场景的策略。

最后说一句:调度策略选型不是一劳永逸的。随着业务发展,你可能需要调整参数甚至更换策略。保持监控,持续优化,才是正道。