2. 任务调度基础:定义、核心功能与常见策略
聊流批一体之前,咱们得先把地基打牢。任务调度这东西,说白了就是「谁先干、谁后干、干多久」的问题。我见过不少团队,一上来就搞分布式调度框架,结果连最基本的 FIFO 和优先级都没搞清楚,最后系统乱成一锅粥。
这一节,咱们就踏踏实实把任务调度的底裤扒干净。
2.1 任务调度的定义
任务调度,简单讲就是一套规则,决定计算资源怎么分配给不同的任务。你想想看,一个集群里 CPU、内存、网络带宽都是有限的,几十上百个任务同时跑,谁先拿到资源?谁该被暂停?这就是调度要解决的问题。
我个人习惯把调度理解成「排队系统」的升级版。普通排队就是先来后到,但任务调度要考虑优先级、资源配额、依赖关系、甚至数据本地性。我在项目中遇到过最典型的场景:一个离线批处理任务占着 80% 的集群资源跑了俩小时,结果实时流任务因为拿不到资源频繁延迟报警。嗯,这就是调度策略没设计好。
核心定义:任务调度 = 在有限资源约束下,按照特定策略决定任务执行顺序与资源分配的过程。
2.2 调度系统的核心功能
一个成熟的调度系统,至少得具备下面这五个能力。我按重要程度排了个序,你在选型或自研时可以参考。
| 功能模块 | 说明 | 我踩过的坑 |
|---|---|---|
| 任务管理 | 任务的注册、启停、状态跟踪 | 曾经没做任务状态持久化,调度器挂了全丢了 |
| 资源管理 | CPU、内存、GPU 等资源的分配与回收 | 资源超卖导致任务互相干扰,后来加了 cgroup 隔离 |
| 依赖管理 | 任务间的上下游依赖、DAG 执行 | 环形依赖没检测,调度器死循环了 |
| 策略引擎 | 执行具体的调度算法(FIFO、优先级等) | 优先级队列没做饥饿保护,低优先级任务永远跑不上 |
| 监控与告警 | 任务执行时长、失败重试、资源利用率 | 没监控到任务卡死,数据延迟了 6 小时才发现 |
这里我想多说一句:依赖管理在流批一体场景下特别重要。批任务和流任务之间可能有数据依赖,比如流任务产出的中间结果要等批任务校验完才能用。我曾经在一个项目中,就因为没处理好这种跨类型依赖,导致数据对不上,排查了整整两天。
2.3 常见的调度策略
调度策略是调度系统的灵魂。咱们从最简单的开始,一个一个说清楚。
2.3.1 FIFO(先进先出)
FIFO 就是先来后到。谁先提交,谁先执行。实现简单,逻辑清晰,适合任务类型单一、执行时间差不多的场景。
但它的缺点也很明显——队头阻塞。你想想看,一个跑 10 小时的批任务排在前面,后面所有任务都得等着。实时流任务要是排在这种大任务后面,延迟直接爆炸。
避坑指南:我曾经在一个实时数仓项目里用了纯 FIFO,结果凌晨的批处理任务把白天的流任务全堵住了。后来改成了多级队列 + 优先级抢占,才解决问题。纯 FIFO 只适合实验环境,生产环境慎用。
2.3.2 时间片轮转
时间片轮转,就是把 CPU 时间切成小片,每个任务轮流用。比如每个任务一次最多跑 100ms,时间到了就挂起,换下一个任务跑。
这种策略的好处是公平——每个任务都能得到响应。坏处是上下文切换开销大,而且不适合 I/O 密集型的任务。你想想看,一个任务刚把数据从磁盘读到内存,时间片到了被切走,下次再切回来数据可能已经被换出了,效率很低。
我个人的经验是:时间片轮转适合短任务、交互式任务,比如用户提交的临时查询。对于长周期的批处理任务,时间片轮转反而会拖慢整体吞吐。
2.3.3 优先级调度
优先级调度,就是给每个任务打一个优先级分数,分数高的先执行。这是生产环境最常用的策略。
优先级调度有几个关键点要注意:
- 优先级反转:高优先级任务等低优先级任务释放资源,导致高优先级反而跑得慢。解决方案是优先级继承或优先级天花板协议。
- 饥饿问题:高优先级任务一直来,低优先级任务永远跑不上。解决方案是动态优先级老化——等待时间越长,优先级自动提升。
- 优先级与资源配额解耦:高优先级不代表可以无限占用资源,需要配合资源配额使用。
实战建议:我在设计调度策略时,通常采用「多级反馈队列」的思路——把任务分成几个优先级队列,高优先级队列用时间片轮转保证响应,低优先级队列用 FIFO 保证吞吐。同时每个队列内部做老化,防止饥饿。这样既保证了实时任务的响应速度,又兼顾了批处理任务的吞吐量。
2.4 三种策略对比
为了方便你选型,我把三种策略的核心差异整理成了一张表:
| 策略 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| FIFO | 先来先服务 | 实现简单、公平 | 队头阻塞、响应慢 | 任务类型单一、执行时间相近 |
| 时间片轮转 | 轮流执行固定时间 | 响应均匀、避免饥饿 | 上下文切换开销大 | 短任务、交互式查询 |
| 优先级调度 | 按优先级排序执行 | 灵活、可区分重要任务 | 优先级反转、饥饿问题 | 混合负载、流批一体 |
2.5 调度策略的核心逻辑
下面这张图,我把三种策略的核心逻辑画出来了。你可以看到,FIFO 就是一条队列,时间片轮转是循环队列,优先级调度是多条队列加优先级比较。
2.6 流批一体场景下的调度策略选择
讲完基础,咱们回到流批一体的语境。你想想看,流任务要求低延迟(秒级响应),批任务要求高吞吐(处理大量数据)。这两种任务放在同一个集群里,调度策略怎么选?
我个人推荐多级反馈队列 + 动态优先级老化的组合方案:
- 流任务放在高优先级队列,用时间片轮转保证响应速度
- 批任务放在低优先级队列,用 FIFO 保证吞吐
- 动态老化:批任务等待时间超过阈值,自动提升优先级,防止饥饿
- 资源配额:给流任务预留最低资源保障,批任务使用剩余资源
一个小技巧:我在实际项目中,会给流任务设置一个「最小资源保障线」,比如 20% 的 CPU 和内存是流任务专用的,批任务不能抢占。这样即使批任务再多,流任务的延迟也不会崩。剩下的 80% 资源,流批按优先级竞争。