2. DAG 基础概念:有向无环图定义、顶点与边、入度与出度、拓扑排序核心思想

在构建分布式任务调度系统之前,必须彻底理解 DAG(有向无环图)这一核心数据结构。DAG 不仅是任务依赖关系的数学表达,更是整个调度引擎的骨架。本节将系统性地拆解 DAG 的四个基本概念:定义、顶点与边、入度与出度、拓扑排序,并揭示它们如何映射到实际的任务调度场景中。

2.1 有向无环图(DAG)定义

DAG 是一种特殊的有向图,它满足两个关键约束:有向 和 无环。

  • 有向(Directed):图中的每条边都具有明确的方向,表示从一个顶点指向另一个顶点。在任务调度中,方向代表依赖关系的流向——前驱任务完成后才能启动后继任务。
  • 无环(Acyclic):图中不存在任何环路(Cycle),即无法从某个顶点出发,沿着有向边回到该顶点。无环性是保证调度可终止、无死锁的前提。
架构视角: 无环约束是 DAG 调度系统的第一原则。一旦依赖图中出现环,调度器将陷入无限等待,任务永远无法完成。因此,所有生产级调度系统在提交 DAG 时都会执行环检测(Cycle Detection)。

2.2 顶点(Vertex)与边(Edge)

DAG 由两个基本元素构成:顶点和边。在任务调度上下文中,它们有明确的语义映射。

图论概念 调度语义 示例
顶点(Vertex) 一个独立的任务单元(Task) 数据清洗任务、模型训练任务、报表生成任务
有向边(Directed Edge) 任务间的依赖关系(Dependency) 任务 A 完成后才能执行任务 B
源顶点(Source) 入度为 0 的任务,无前置依赖 数据采集任务(最先执行)
汇顶点(Sink) 出度为 0 的任务,无后继依赖 最终结果推送任务(最后执行)

边的方向性至关重要:A → B 表示 A 是 B 的前驱(Predecessor),B 是 A 的后继(Successor)。调度器必须保证所有前驱任务成功完成后,才能调度后继任务。

2.3 入度(In-Degree)与出度(Out-Degree)

入度和出度是描述顶点在 DAG 中连接关系的两个关键度量,它们直接决定了任务的调度优先级和并行度。

  • 入度(In-Degree):指向该顶点的边的数量。在调度中,入度表示一个任务有多少个直接前驱依赖。入度为 0 的任务是可立即调度的任务。
  • 出度(Out-Degree):从该顶点出发的边的数量。出度表示一个任务有多少个直接后继任务。出度高的任务往往是数据分发的关键节点。
调度优化提示: 入度是调度器判断任务就绪状态的核心指标。当某个前驱任务完成时,调度器会将其所有后继任务的入度减 1。当入度降为 0 时,该后继任务进入就绪队列,等待调度执行。这种机制称为“基于入度的调度触发”。

2.4 拓扑排序(Topological Sorting)核心思想

拓扑排序是将 DAG 中的所有顶点排列成一个线性序列,使得对于每一条有向边 U → V,顶点 U 在序列中都出现在顶点 V 之前。这个序列定义了任务的合法执行顺序。

拓扑排序的核心算法——Kahn 算法——正是基于入度概念实现的:

  1. 初始化:计算所有顶点的入度,将所有入度为 0 的顶点加入队列。
  2. 迭代处理:从队列中取出一个顶点,将其加入拓扑序列;然后遍历该顶点的所有后继,将每个后继的入度减 1;如果某个后继的入度变为 0,则将其加入队列。
  3. 终止条件:当队列为空时,如果拓扑序列中的顶点数等于 DAG 的总顶点数,则排序成功;否则说明图中存在环。
// Kahn 算法伪代码
function topologicalSort(graph):
    inDegree = computeInDegree(graph)   // 计算所有顶点入度
    queue = new Queue()
    for each vertex v in graph:
        if inDegree[v] == 0:
            queue.enqueue(v)            // 入度为 0 的顶点入队

    result = []
    while queue is not empty:
        u = queue.dequeue()
        result.append(u)
        for each successor v of u:
            inDegree[v]--
            if inDegree[v] == 0:
                queue.enqueue(v)

    if result.length != graph.vertexCount:
        throw "Cycle detected: DAG contains a cycle"
    return result
架构陷阱: 拓扑排序只保证依赖顺序合法,不保证唯一性。当多个入度为 0 的任务同时存在时,调度器可以自由选择执行顺序,这为并行调度和优先级调度留下了优化空间。但必须注意:拓扑排序结果并不等同于实际执行时间线,它只是“可行性约束”。

2.5 DAG 结构与调度映射关系

下面的 SVG 图展示了 DAG 的核心概念及其在任务调度中的映射关系,包括顶点、边、入度、出度以及拓扑排序的生成过程。

DAG 核心概念与调度映射 DAG 图结构 A (入度=0) B (入度=1) C (入度=1) D (出度=0) 拓扑排序序列 步骤 1: 选择入度为 0 的 A 步骤 2: A 完成后,B、C 入度减 1 步骤 3: B、C 入度变为 0,加入队列 步骤 4: 选择 B 或 C,执行后 D 入度减 1 步骤 5: D 入度变为 0,执行 D A → B → C → D (或 A → C → B → D,均合法)

2.6 入度驱动的调度执行流程

下图展示了调度器如何利用入度机制,将 DAG 拓扑排序转化为实际的任务执行流程。这是分布式调度引擎的核心运行时逻辑。

入度驱动的调度执行流程 1. 初始化 计算所有顶点入度 2. 就绪队列 入度=0 的顶点入队 3. 调度执行 从队列取出任务执行 4. 依赖更新 后继入度减 1 循环判断:入度变为 0 的后继加入就绪队列 重复步骤 2 → 3 → 4,直到所有任务执行完毕 若队列为空但仍有未执行任务 → 检测到环,报错 反馈循环

2.7 关键概念总结

概念 数学定义 调度含义 架构影响
有向无环图 有向边 + 无环路 任务依赖关系可终止 环检测是调度器第一道防线
顶点 图的节点 单个任务单元 顶点粒度决定调度并行度
有向边 节点间的有向连接 任务依赖约束 边数量影响调度开销
入度 指向顶点的边数 前置依赖数量 入度=0 是调度触发条件
出度 从顶点出发的边数 后继任务数量 出度高需关注数据扇出
拓扑排序 DAG 的线性化序列 合法执行顺序 不唯一性提供调度优化空间
架构师思考: 理解 DAG 的入度驱动机制是设计高性能调度器的起点。在实际系统中,入度通常以引用计数(Reference Count)的形式存储在内存中,每次任务完成时原子递减。这种设计使得调度器可以在 O(1) 时间内判断任务是否就绪,是实现高吞吐调度的关键。