任务模型设计:任务抽象、状态机、优先级与依赖
任务模型是整个调度系统的地基。地基没打好,后面再怎么优化都白搭。我见过不少团队一上来就写调度逻辑,结果任务状态乱成一锅粥,依赖关系理不清,最后只能推倒重来。今天咱们就把这块说透。
任务抽象:一个任务到底长什么样?
说白了,任务就是一个可执行的工作单元。但光有「执行」还不够,你得告诉调度器:这个任务是谁、什么时候能跑、跑完结果怎么处理。
我个人习惯把任务抽象成三个层次:
- 元信息层:任务ID、名称、创建时间、所属业务线
- 执行信息层:执行器类型、入参、超时时间、重试策略
- 调度信息层:优先级、依赖列表、状态、队列归属
来看一个简化版的任务模型定义:
class Task:
def __init__(self, task_id, name, executor_type, params):
# 元信息
self.task_id = task_id
self.name = name
self.created_at = time.time()
# 执行信息
self.executor_type = executor_type # 'spark', 'flink', 'python'
self.params = params
self.timeout_secs = 300
self.retry_policy = RetryPolicy(max_attempts=3, backoff=2.0)
# 调度信息
self.priority = Priority.NORMAL
self.dependencies = [] # 依赖的任务ID列表
self.state = TaskState.PENDING
self.queue_name = 'default'
任务状态机:别让状态乱飞
状态机是任务模型里最容易出坑的地方。我见过最夸张的一个项目,任务状态有 17 种,光「等待中」就分了三种。你想想看,调度器每次判断状态都要做一堆 if-else,不出 bug 才怪。
我建议只保留 6 个核心状态:
| 状态 | 含义 | 可流转到 |
|---|---|---|
| PENDING | 已创建,等待调度 | READY, CANCELLED |
| READY | 依赖已满足,可执行 | RUNNING, CANCELLED |
| RUNNING | 正在执行 | SUCCEEDED, FAILED, TIMEOUT |
| SUCCEEDED | 执行成功 | (终态) |
| FAILED | 执行失败 | READY(重试时), CANCELLED |
| TIMEOUT | 超时未完成 | READY(重试时), CANCELLED |
| CANCELLED | 被取消 | (终态) |
状态流转必须严格遵循这张表。我曾经在一个项目里发现有人直接让 PENDING 跳到了 SUCCEEDED,查了半天是因为某个开发在测试代码里写死了状态。嗯,从那以后我强制所有状态变更都走统一的状态机接口。
下面这张图展示了完整的状态流转:
任务优先级:谁先跑,谁后跑?
优先级设计看似简单,但实际落地时坑不少。我建议用「优先级队列 + 权重轮询」的组合方案。
先定义几个优先级等级:
| 等级 | 数值范围 | 适用场景 |
|---|---|---|
| CRITICAL | 90-100 | 线上故障恢复、核心链路 |
| HIGH | 70-89 | 用户实时请求、SLA 任务 |
| NORMAL | 40-69 | 日常批处理、报表生成 |
| LOW | 10-39 | 数据归档、历史清理 |
| BACKGROUND | 0-9 | 预计算、缓存预热 |
为什么用数值范围而不是固定值?因为这样可以在运行时动态调整。比如某个 NORMAL 任务突然需要加速,你可以把它的优先级从 50 提到 65,而不需要改代码。
任务依赖:DAG 是核心
任务依赖本质上是一个有向无环图(DAG)。每个任务可以依赖多个前置任务,只有所有前置任务都成功了,当前任务才能进入 READY 状态。
依赖关系有三种常见类型:
- 强依赖:前置任务必须成功,否则当前任务不执行
- 弱依赖:前置任务失败时,当前任务可以跳过依赖继续执行
- 条件依赖:根据前置任务的输出结果决定是否执行当前任务
来看一个依赖检查的伪代码:
def can_execute(task, task_store):
"""检查任务是否可以执行"""
for dep_id in task.dependencies:
dep_task = task_store.get(dep_id)
if dep_task is None:
return False, f"依赖任务 {dep_id} 不存在"
if dep_task.state == TaskState.SUCCEEDED:
continue
if dep_task.state in (TaskState.FAILED, TaskState.TIMEOUT):
if task.dependency_type == 'weak':
continue # 弱依赖,跳过
else:
return False, f"依赖任务 {dep_id} 失败"
# 依赖任务还在执行或等待中
return False, f"依赖任务 {dep_id} 未完成"
return True, "所有依赖已满足"
组合设计:优先级 + 依赖的协同
优先级和依赖不是孤立的。举个例子:一个 HIGH 优先级的任务依赖了一个 LOW 优先级的任务。如果 LOW 任务一直排不上队,HIGH 任务就永远无法执行。这就是典型的「优先级反转」问题。
我的解决方案是「依赖传递优先级」:
- 当 HIGH 任务依赖 LOW 任务时,LOW 任务的优先级临时提升到 HIGH
- LOW 任务执行完毕后,恢复原始优先级
- 如果 LOW 任务还有自己的依赖,递归传递
这样做的好处是避免了死锁,坏处是增加了调度器的复杂度。我个人建议只在 CRITICAL 和 HIGH 等级的任务上启用这个机制,NORMAL 及以下的任务不需要。
好了,任务模型的核心设计就这些。记住三个关键词:抽象清晰、状态可控、依赖有序。下一块咱们聊聊调度策略——怎么把这些任务高效地分配出去。