1、DAG基础概念:什么是DAG、有向无环图的数学定义、DAG在任务调度中的应用场景
1.1 从一个实际问题说起
我记得刚带团队那会儿,接手过一个数据管道项目。每天要跑几十个任务,有的任务依赖上游数据,有的任务要等前一个跑完才能启动。最开始我们用脚本硬写依赖关系,结果呢?三天两头出问题——上游还没跑完,下游就启动了,数据全乱套。
后来我意识到,这其实是个图论问题。每个任务是一个节点,依赖关系是一条有向边。只要保证整个图里没有循环依赖,就能用拓扑排序确定执行顺序。嗯,这就是DAG——有向无环图。
1.2 什么是DAG?
DAG,全称 Directed Acyclic Graph,中文叫「有向无环图」。说白了,就是一张图,里面的边都有方向,而且你顺着边走,永远走不回起点。
拆开来看:
- 有向(Directed):每条边都有方向,从 A 指向 B,表示 A 到 B 有依赖关系,但反过来不一定成立。
- 无环(Acyclic):图中不存在环路。你从任意节点出发,沿着边的方向走,最终一定会停下来,不会陷入死循环。
你想想看,如果任务调度里出现了环——A 依赖 B,B 依赖 C,C 又依赖 A——那这三个任务永远没法开始执行。这就是为什么DAG在任务调度领域这么重要。
1.3 数学定义
从数学角度看,DAG的定义其实很简洁:
一个有向图 G = (V, E),其中:
- V 是顶点集合(节点)
- E 是有向边集合(有序对 (u, v),u, v ∈ V)
- 不存在顶点序列 v₀, v₁, ..., vₖ,使得 v₀ = vₖ 且 (vᵢ, vᵢ₊₁) ∈ E
最后一条就是「无环」的数学表达。我习惯把这个定义记成:从任意节点出发,深度优先遍历不会遇到已访问的祖先节点。
关键性质:DAG 一定存在拓扑排序。也就是说,总能把所有节点排成一个线性序列,使得每条边的起点都在终点之前。这个性质是任务调度的理论基础。
1.4 DAG在任务调度中的应用场景
我在实际项目中,DAG 最常见的应用场景有这么几类:
| 场景 | 节点含义 | 边含义 | 典型系统 |
|---|---|---|---|
| 数据管道 | 数据处理步骤 | 数据依赖 | Airflow、Luigi |
| CI/CD 流水线 | 构建/测试/部署阶段 | 阶段先后顺序 | Jenkins Pipeline、GitLab CI |
| 分布式计算 | 计算任务 | 数据shuffle依赖 | Spark DAG Scheduler |
| 工作流引擎 | 审批/处理节点 | 流程流转 | Activiti、Camunda |
| 编译器优化 | 指令/基本块 | 数据流依赖 | LLVM、GCC |
拿数据管道来说,我曾经维护过一个每天处理 10TB 日志的系统。每个 ETL 任务就是一个 DAG 节点,任务之间的上下游关系就是有向边。只要 DAG 定义正确,调度器就能自动算出哪些任务可以并行跑,哪些必须串行等。
我的经验:刚开始用 DAG 做任务调度时,最容易踩的坑是「隐式依赖」。比如两个任务都读写同一个数据库表,但你没在 DAG 里显式声明依赖关系。结果并发执行时出现脏数据。记住:所有共享资源的访问,都要在 DAG 里体现为显式边。
1.5 一张图看懂DAG核心概念
下面这张 SVG 图,把 DAG 的核心要素都串起来了。我建议你仔细看看节点之间的箭头方向,以及为什么这个图没有环。
1.6 为什么DAG在分布式系统中这么火?
说白了,分布式系统最怕两件事:死锁和无限循环。DAG 天然无环的特性,正好解决了这两个问题。
我参与过一个 Spark 作业调优项目。Spark 内部就是用 DAG 来表示计算任务的依赖关系。每个 RDD 的 transformation 操作都会生成一个新的 DAG 节点,shuffle 操作会产生宽依赖边。调度器拿到这个 DAG 后,就能自动划分 stage,决定哪些 task 可以并行,哪些需要 shuffle 等待。
注意:DAG 虽然解决了循环依赖问题,但并不能解决所有调度难题。比如:
- DAG 不处理资源竞争(两个任务抢同一台机器的 CPU)
- DAG 不处理任务优先级(同等依赖关系的任务谁先跑?)
- DAG 不处理容错(某个节点挂了,下游怎么办?)
这些问题需要调度器在 DAG 之上额外处理。后面的章节我会详细讲。
1.7 小结
这一章我们聊了 DAG 的基本概念:
- DAG = 有向 + 无环,边有方向,图无环路
- 数学定义:不存在顶点序列能回到自身
- 应用场景:数据管道、CI/CD、分布式计算、工作流引擎、编译器优化