4、任务状态机:任务生命周期

任务调度,说白了就是管理任务从生到死的过程。每个任务都有自己的一套生命周期,就像人一样——出生、干活、成功、失败,或者中途被干掉。这套状态转换机制,是调度系统的核心骨架。

我个人习惯把任务状态机比作一个有限状态自动机。你想想看,任务不可能从「Pending」直接跳到「Succeeded」,中间必须经过「Running」。这种约束不是拍脑袋定的,而是为了保证系统行为可预测、可追溯。

4.1 五大核心状态

任务一共就五种状态,不多不少。我见过不少新手把状态设计得特别复杂,十几个状态来回跳,最后自己都搞晕了。其实核心就这五个:

状态 含义 典型场景
Pending 任务已提交,等待调度 刚提交任务,资源还没分配
Running 任务正在执行 资源已分配,进程已启动
Succeeded 任务正常完成 退出码为0,输出符合预期
Failed 任务执行出错 退出码非0,或超时、OOM
Killed 任务被主动终止 用户取消、系统驱逐、优先级抢占

嗯,这里要注意:Killed 和 Failed 是两码事。Failed 是任务自己跑挂了,Killed 是外部主动把它干掉的。我在项目中遇到过有人把这两个状态混为一谈,结果排查问题时浪费了大量时间。

4.2 状态转换触发条件

状态不会自己变,得有触发条件。我整理了一张完整的转换图,你先看看:

Pending Running Succeeded Failed Killed 资源分配 正常完成 异常退出 主动终止 取消提交 图例 主要转换 次要转换

这张图我画得比较简洁,但核心逻辑都在里面了。下面我逐个拆解每个转换的触发条件。

4.3 状态转换详解

Pending → Running

这是最关键的转换。触发条件只有一个:资源分配成功。调度器从队列里取出任务,找到满足 CPU、内存、GPU 等资源需求的节点,然后启动任务进程。

我曾经踩过一个坑:任务一直卡在 Pending 状态,查了半天发现是资源请求写错了,请求了 32 核但集群里最大节点只有 16 核。调度器不会帮你自动降级,它就老老实实等着。

小技巧:如果任务长时间 Pending,优先检查资源请求是否合理。可以用 kubectl describe pod 或 squeue 看调度事件。

Running → Succeeded

任务进程正常退出,退出码为 0。这里有个细节:不是所有退出码 0 都算成功。我见过有些程序把错误吞掉了,exit 0 但实际输出是空的。所以建议在任务脚本里加个输出校验。

Running → Failed

触发条件比较多:

  • 进程退出码非 0
  • 被 OOM Killer 杀掉(内核日志会有记录)
  • 超过设定的超时时间(timeout)
  • 节点宕机或网络中断

嗯,这里要注意:节点宕机导致的 Failed 和 Killed 不一样。节点挂了,任务是被动失败的,不是主动杀死的。调度系统应该能区分这两种情况,否则重试策略会出问题。

Running → Killed

这是主动行为。触发场景包括:

  • 用户手动取消(比如点了取消按钮)
  • 调度器抢占(高优先级任务来了,低优先级任务被驱逐)
  • 资源超量使用被系统强制终止
  • 节点维护,需要腾空

我个人习惯在 Killed 事件里记录一下「谁干的」和「为什么」。否则事后复盘时,你根本不知道任务是被用户取消的,还是被系统自动杀掉的。

Pending → Killed

这个转换比较特殊。任务还没开始跑就被杀了。触发条件:

  • 用户提交后反悔了,取消提交
  • 依赖的上游任务失败了,没必要再跑
  • 任务队列被清空(比如集群缩容)
避坑指南:我曾经遇到过 Pending 状态的任务被误杀的情况。原因是调度器有个「空闲任务清理」功能,把等待超过 1 小时的任务全杀了。但那个任务其实是在等上游数据,属于正常等待。后来我加了个「等待原因」字段,只有真正异常的任务才清理。

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 要区分清楚,影响重试策略

任务状态机看着简单,但实际落地时坑不少。我建议你在设计阶段就把状态转换图画清楚,每个转换的触发条件写明白,最好再画个状态转换矩阵。这样开发时不容易漏掉边界情况。

可可精品资料库