2. 核心概念:DAG(有向无环图)定义、任务节点与依赖边、拓扑排序原理

在分布式任务调度领域,DAG(Directed Acyclic Graph,有向无环图)是描述任务依赖关系最严谨的数学模型。理解 DAG 的三个核心要素——节点、边与无环约束——是设计调度服务边界的前提。

2.1 DAG 的数学定义与工程映射

从图论角度,DAG 是一个由顶点集合 V 和有向边集合 E 构成的二元组 G=(V, E),且不存在从某个顶点出发经过若干条有向边回到该顶点的路径。在任务调度场景中,这种抽象被直接映射为:

图论元素 调度领域映射 工程示例
顶点 (Vertex) 任务节点 (Task Node) 数据清洗、特征工程、模型训练
有向边 (Directed Edge) 依赖关系 (Dependency) “模型训练”依赖“特征工程”完成
无环约束 无循环依赖校验 禁止 A→B→C→A 的环形引用
入度 (In-degree) 前置依赖数量 入度为 0 表示可立即调度
出度 (Out-degree) 下游依赖数量 出度为 0 表示终端节点
架构要点: DAG 的无环特性是调度系统正确性的生命线。一旦出现环,拓扑排序将无法完成,调度器会陷入死锁。因此,边界服务必须在提交阶段进行环检测,而非等到运行时。

2.2 任务节点与依赖边的设计粒度

任务节点(Task Node)是 DAG 中最小的可调度单元。在领域驱动设计中,每个节点对应一个领域事件或命令的触发点。节点的属性设计直接影响调度服务的边界划分:

  • 节点标识:全局唯一 ID,用于追踪和幂等。
  • 执行类型:Shell 脚本、HTTP 调用、Spark 作业、子 DAG 等。
  • 超时策略:硬超时与软超时,决定调度器的中断行为。
  • 重试配置:最大重试次数、退避算法,属于调度服务的核心策略。
  • 上下文数据:输入输出参数的 Schema,用于数据血缘追踪。

依赖边(Dependency Edge)则定义了节点间的时序与数据约束。常见的依赖类型包括:

依赖类型 语义 调度行为
硬依赖 (Hard) 上游必须成功 上游失败则下游跳过或标记失败
软依赖 (Soft) 上游完成即可(含失败) 上游失败时下游仍可执行
数据依赖 上游输出作为下游输入 需传递数据路径或内存引用
信号依赖 仅等待上游完成信号 不关心上游执行结果数据
设计建议: 在限界上下文中,依赖边的类型应作为值对象(Value Object)封装在调度领域内部,而非暴露给外部调用方。这可以防止客户端错误地设置循环依赖或矛盾约束。

2.3 拓扑排序原理与调度顺序生成

拓扑排序(Topological Sorting)是 DAG 调度引擎的核心算法。它生成一个线性序列,使得对于每一条有向边 u→v,节点 u 在序列中都出现在节点 v 之前。调度器依据该序列决定任务的触发顺序。

最常用的算法是 Kahn 算法(基于入度),其步骤清晰且易于在分布式环境中实现:

  1. 初始化一个队列,将所有入度为 0 的节点入队。
  2. 从队列中取出一个节点,将其加入拓扑序列。
  3. 遍历该节点的所有出边,将目标节点的入度减 1。
  4. 若目标节点入度变为 0,则将其入队。
  5. 重复步骤 2-4,直到队列为空。
  6. 若最终序列长度小于节点总数,则说明图中存在环。
// Kahn 算法伪代码(调度服务核心逻辑)
function topologicalSort(graph):
    inDegree = computeInDegree(graph)
    queue = new Queue()
    for node in graph.nodes:
        if inDegree[node] == 0:
            queue.enqueue(node)

    sortedList = []
    while queue is not empty:
        node = queue.dequeue()
        sortedList.append(node)
        for neighbor in graph.getNeighbors(node):
            inDegree[neighbor]--
            if inDegree[neighbor] == 0:
                queue.enqueue(neighbor)

    if sortedList.length != graph.nodes.length:
        throw new CyclicDependencyException("DAG contains a cycle")
    return sortedList
架构陷阱: 在微服务架构中,拓扑排序不应在每次调度时都从零计算。建议将排序结果缓存为 DAG 的快照(Snapshot),仅在 DAG 结构变更时重新计算。这能显著降低调度延迟。

2.4 DAG 结构可视化:节点、边与拓扑顺序

下图展示了一个典型的 ETL 任务 DAG,包含 6 个任务节点和 7 条依赖边。拓扑排序结果为 [A, B, C, D, E, F],调度器将按此顺序触发任务,同时利用并行度优化:

ETL 任务 DAG 示例:节点、依赖边与拓扑排序 ETL 任务 DAG:节点、依赖边与拓扑排序 A: 数据采集 B: 数据清洗 C: 格式转换 D: 质量校验 E: 数据聚合 F: 数据入库 拓扑排序结果:A → B → C → D → E → F (B 与 C 可并行调度)

2.5 并行度与关键路径

拓扑排序仅保证依赖顺序,不直接决定并行度。调度器需要进一步分析 DAG 的关键路径(Critical Path)——从起点到终点的最长路径,它决定了整个 DAG 的最短完成时间。在架构设计中:

  • 关键路径上的节点:应分配最高优先级资源,避免成为瓶颈。
  • 非关键路径节点:可在资源空闲时延迟执行,或使用较低规格的 Worker。
  • 并行度上限:由 DAG 的宽度(同一层级的节点数)决定,调度器需维护一个线程安全的就绪队列。
领域驱动视角: 关键路径计算属于调度引擎的领域服务(Domain Service),不应放在应用层或基础设施层。它依赖 DAG 的完整快照,输出一个有序的节点优先级列表,供资源分配模块消费。

下图展示了同一 DAG 的并行调度视图,其中 B 和 C 属于同一层级,可同时执行:

DAG 并行调度层级视图 DAG 并行调度层级视图(按拓扑层级分组) 层级 0: A 层级 1: B 层级 1: C 层级 2: D 层级 2: E 层级 3: F

2.6 本章小结