核心概念对比:DAG定义、Task/Op、Operator/Solid、调度器架构差异

好,咱们进入第二章。这一章我打算把 Airflow 和 Dagster 最核心的几个概念掰开揉碎讲清楚。说白了,你学调度框架,本质上就是在学它怎么定义任务、怎么组织任务、以及怎么把任务跑起来。

我当年从 Airflow 迁移到 Dagster 的时候,最头疼的就是概念映射。明明都是做数据编排,但两套术语体系完全不同。今天咱们就把它们一一对应上,顺便聊聊我踩过的坑。

2.1 DAG 定义:静态 vs 动态

先看最基础的概念——DAG。在 Airflow 里,DAG 是一个 Python 文件,你写好之后,调度器定期扫描这个文件,解析出 DAG 结构。这个结构是静态的,一旦生成,运行期间不会变。

核心差异:Airflow 的 DAG 是声明式静态结构,Dagster 的 DAG 是运行时动态构建的。

举个例子,Airflow 里你定义一个 DAG 是这样的:

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

with DAG(
    dag_id='my_pipeline',
    start_date=datetime(2024, 1, 1),
    schedule='@daily',
    catchup=False
) as dag:
    task_a = PythonOperator(
        task_id='extract',
        python_callable=extract_fn
    )
    task_b = PythonOperator(
        task_id='transform',
        python_callable=transform_fn
    )
    task_a >> task_b

你看,DAG 的结构在写代码的时候就定死了。task_a 后面一定是 task_b,不会因为某天的数据不同就改变依赖关系。

但 Dagster 不一样。它的 DAG 是动态的,可以在运行时根据输入数据决定下一步做什么。我有个项目需要根据上游表的分区数量动态生成下游任务,在 Airflow 里折腾了半天,换到 Dagster 一行代码就解决了。

from dagster import job, op, DynamicOut, DynamicOutput

@op
def get_partitions():
    return ['2024-01-01', '2024-01-02', '2024-01-03']

@op(out=DynamicOut())
def process_partition(partition):
    yield DynamicOutput(partition, mapping_key=partition)

@job
def dynamic_pipeline():
    partitions = get_partitions()
    process_partition(partitions).map(lambda p: ...)

我的建议:如果你的 pipeline 结构固定、变化少,Airflow 的静态 DAG 反而更清晰。但如果你需要根据数据动态生成任务,Dagster 的动态特性会让你少写很多代码。

2.2 Task/Op 与 Operator/Solid

接下来看任务单元。Airflow 里叫 Task,Dagster 里叫 Op。它们都是最小的执行单元,但设计哲学完全不同。

维度 Airflow Task Dagster Op
定义方式 通过 Operator 实例化 通过 @op 装饰器
输入输出 隐式传递(XCom) 显式声明(Input/Output)
类型安全 无 有(可指定类型)
复用性 低(Operator 复用) 高(Op 可组合)

Airflow 的 Task 是通过 Operator 创建的。Operator 是执行逻辑的模板,比如 PythonOperator、BashOperator、PostgresOperator。你每次用 Operator 创建一个 Task,本质上是在实例化一个类。

Dagster 的 Op 更轻量。它就是一个函数,加上 @op 装饰器。输入输出通过参数和返回值显式声明,不像 Airflow 那样用 XCom 隐式传递。我刚开始用 Dagster 时觉得这多此一举,直到有一次排查数据问题——Airflow 的 XCom 传值全靠猜,Dagster 的显式声明让我一眼就能看出数据流向。

避坑指南:我曾经在 Airflow 里用 XCom 传递一个 10MB 的 DataFrame,结果调度器直接 OOM 了。XCom 设计初衷是传递小数据(比如文件路径、状态标志),千万别用它传大数据。Dagster 的 Input/Output 默认走内存,但可以配置成文件系统或对象存储,灵活得多。

2.3 调度器架构差异

调度器是这两个框架最本质的区别。Airflow 的调度器是中心化轮询,Dagster 的调度器是事件驱动。

Airflow 的调度器进程会每隔几秒扫描一次 DAG 文件夹,检查哪些 DAG 需要触发。这种方式简单粗暴,但问题也很明显:

  • 性能瓶颈:DAG 数量多了之后,扫描时间线性增长。我维护过 500+ DAG 的 Airflow 集群,调度器 CPU 常年 80%+。
  • 延迟高:从任务就绪到被调度,平均有 5-10 秒的轮询间隔。
  • 单点故障:调度器挂了,整个集群就停了。

Dagster 的调度器采用事件驱动模型。它不轮询,而是监听事件(比如时间到达、上游完成、外部触发)。一旦事件发生,立即触发下游任务。

我画了一张架构对比图,你看一眼就明白了:

调度器架构对比:Airflow vs Dagster Airflow(轮询模式) 调度器(Scheduler) 轮询扫描 DAG 文件夹 解析 DAG 结构 触发任务执行 Dagster(事件驱动) 事件监听器 时间事件 / 上游完成 立即触发下游 执行 Op 轮询模式:延迟高,适合低频调度 事件驱动:低延迟,适合实时场景

你想想看,如果你的 pipeline 每天只跑一次,Airflow 的轮询模式完全够用。但如果你要做实时数据接入,每秒钟都有新数据进来,轮询模式就扛不住了。

我的经验:在数据量小、调度频率低的场景下,Airflow 的调度器足够稳定。但一旦你的 DAG 数量超过 200 个,或者调度间隔小于 5 分钟,我建议你认真考虑 Dagster 的事件驱动架构。

2.4 总结对比表

最后,我把这一章的核心差异整理成一张表,方便你快速查阅:

维度 Airflow Dagster
DAG 定义 静态声明式 动态运行时构建
任务单元 Task(通过 Operator 实例化) Op(通过 @op 装饰器)
数据传递 XCom(隐式) Input/Output(显式)
调度模式 中心化轮询 事件驱动
扩展性 DAG 数量多时性能下降 适合大规模、高频调度
学习曲线 低(概念少) 中(概念更精细)

嗯,这一章的内容就到这儿。核心概念搞清楚了,后面讲具体怎么选型、怎么迁移,你心里就有底了。