2、核心概念:任务、作业、工作流、调度器、执行器

聊任务调度,很多人一上来就扎进代码里。我个人的习惯是,先把几个核心概念掰扯清楚。说白了,这五个词就是整个调度系统的“乐高积木”。你搞懂了它们各自长什么样、怎么拼,后面看什么框架都一通百通。

一句话总结:任务是最小执行单元,作业是任务的集合,工作流是作业的编排,调度器是大脑,执行器是手脚。

2.1 任务(Task)—— 最小执行单元

任务,就是你要让计算机干的一件具体的事。它不能再拆了,拆了就变味儿了。

举个例子:

  • 执行一条 SQL 查询
  • 运行一个 Shell 脚本
  • 调用一个 HTTP 接口
  • 处理一个文件

我在项目中见过最坑的事,就是把一个“下载 10GB 文件并解压再清洗”的逻辑写成一个任务。结果每次重跑都要从头下载,气得运维小哥直拍桌子。记住,任务要足够小、足够原子。

我的经验:一个任务最好能在 5 分钟内跑完。超过这个时间,你就该考虑拆分了。不是技术做不到,而是排查问题时你会感谢自己。

2.2 作业(Job)—— 任务的集合

作业,就是把一组相关的任务打包在一起。它定义了这些任务怎么跑:串行还是并行?失败后重试几次?超时时间是多少?

你想想看,一个数据仓库的 ETL 流程,通常包含:

  1. 抽取数据(Extract)
  2. 清洗数据(Clean)
  3. 转换格式(Transform)
  4. 加载到目标表(Load)

这四个步骤,每个都是一个任务。把它们组合成一个“每日数据同步作业”,就是作业干的事。

概念 粒度 是否可独立调度 典型例子
任务 最细 一般不单独调度 执行一条 SQL
作业 中等 是 每日数据同步

2.3 工作流(Workflow)—— 作业的编排

工作流,说白了就是“谁先谁后”的问题。单个作业好办,但现实中的业务往往是多个作业互相依赖的。

比如:

  • 作业 A(数据采集)必须先跑完
  • 然后作业 B(数据清洗)和作业 C(数据聚合)可以并行跑
  • 最后作业 D(生成报表)等 B 和 C 都跑完才能开始

这就是一个典型的工作流。我曾经接手过一个老系统,依赖关系写在 Shell 脚本的 sleep 里——等 5 分钟,觉得上游该跑完了。结果呢?数据对不上,排查了一整天才发现是时序问题。

避坑指南:工作流的依赖关系一定要显式声明,不要靠“时间窗口”来隐式保证。我曾经吃过这个亏,凌晨三点被电话叫醒去修数据管道,那种滋味……嗯,你懂的。

2.4 调度器(Scheduler)—— 大脑

调度器是整个系统的核心。它负责三件事:

  1. 什么时候跑 —— 基于时间表达式(Cron、时间间隔等)
  2. 跑什么 —— 触发哪个作业或工作流
  3. 跑完怎么办 —— 成功、失败、重试、告警

调度器不干活,它只发号施令。就像项目经理,自己不写代码,但知道谁该干什么、什么时候干。

常见的调度器有 Quartz、Airflow 的 Scheduler、XXL-JOB 的调度中心。它们的设计思路大同小异,核心就是一个“时间轮”或者“优先级队列”。

2.5 执行器(Executor)—— 手脚

执行器,就是真正干活的。它收到调度器的指令,拉起任务进程,监控运行状态,然后把结果汇报回去。

执行器通常部署在 worker 节点上。我习惯把执行器设计成无状态的——这样扩容特别方便,机器不够了直接加一台,调度器自动分配任务过去。

关键设计原则:调度器和执行器一定要解耦。调度器挂了,正在跑的任务不受影响;执行器挂了,调度器可以把任务分给其他执行器。这是高可用的基础。

2.6 它们之间的关系

下面这张图,把五个概念串起来了。我每次给团队新人讲调度系统,都会先画这张图。

任务调度核心概念关系图 任务调度核心概念关系图 调度器(Scheduler) 大脑 · 决定何时触发 执行器(Executor) 手脚 · 真正干活 下发指令 工作流(Workflow) 作业的编排 作业(Job) 任务的集合 任务(Task) 最小执行单元 包含 包含 触发 触发 执行 说明: 1. 调度器触发工作流或作业,不直接触发任务 2. 执行器接收调度指令,拉起任务进程 3. 工作流包含多个作业,作业包含多个任务

2.7 实际项目中的分层设计

我参与过的一个金融级调度平台,就是按这五个概念来分层的:

  • API 层:接收用户提交的作业定义和工作流 DAG
  • 调度层:基于 Quartz 做时间触发,把就绪的作业扔进消息队列
  • 执行层:Worker 进程从队列拉取任务,启动子进程执行
  • 存储层:MySQL 存元数据,Redis 存实时状态

这种分层的好处是,每一层都可以独立扩展。调度层压力大了,加调度节点;执行层任务积压了,加 Worker。互不干扰。

一个小技巧:任务执行结果一定要有明确的“成功/失败/超时”状态,不要搞什么“部分成功”。我在代码里见过返回码是 0 但数据没写全的情况,排查起来特别痛苦。

2.8 常见误区

最后,说几个我踩过的坑:

  1. 把任务当作业用 —— 一个任务里塞了太多逻辑,失败重跑成本极高
  2. 工作流画成蜘蛛网 —— 依赖关系太复杂,一个节点挂了影响一大片
  3. 调度器和执行器混在一起 —— 调度器挂了,执行器也跟着挂,没有高可用
  4. 忽略任务幂等性 —— 同一个任务跑两次,数据翻倍了

嗯,这些概念看起来简单,但真正落地的时候,每一个都值得深挖。后面我们会逐个展开,看看它们在实际框架里是怎么实现的。