4、资源调度策略:FIFO调度、优先级调度、公平调度、容量调度、抢占式调度
资源调度,说白了就是决定「下一个轮到谁干活」。
我刚开始做调度系统时,觉得这玩意儿不就是个队列嘛。后来线上出了几次事故,才发现调度策略选不对,整个集群都能给你搞崩掉。今天咱们就把五种主流调度策略掰开揉碎聊清楚。
核心观点:没有银弹。每种调度策略都有自己的适用场景,选错了就是灾难。
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),就是调度器有权强制终止正在运行的任务,把资源让给更高优先级的任务。
说实话,这招有点暴力。但有时候不得不这么做。
抢占式调度的核心流程:
- 监控:持续检查各队列资源使用情况
- 判断:发现高优先级队列资源不足,低优先级队列超量使用
- 选择:从低优先级队列中选出可被抢占的任务
- 执行:发送 kill 信号,回收资源
- 重分配:将回收的资源分配给高优先级队列
重要提醒:抢占不是免费的。被 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 的容量调度内部也支持优先级和抢占。没有最好的策略,只有最适合你场景的策略。
最后说一句:调度策略选型不是一劳永逸的。随着业务发展,你可能需要调整参数甚至更换策略。保持监控,持续优化,才是正道。