3. 为什么需要DAG
说实话,我刚开始做数据处理那几年,根本不知道DAG是什么。那时候团队小,任务少,写几个Shell脚本串起来就跑。后来项目越来越大,脚本越来越多,问题就来了。
我记得有一次,凌晨三点被电话吵醒——线上报表没出来。查了半天,发现是A任务跑完了,但B任务没等到它的结果,直接拿了空数据往下算。那一整夜的报表全废了。嗯,从那以后,我开始认真思考一个问题:任务之间的依赖,到底该怎么管?
任务编排的痛点:你肯定遇到过
先说说没有DAG时,我们是怎么编排任务的。我见过最原始的方式——硬编码顺序。
# 伪代码:硬编码依赖
run_task_A()
if task_A.success:
run_task_B()
run_task_C()
if task_B.success and task_C.success:
run_task_D()
看着简单对吧?但实际项目里,这种写法会带来一堆问题:
- 顺序写死,改不动——想插一个新任务?你得改所有上下游的代码。
- 失败处理靠猜——B失败了,C要不要继续?D要不要重跑?全靠人肉判断。
- 并行?不存在的——明明B和C可以同时跑,但代码里只能串行。
- 监控全靠日志——任务卡住了?去服务器上grep吧。
你想想看,当任务数量从几个变成几十个、几百个,这种硬编码方式根本撑不住。说白了,任务编排的核心痛点就三个:顺序、并行、容错。而这三个问题,恰恰是DAG要解决的。
依赖关系的本质:不只是"谁先谁后"
很多人以为依赖关系就是"A做完才能做B"。其实没那么简单。我个人习惯把依赖关系分成三种:
| 依赖类型 | 含义 | 例子 |
|---|---|---|
| 数据依赖 | 下游任务需要上游任务的输出数据 | ETL做完,报表才能跑 |
| 资源依赖 | 多个任务共享同一份资源(数据库连接、文件锁等) | 两个任务不能同时写同一个表 |
| 逻辑依赖 | 业务规则要求某些任务必须按特定顺序执行 | 先校验数据质量,再入库 |
我在项目中遇到过最头疼的是隐式依赖——代码里没写,但实际存在。比如两个任务都往同一个Hive分区写数据,谁先谁后?没人知道。结果就是数据被覆盖,查都查不出来。
依赖关系的本质是什么?
我个人理解,依赖关系本质上是一种约束——它告诉系统:在某个条件满足之前,你不能做某件事。这个条件可能是"上游任务成功了",也可能是"某个文件到达了",或者是"某个时间点到了"。
而DAG,就是把这些约束画成一张图。图里的每个节点是一个任务,每条边是一个约束。系统只要沿着图走,就能保证所有约束都被满足。
DAG在工程中的价值:不只是画图那么简单
DAG的价值,我总结成四个字:确定性。有了DAG,任务的执行顺序、并行策略、失败处理,都有了明确的规则。不用再靠猜,不用再靠人肉盯。
具体来说,DAG在工程中解决了这几个关键问题:
- 自动推导执行顺序——你只需要声明依赖,系统自己算谁先谁后。
- 并行执行最大化——没有依赖关系的任务,自动并行跑。
- 失败隔离与重试——一个任务挂了,只影响它的下游,不会波及整个管道。
- 可观测性——每个任务的状态、耗时、输入输出,一目了然。
- 增量更新——只重跑失败的任务,不用全量重跑。
一个小技巧:我习惯在DAG里给每个任务加一个"超时时间"。曾经有个任务因为数据量暴增,跑了12个小时还没结束,把整个管道堵死了。加了超时之后,超过阈值自动告警+终止,再也没出过类似问题。
下面这张图,展示了一个典型的DAG任务编排结构。你可以看到,任务之间通过箭头表达依赖,没有依赖的任务可以并行执行。
你看,有了DAG,整个流程变得清晰多了。哪个任务依赖哪个,哪里可以并行,哪里是瓶颈,一眼就能看出来。这就是DAG在工程中的核心价值——把隐式的依赖变成显式的图,把混乱的流程变成可控的管道。
总结一下:
- 没有DAG,任务编排就是一团乱麻——顺序靠猜,并行靠运气,容错靠人肉。
- 依赖关系的本质是约束——它告诉系统"什么时候能做什么事"。
- DAG的价值在于确定性——自动推导顺序、最大化并行、失败隔离、可观测。