3. 数据依赖核心概念

在 Airflow 中,DAG 并不是“一串按顺序执行的任务”这么简单。真正决定一个任务何时能跑、跑得对不对、补数时会不会踩坑的,是它背后的一整套依赖体系。很多初学者把依赖等同于 task_a >> task_b,结果在回填历史数据时发现任务提前执行、读到空分区、或者上游还没产出下游就跑了。要避免这些问题,必须把依赖拆成四个维度来看:任务依赖、数据依赖、时间依赖、触发规则。

一、核心原理:四类依赖各管什么

任务依赖描述的是任务之间的执行顺序,由 DAG 中的边(edge)表达,例如 extract >> transform >> load。它只保证“谁先谁后”,不保证“数据是否真的就绪”。数据依赖描述的是任务对具体数据分区、文件、表或外部数据集的依赖,例如“必须等 ods_orders 的 dt=2024-05-01 分区产出后才能跑”。时间依赖由 schedule、start_date、data_interval 共同决定,它回答的是“这个 DAG 在哪个逻辑时间点被触发”。触发规则则由 trigger_rule 控制,决定上游任务处于成功、失败、跳过等状态时,当前任务是否允许运行。

这四者不是并列孤立的,而是层层叠加:时间依赖决定 DAG Run 何时产生,任务依赖决定 Run 内部的任务顺序,数据依赖决定任务是否真的具备可处理的数据,触发规则决定异常状态下任务是否放行。理解这个叠加关系,是后面学习补数回填的前提。

Airflow 依赖体系全景 时间依赖 schedule / start_date data_interval 任务依赖 extract >> transform DAG 边 / 上下游 数据依赖 分区 / 表 / 文件 Dataset / Asset 触发规则 trigger_rule all_success 等 DAG Run 调度决策 时间到 → 任务顺序满足 → 数据就绪 → 触发规则放行 → 任务执行 补数回填时的关键约束 逻辑时间 ≠ 物理时间;数据分区必须与 data_interval 对齐;触发规则影响失败重跑

二、方法流程:从 DAG 定义到依赖落地

在编写 DAG 时,建议按以下顺序梳理依赖,而不是先写任务再补依赖:

  1. 确定时间粒度:明确 DAG 是小时级、天级还是月级,设置 start_date 与 schedule,并理解 data_interval_start 与 data_interval_end 的含义。
  2. 画出任务依赖:用 >> 或 set_upstream/set_downstream 建立任务顺序,避免出现环形依赖。
  3. 标注数据依赖:明确每个任务读取哪个分区、写入哪个分区;跨 DAG 依赖可用 Dataset(Airflow 2.4+)或 ExternalTaskSensor 表达。
  4. 选择触发规则:默认 all_success,但清洗、告警、汇总等场景常需要 all_done、one_success 或 none_failed_min_one_success。
  5. 验证回填行为:用 airflow dags backfill 或 UI 的 Backfill 功能,观察逻辑日期与数据分区是否一致。

下面是一个同时体现时间依赖、任务依赖与数据依赖的 DAG 片段:

from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta

default_args = {
    "start_date": datetime(2024, 5, 1),
    "retries": 2,
    "retry_delay": timedelta(minutes=5),
}

with DAG(
    dag_id="orders_daily",
    schedule="0 2 * * *",          # 时间依赖:每天 02:00 触发
    default_args=default_args,
    catchup=True,
) as dag:

    def extract(ds, **ctx):
        # 数据依赖:读取 ds 对应分区
        print(f"extract partition dt={ds}")

    def transform(ds, **ctx):
        print(f"transform partition dt={ds}")

    def load(ds, **ctx):
        print(f"load partition dt={ds}")

    t1 = PythonOperator(task_id="extract", python_callable=extract)
    t2 = PythonOperator(task_id="transform", python_callable=transform)
    t3 = PythonOperator(task_id="load", python_callable=load)

    t1 >> t2 >> t3                # 任务依赖

三、案例场景:触发规则如何影响补数

假设某天 transform 失败,但 extract 成功。默认 all_success 下,load 不会执行,这是符合预期的。但如果业务要求“无论上游是否失败,都要执行告警任务”,就需要把告警任务的 trigger_rule 设为 all_done。再比如,多个上游分支中只要有一个成功就继续汇总,应使用 one_success。补数时如果错误地使用了 all_done,可能导致下游在数据不完整的情况下继续写入,造成脏数据。

触发规则 含义 典型场景
all_success 所有上游成功才执行 默认,严格数据加工链
all_failed 所有上游失败才执行 统一失败兜底处理
all_done 所有上游结束即执行 告警、清理、审计
one_success 任一上游成功即执行 多源汇总、择优取数
none_failed_min_one_success 无失败且至少一个成功 允许跳过但不允许失败

四、误区与复盘

误区一:把任务依赖当成数据依赖。 t1 >> t2 只说明 t1 先跑,不代表 t1 产出的分区就是 t2 需要的分区。补数时若逻辑日期错位,t2 可能读到空数据。

误区二:忽略 data_interval。 Airflow 2.x 中 ds 表示数据区间起点,不是“今天”。回填 5 月 1 日的数据,实际处理的是 2024-05-01 这个逻辑分区,而不是执行当天的日期。

误区三:滥用 all_done。 它会让下游在上游失败时也执行,若下游没有幂等保护,容易产生重复或脏数据。

复盘建议:每次设计 DAG 时,用一句话描述“这个任务在什么时间、依赖哪些数据、上游什么状态下才允许运行”。如果这句话说不清楚,说明依赖设计还不完整。补数前先在小范围日期上验证,再全量回填。

掌握这四类依赖之后,你就能准确判断一个任务“为什么没跑”“为什么提前跑”“为什么补数补错了”。下一阶段我们会把这些概念直接应用到补数回填的实战操作中。