3. 任务调度模型:任务状态机、调度器核心抽象、任务依赖关系建模

任务调度模型是分布式 DAG 调度系统的逻辑内核。它定义了任务从创建到完成的完整生命周期(状态机)、调度器如何与执行引擎交互(核心抽象),以及任务之间的先后与数据依赖关系(依赖建模)。本章从这三个维度展开,构建调度模型的形式化基础。

3.1 任务状态机:生命周期的精确控制

每个任务实例在 DAG 调度系统中都经历一组有限的状态转换。标准的状态机设计包含以下核心状态:

状态 含义 触发条件
PENDING 任务已创建,等待依赖条件满足 DAG 解析完成,任务节点初始化
READY 所有上游依赖已满足,等待调度 上游任务全部进入 SUCCESS 状态
RUNNING 任务正在执行引擎中运行 调度器分配资源并提交执行
SUCCESS 任务正常完成 执行引擎返回成功信号
FAILED 任务执行出错 执行引擎返回失败信号或超时
SKIPPED 任务被跳过(条件分支未命中) 上游条件判断为 false
CANCELED 任务被用户或系统取消 手动取消或 DAG 整体终止
设计要点: 状态机必须保证幂等转换——同一状态在相同输入下只能产生唯一输出。例如,从 RUNNING 到 SUCCESS 的转换必须携带执行结果校验,防止网络抖动导致重复提交。

下图展示了任务状态机的完整转换拓扑:

任务状态机转换图 PENDING READY RUNNING SUCCESS FAILED SKIPPED CANCELED 虚线表示条件分支或取消路径
实践建议: 在实现状态机时,使用事件驱动 + 状态表模式,而非嵌套 if-else。状态表将每个 (当前状态, 事件) 映射到 (下一状态, 动作),便于测试和审计。

3.2 调度器核心抽象:从策略到执行

调度器是 DAG 调度系统的中枢,负责将 READY 状态的任务分配给执行引擎。核心抽象包含三个层次:

  1. 调度策略(Scheduling Policy):决定“下一个调度哪个任务”。常见策略包括:
    • FIFO:按就绪时间顺序调度,简单但缺乏优先级控制。
    • 优先级队列:为任务分配权重,高优先级任务先执行。
    • 关键路径优先:优先调度 DAG 关键路径上的任务,减少整体完成时间。
    • 资源感知:结合集群资源利用率,避免热点节点过载。
  2. 调度队列(Scheduling Queue):管理 READY 任务的缓冲区。支持入队、出队、优先级重排、去重等操作。
  3. 执行适配器(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();
}

下图展示了调度器核心抽象的内部架构:

调度器核心抽象架构 调度策略 (Scheduling Policy) 调度队列 (Scheduling Queue) 执行适配器 (Execution Adapter) 调度器职责 1. 从 READY 队列选任务 2. 执行资源匹配 3. 提交到执行引擎 4. 处理完成/失败回调 5. 更新 DAG 状态 适配器后端 Kubernetes YARN 本地线程池 Serverless 自定义后端
常见陷阱: 调度器不应直接依赖执行引擎的 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 调度的基础。

下图展示了依赖关系建模的两种典型结构:

任务依赖关系建模 控制依赖示例 任务 A 任务 B 任务 C B 依赖 A C 依赖 B 数据依赖示例 任务 X 任务 Y 任务 Z 输出数据集 A 输出数据集 B
建模最佳实践: 对于大规模 DAG(节点数 > 10,000),推荐使用混合模型:邻接表存储依赖关系,同时维护入度计数缓存。依赖变更时,通过事件通知更新缓存,避免全量重建。

3.4 状态机、调度器与依赖模型的协同

三个组件在运行时紧密协作:

  1. DAG 解析阶段:根据依赖模型构建邻接表,计算每个任务的初始入度。
  2. 调度循环:调度器从 READY 队列取出任务,调用执行适配器提交,并将任务状态转为 RUNNING。
  3. 完成回调:执行引擎返回结果后,调度器更新任务状态为 SUCCESS 或 FAILED,并更新下游任务的入度计数。
  4. 级联触发:入度变为 0 的下游任务自动进入 READY 队列,等待下一轮调度。
关键设计原则: 状态机负责“是什么”,调度器负责“何时做”,依赖模型负责“谁先做”。三者通过事件机制解耦——任务状态变更时发布事件,调度器和依赖模型分别订阅并做出响应。

下表总结了三个组件的职责边界:

组件 核心职责 输出 与外部交互
任务状态机 管理任务生命周期,保证状态转换合法性 状态变更事件 接收调度器和执行引擎的事件
调度器 选择就绪任务,分配资源,提交执行 调度决策、任务分配 与执行适配器、资源管理器交互
依赖模型 维护 DAG 结构,计算入度,检测环 依赖关系、入度计数 为调度器提供 READY 任务列表
性能瓶颈提示: 当 DAG 规模极大(百万级节点)时,入度计数的更新可能成为瓶颈。建议使用批量更新策略:将多个任务完成事件合并后一次性更新下游入度,减少锁竞争和上下文切换。