4、任务状态机:任务生命周期
任务调度,说白了就是管理任务从生到死的过程。每个任务都有自己的一套生命周期,就像人一样——出生、干活、成功、失败,或者中途被干掉。这套状态转换机制,是调度系统的核心骨架。
我个人习惯把任务状态机比作一个有限状态自动机。你想想看,任务不可能从「Pending」直接跳到「Succeeded」,中间必须经过「Running」。这种约束不是拍脑袋定的,而是为了保证系统行为可预测、可追溯。
4.1 五大核心状态
任务一共就五种状态,不多不少。我见过不少新手把状态设计得特别复杂,十几个状态来回跳,最后自己都搞晕了。其实核心就这五个:
| 状态 | 含义 | 典型场景 |
|---|---|---|
| Pending | 任务已提交,等待调度 | 刚提交任务,资源还没分配 |
| Running | 任务正在执行 | 资源已分配,进程已启动 |
| Succeeded | 任务正常完成 | 退出码为0,输出符合预期 |
| Failed | 任务执行出错 | 退出码非0,或超时、OOM |
| Killed | 任务被主动终止 | 用户取消、系统驱逐、优先级抢占 |
嗯,这里要注意:Killed 和 Failed 是两码事。Failed 是任务自己跑挂了,Killed 是外部主动把它干掉的。我在项目中遇到过有人把这两个状态混为一谈,结果排查问题时浪费了大量时间。
4.2 状态转换触发条件
状态不会自己变,得有触发条件。我整理了一张完整的转换图,你先看看:
这张图我画得比较简洁,但核心逻辑都在里面了。下面我逐个拆解每个转换的触发条件。
4.3 状态转换详解
Pending → Running
这是最关键的转换。触发条件只有一个:资源分配成功。调度器从队列里取出任务,找到满足 CPU、内存、GPU 等资源需求的节点,然后启动任务进程。
我曾经踩过一个坑:任务一直卡在 Pending 状态,查了半天发现是资源请求写错了,请求了 32 核但集群里最大节点只有 16 核。调度器不会帮你自动降级,它就老老实实等着。
kubectl describe pod 或 squeue 看调度事件。
Running → Succeeded
任务进程正常退出,退出码为 0。这里有个细节:不是所有退出码 0 都算成功。我见过有些程序把错误吞掉了,exit 0 但实际输出是空的。所以建议在任务脚本里加个输出校验。
Running → Failed
触发条件比较多:
- 进程退出码非 0
- 被 OOM Killer 杀掉(内核日志会有记录)
- 超过设定的超时时间(timeout)
- 节点宕机或网络中断
嗯,这里要注意:节点宕机导致的 Failed 和 Killed 不一样。节点挂了,任务是被动失败的,不是主动杀死的。调度系统应该能区分这两种情况,否则重试策略会出问题。
Running → Killed
这是主动行为。触发场景包括:
- 用户手动取消(比如点了取消按钮)
- 调度器抢占(高优先级任务来了,低优先级任务被驱逐)
- 资源超量使用被系统强制终止
- 节点维护,需要腾空
我个人习惯在 Killed 事件里记录一下「谁干的」和「为什么」。否则事后复盘时,你根本不知道任务是被用户取消的,还是被系统自动杀掉的。
Pending → Killed
这个转换比较特殊。任务还没开始跑就被杀了。触发条件:
- 用户提交后反悔了,取消提交
- 依赖的上游任务失败了,没必要再跑
- 任务队列被清空(比如集群缩容)
4.4 状态转换的原子性
状态转换必须是原子的。什么意思?就是不能出现「任务既在 Running 又在 Succeeded」的情况。我见过一些系统用数据库行锁来实现,也有用乐观锁的。
举个例子,假设你用 MySQL 存任务状态:
-- 不安全的写法
UPDATE tasks SET status = 'Running' WHERE id = 123;
-- 如果这里并发执行,可能两个调度器同时把任务设为 Running
-- 安全的写法(乐观锁)
UPDATE tasks SET status = 'Running', version = version + 1
WHERE id = 123 AND status = 'Pending' AND version = 5;
-- 如果影响行数为 0,说明状态已被其他进程修改
你想想看,如果状态转换不是原子的,两个调度器同时调度同一个任务,那不就跑了两份?资源就浪费了。
4.5 状态持久化与恢复
任务状态不能只存在内存里。万一调度器挂了,重启后得知道每个任务跑到哪一步了。我建议:
- 每次状态变更都写数据库或 etcd
- 写成功后才算状态变更完成
- 启动时从持久化存储恢复所有任务状态
说白了,就是「先写日志,再干活」。这个习惯我在做分布式系统时养成的,后来发现用在任务调度上同样适用。
核心要点回顾:
- 五种状态:Pending、Running、Succeeded、Failed、Killed
- 状态转换必须有明确的触发条件
- 转换过程必须原子化,防止并发问题
- 状态需要持久化,支持故障恢复
- Killed 和 Failed 要区分清楚,影响重试策略
任务状态机看着简单,但实际落地时坑不少。我建议你在设计阶段就把状态转换图画清楚,每个转换的触发条件写明白,最好再画个状态转换矩阵。这样开发时不容易漏掉边界情况。