第三章 任务模型与生命周期:任务定义与状态机
说实话,任务调度这块,我见过太多人一上来就写调度策略,结果连任务本身都没定义清楚。嗯,这就像盖楼不打地基——迟早要塌。今天咱们就把任务模型和生命周期彻底聊透。
3.1 任务定义的三种范式
任务定义,说白了就是告诉调度系统「你要干什么」。我个人习惯把任务分成三类:DAG任务、批处理任务、流式任务。这三类在底层实现上差异很大,但上层抽象可以统一。
3.1.1 DAG任务(有向无环图)
DAG任务是我在项目中用得最多的。它描述的是任务之间的依赖关系——A跑完才能跑B,B和C都跑完才能跑D。这种结构天然适合ETL、模型训练这类有严格顺序要求的场景。
举个例子,一个典型的DAG定义长这样:
{
"dag_id": "etl_pipeline",
"nodes": [
{"id": "extract", "type": "source", "timeout": 300},
{"id": "transform", "type": "compute", "deps": ["extract"], "timeout": 600},
{"id": "load", "type": "sink", "deps": ["transform"], "timeout": 120}
],
"edges": [
{"from": "extract", "to": "transform"},
{"from": "transform", "to": "load"}
]
}
你想想看,如果不用DAG,你怎么保证load在extract之前不会跑?用脚本硬写条件判断?我曾经接手过一个项目,就是用shell脚本硬写依赖,结果每次加新任务都要改一堆if-else,维护成本高得吓人。
3.1.2 批处理任务
批处理任务,就是那种「你给我一堆数据,我一次性处理完」的模式。它没有复杂的依赖关系,但通常对资源消耗很大。
批处理任务的核心参数我列一下:
| 参数 | 说明 | 我的建议值 |
|---|---|---|
| batch_size | 每次处理的数据量 | 1000-10000条,视内存而定 |
| max_retries | 失败重试次数 | 3次,再多就是代码问题了 |
| resource_profile | CPU/内存需求 | 按峰值预估,别抠门 |
批处理任务有个坑——你以为它是「一次性」的,但实际生产环境里,它经常被调度系统反复拉起。比如每天凌晨跑一次历史数据修复,本质上就是批处理。我建议把批处理任务设计成幂等的,跑多少次结果都一样。
3.1.3 流式任务
流式任务,嗯,这个有点不一样。它没有明确的「开始」和「结束」,而是一直在运行,持续消费数据源。
流式任务的定义通常包含:
- source:数据源,比如Kafka topic、文件目录
- transform:处理逻辑,通常是窗口聚合或状态计算
- sink:输出目标,比如数据库、消息队列
- checkpoint:容错机制,记录处理进度
我记得有一次,一个流式任务因为checkpoint配置不当,每次重启都从最早的数据开始重放,结果Kafka积压了上亿条消息。后来我强制要求:流式任务必须配置checkpoint,且checkpoint间隔不能超过60秒。
3.2 任务状态机
任务状态机,说白了就是任务的一生。从提交到结束,它会经历哪些状态?每个状态之间怎么转换?
下面这张图是我自己总结的标准状态机:
3.2.1 提交状态
任务刚被用户或系统创建,还没进入调度队列。这个阶段,调度系统会做两件事:
- 校验:检查任务定义是否合法,比如DAG有没有环、参数是否完整
- 持久化:把任务信息写入数据库,防止丢任务
我曾经遇到过一个bug:任务提交后直接消失了。查了半天,发现是校验阶段抛了异常,但异常被吞掉了。后来我加了一条铁律——提交阶段任何异常都必须记录到审计日志。
3.2.2 等待状态
任务通过校验,进入调度队列。但它不一定马上能跑——可能资源不够,可能依赖的任务还没完成。
等待状态是调度系统最核心的优化点。我见过一些系统,等待队列里堆了几万个任务,调度器却不知道先跑哪个。嗯,这里就需要优先级和公平策略了。
3.2.3 运行状态
任务被分配到资源,开始执行。这个阶段,调度系统要持续监控:
- 心跳检测:任务还在不在跑?有没有挂掉?
- 资源使用:CPU、内存、IO是否正常?
- 进度反馈:跑了多少?还剩多少?
运行状态有个容易忽略的点——任务可能「假死」。就是进程还在,但已经不干活了。我遇到过好几次,任务占着资源不释放,调度器以为它还在跑,结果整个集群被拖垮。解决方案是加一个「活跃度检测」,定期检查任务是否有数据输出。
3.2.4 完成状态
任务正常结束。这个状态看起来简单,但有个细节:完成不代表成功。有些任务跑完了,但结果不符合预期。比如ETL任务跑完了,但目标表里一条数据都没写进去。
我建议在完成状态后加一个「结果验证」步骤。比如检查输出行数、校验数据质量。如果验证不通过,可以自动触发回滚或重跑。
3.2.5 失败状态
任务异常终止。失败的原因五花八门:代码bug、资源不足、数据异常、网络超时……
失败处理是调度系统的「兜底」能力。我的经验是:
- 自动重试:对于临时性故障(比如网络抖动),重试3次
- 死信队列:重试3次还失败,丢进死信队列,人工介入
- 依赖传播:如果A任务失败,依赖A的B、C任务也要标记为「上游失败」
3.3 状态机的最佳实践
说了这么多,总结几条我踩坑踩出来的经验:
| 实践 | 说明 |
|---|---|
| 状态持久化 | 每次状态变更都写数据库,别只存在内存里。不然调度器一重启,任务状态全丢了 |
| 状态幂等 | 同一个任务重复提交,不会产生两个实例。用唯一ID去重 |
| 状态超时 | 每个状态都要有超时时间。比如「等待」超过10分钟,自动告警 |
| 状态审计 | 记录每次状态变更的时间、原因、操作人。出了问题好回溯 |
嗯,任务模型和生命周期就聊到这儿。记住一句话:任务定义决定了调度系统能做什么,状态机决定了调度系统怎么保证正确性。这两块搞扎实了,后面的调度策略、资源管理才有意义。
公众号:蓝海资料掘金营,微信deep3321