3. 任务调度模型:任务状态机、调度器核心抽象、任务依赖关系建模
任务调度模型是分布式 DAG 调度系统的逻辑内核。它定义了任务从创建到完成的完整生命周期(状态机)、调度器如何与执行引擎交互(核心抽象),以及任务之间的先后与数据依赖关系(依赖建模)。本章从这三个维度展开,构建调度模型的形式化基础。
3.1 任务状态机:生命周期的精确控制
每个任务实例在 DAG 调度系统中都经历一组有限的状态转换。标准的状态机设计包含以下核心状态:
| 状态 | 含义 | 触发条件 |
|---|---|---|
| PENDING | 任务已创建,等待依赖条件满足 | DAG 解析完成,任务节点初始化 |
| READY | 所有上游依赖已满足,等待调度 | 上游任务全部进入 SUCCESS 状态 |
| RUNNING | 任务正在执行引擎中运行 | 调度器分配资源并提交执行 |
| SUCCESS | 任务正常完成 | 执行引擎返回成功信号 |
| FAILED | 任务执行出错 | 执行引擎返回失败信号或超时 |
| SKIPPED | 任务被跳过(条件分支未命中) | 上游条件判断为 false |
| CANCELED | 任务被用户或系统取消 | 手动取消或 DAG 整体终止 |
设计要点: 状态机必须保证幂等转换——同一状态在相同输入下只能产生唯一输出。例如,从 RUNNING 到 SUCCESS 的转换必须携带执行结果校验,防止网络抖动导致重复提交。
下图展示了任务状态机的完整转换拓扑:
实践建议: 在实现状态机时,使用事件驱动 + 状态表模式,而非嵌套 if-else。状态表将每个 (当前状态, 事件) 映射到 (下一状态, 动作),便于测试和审计。
3.2 调度器核心抽象:从策略到执行
调度器是 DAG 调度系统的中枢,负责将 READY 状态的任务分配给执行引擎。核心抽象包含三个层次:
- 调度策略(Scheduling Policy):决定“下一个调度哪个任务”。常见策略包括:
- FIFO:按就绪时间顺序调度,简单但缺乏优先级控制。
- 优先级队列:为任务分配权重,高优先级任务先执行。
- 关键路径优先:优先调度 DAG 关键路径上的任务,减少整体完成时间。
- 资源感知:结合集群资源利用率,避免热点节点过载。
- 调度队列(Scheduling Queue):管理 READY 任务的缓冲区。支持入队、出队、优先级重排、去重等操作。
- 执行适配器(Execution Adapter):将调度决策转化为对执行引擎的调用。适配器封装了与 Kubernetes、YARN、本地线程池等不同后端的通信细节。
核心抽象接口(伪代码):
interface Scheduler {
// 注册 DAG 实例
void registerDAG(DAGInstance dag);
// 调度下一个就绪任务
TaskInstance schedule();
// 处理任务完成事件
void onTaskComplete(TaskInstance task, TaskResult result);
// 处理任务失败事件
void onTaskFailed(TaskInstance task, TaskError error);
// 获取当前调度状态
SchedulingStatus getStatus();
}
下图展示了调度器核心抽象的内部架构:
常见陷阱: 调度器不应直接依赖执行引擎的 API 细节。务必通过适配器模式解耦,否则更换执行后端时需要重写调度器核心逻辑。
3.3 任务依赖关系建模:DAG 的骨架
任务依赖关系定义了 DAG 的结构。建模时需区分两种基本依赖类型:
- 控制依赖(Control Dependency):任务 B 必须在任务 A 完成后才能开始。这是 DAG 中最常见的依赖形式,对应“先决条件”关系。
- 数据依赖(Data Dependency):任务 B 需要任务 A 的输出数据作为输入。数据依赖通常隐含控制依赖,但反之不成立。
依赖关系的建模方式直接影响调度器的复杂度:
| 建模方式 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 邻接表 | 每个任务维护上游/下游任务 ID 列表 | 实现简单,遍历快 | 依赖关系变更时需更新多个节点 |
| 邻接矩阵 | 使用 N×N 矩阵表示任务间依赖 | 查询任意依赖关系 O(1) | 空间复杂度 O(N²),不适合大规模 DAG |
| 边列表 | 独立存储所有 (上游, 下游) 边 | 易于序列化和持久化 | 查询某任务的所有依赖需全表扫描 |
| 混合模型 | 邻接表 + 边索引 | 兼顾查询效率和存储成本 | 实现复杂度较高 |
依赖解析算法: 调度器通过入度计数(in-degree)判断任务是否 READY。初始时,所有入度为 0 的任务进入 READY 队列;每当一个任务完成,其下游任务的入度减 1,入度变为 0 的任务立即进入 READY 队列。该算法时间复杂度 O(V+E),是 DAG 调度的基础。
下图展示了依赖关系建模的两种典型结构:
建模最佳实践: 对于大规模 DAG(节点数 > 10,000),推荐使用混合模型:邻接表存储依赖关系,同时维护入度计数缓存。依赖变更时,通过事件通知更新缓存,避免全量重建。
3.4 状态机、调度器与依赖模型的协同
三个组件在运行时紧密协作:
- DAG 解析阶段:根据依赖模型构建邻接表,计算每个任务的初始入度。
- 调度循环:调度器从 READY 队列取出任务,调用执行适配器提交,并将任务状态转为 RUNNING。
- 完成回调:执行引擎返回结果后,调度器更新任务状态为 SUCCESS 或 FAILED,并更新下游任务的入度计数。
- 级联触发:入度变为 0 的下游任务自动进入 READY 队列,等待下一轮调度。
关键设计原则: 状态机负责“是什么”,调度器负责“何时做”,依赖模型负责“谁先做”。三者通过事件机制解耦——任务状态变更时发布事件,调度器和依赖模型分别订阅并做出响应。
下表总结了三个组件的职责边界:
| 组件 | 核心职责 | 输出 | 与外部交互 |
|---|---|---|---|
| 任务状态机 | 管理任务生命周期,保证状态转换合法性 | 状态变更事件 | 接收调度器和执行引擎的事件 |
| 调度器 | 选择就绪任务,分配资源,提交执行 | 调度决策、任务分配 | 与执行适配器、资源管理器交互 |
| 依赖模型 | 维护 DAG 结构,计算入度,检测环 | 依赖关系、入度计数 | 为调度器提供 READY 任务列表 |
性能瓶颈提示: 当 DAG 规模极大(百万级节点)时,入度计数的更新可能成为瓶颈。建议使用批量更新策略:将多个任务完成事件合并后一次性更新下游入度,减少锁竞争和上下文切换。