核心概念对比: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 的调度器采用事件驱动模型。它不轮询,而是监听事件(比如时间到达、上游完成、外部触发)。一旦事件发生,立即触发下游任务。
我画了一张架构对比图,你看一眼就明白了:
你想想看,如果你的 pipeline 每天只跑一次,Airflow 的轮询模式完全够用。但如果你要做实时数据接入,每秒钟都有新数据进来,轮询模式就扛不住了。
我的经验:在数据量小、调度频率低的场景下,Airflow 的调度器足够稳定。但一旦你的 DAG 数量超过 200 个,或者调度间隔小于 5 分钟,我建议你认真考虑 Dagster 的事件驱动架构。
2.4 总结对比表
最后,我把这一章的核心差异整理成一张表,方便你快速查阅:
| 维度 | Airflow | Dagster |
|---|---|---|
| DAG 定义 | 静态声明式 | 动态运行时构建 |
| 任务单元 | Task(通过 Operator 实例化) | Op(通过 @op 装饰器) |
| 数据传递 | XCom(隐式) | Input/Output(显式) |
| 调度模式 | 中心化轮询 | 事件驱动 |
| 扩展性 | DAG 数量多时性能下降 | 适合大规模、高频调度 |
| 学习曲线 | 低(概念少) | 中(概念更精细) |
嗯,这一章的内容就到这儿。核心概念搞清楚了,后面讲具体怎么选型、怎么迁移,你心里就有底了。