2. 任务生命周期模型:从创建到销毁的完整状态机
调度任务这东西,说白了就是一个有状态的过程。你提交一个任务,它不会瞬间完成,中间会经历各种状态变化。我见过不少团队,一开始只定义了「成功」和「失败」两种状态,结果线上出问题的时候,连任务到底卡在哪一步都说不清楚。
所以这一章,我们来聊聊任务从生到死的完整状态机模型。我个人习惯把状态分成三类:稳定态、过渡态和终态。稳定态就是任务停在那里等你操作,过渡态是正在执行中,终态就是不会再变了。
核心观点:一个设计良好的状态机,能让你的调度系统在异常发生时快速定位问题,而不是靠猜。
2.1 九种核心状态定义
我参与过的几个数据平台项目,最终沉淀下来的状态模型基本一致。下面这张表,你可以直接拿去用。
| 状态 | 类型 | 含义 | 触发动作 |
|---|---|---|---|
| 创建 | 初始态 | 任务实例刚生成,还没做任何检查 | 调度器实例化任务 |
| 就绪 | 稳定态 | 依赖满足、资源够用,等待执行 | 依赖检查通过 |
| 运行 | 过渡态 | 正在执行用户代码 | 调度器分配资源并启动 |
| 成功 | 终态 | 执行完成,退出码为0 | 任务正常结束 |
| 失败 | 终态 | 执行异常,退出码非0 | 任务异常退出 |
| 重试 | 过渡态 | 失败后自动重新调度 | 重试策略触发 |
| 暂停 | 稳定态 | 人为挂起,不执行也不销毁 | 运维手动操作 |
| 终止 | 终态 | 人为强制结束运行中的任务 | 运维手动操作 |
| 废弃 | 终态 | 任务不再需要,标记清理 | 生命周期策略触发 |
嗯,这里要注意,「创建」和「就绪」虽然看起来差不多,但区别很大。创建只是生成了一个实例,还没做依赖检查。就绪是确认了所有上游都跑完了,资源也预留好了。我曾经在一个项目里把这两个状态合并了,结果上游任务重跑时,下游任务莫名其妙就启动了,数据对不上,排查了两天才发现是状态设计的问题。
2.2 状态流转图
光看表格不够直观,我画了一张状态机流转图。你盯着看一分钟,整个生命周期就清楚了。
你仔细看这张图,会发现几个关键点。第一,「运行」是唯一能产出「成功」或「失败」的状态。第二,「重试」不是直接回到「运行」,而是先回到「就绪」重新排队。为什么?因为重试时可能需要重新分配资源,或者等待依赖重新跑完。我见过有人把重试直接连到运行,结果资源没释放,任务跑起来就报错。
2.3 状态流转的代码实现
理论说完了,来点实际的。下面是一个简化版的状态机实现,用 Python 写的。你注意看状态转移的校验逻辑。
class TaskStateMachine:
def __init__(self):
# 定义合法的状态转移
self._transitions = {
'created': ['ready'],
'ready': ['running', 'paused', 'abandoned'],
'running': ['success', 'failed', 'paused', 'killed'],
'failed': ['retrying', 'killed', 'abandoned'],
'retrying': ['ready'],
'paused': ['ready', 'abandoned'],
'success': ['abandoned'],
'killed': ['abandoned'],
'abandoned': [] # 终态,不可再转移
}
def transition(self, task, new_state):
current = task.state
if new_state not in self._transitions.get(current, []):
raise ValueError(
f"非法状态转移: {current} -> {new_state}"
)
# 记录状态变更日志
task.state = new_state
task.state_changed_at = datetime.now()
# 这里可以触发回调,比如发送告警
self._on_state_change(task, current, new_state)
def _on_state_change(self, task, old, new):
# 实际项目中,这里会写日志、发消息、更新缓存
print(f"[状态变更] {task.id}: {old} -> {new}")
我的习惯:每次状态变更都记录完整的时间戳和操作人。线上排查问题时,这行日志能帮你省下至少半小时。
2.4 避坑指南:状态设计中的常见问题
做状态机设计,有几个坑我踩过,你注意避开。
- 状态爆炸:有人把「重试中」「重试成功」「重试失败」都单独定义成状态。其实没必要,重试只是一个过渡动作,最终还是会落到「成功」或「失败」。状态越多,维护成本越高。
- 缺少暂停态:我曾经在一个项目里没设计暂停态,结果上游数据延迟,下游任务已经跑起来了,只能手动 kill 掉再重跑。有了暂停态,你可以优雅地挂起任务,等条件满足再恢复。
- 终态不可逆:记住,「成功」「失败」「终止」「废弃」一旦进入,就不能再变。我见过有人把「失败」的任务手动改成「就绪」重新跑,结果状态审计对不上,数据链路全乱了。正确的做法是:失败后走「重试」流程,或者新建一个实例。
- 并发状态冲突:同一个任务实例,不能同时处于「运行」和「暂停」。分布式环境下,一定要用乐观锁或分布式锁来保证状态变更的原子性。
特别注意:状态机的实现一定要幂等。比如「成功」状态被重复设置两次,不能报错,也不能产生副作用。你的下游系统可能因为网络重试而重复调用状态变更接口。
2.5 状态持久化与查询
状态数据怎么存?我个人建议用一张单独的状态变更历史表,而不是只更新任务表里的最新状态。你想想看,如果只存最新状态,你怎么知道这个任务重试了几次?每次重试间隔多久?
| 字段 | 类型 | 说明 |
|---|---|---|
| task_id | string | 任务实例ID |
| from_state | string | 变更前状态 |
| to_state | string | 变更后状态 |
| operator | string | 操作人(系统或用户) |
| reason | string | 变更原因 |
| created_at | datetime | 变更时间 |
有了这张表,你可以轻松回答这些问题:这个任务为什么失败了?谁把它暂停了?重试了几次?每次间隔多久?
好了,状态机的核心模型就这些。你把这个模型吃透了,后面讲调度策略、重试机制、监控告警,都会顺很多。记住一句话:状态设计的好坏,直接决定了你的调度系统能扛多大的复杂度。