[数据编排/任务调度] Apache AirFlow 使用指南
0 序
1 概述: Apache Airflow
核心组件(任何部署模式都有)
- Airflow 的部署方式,本质上由两个东西决定:跑在哪(单机/分布式) 和 用什么 Executor(谁来执行任务)。所以先拆解架构组件,再按 Executor 分几种典型部署模式。
┌─────────────┐
用户/调度 │ Scheduler │ ← 调度心脏:扫描 DAG、决定任务何时跑
└──────┬──────┘
│ 调度任务
┌──────▼──────┐
│ 元数据库 DB │ ← 存 DAG 状态/运行记录(SQLite/Postgres/MySQL)
└──────┬──────┘
┌──────────────────┼──────────────────┐
┌────▼────┐ ┌──────▼──────┐ ┌─────▼─────┐
│Webserver│ │ Executor │ │ Triggerer │
└─────────┘ │ (执行策略) │ └───────────┘
└──────┬──────┘ (可选,支持懒加载任务)
▼
┌─────────────┐
│ Worker │ ← 真正跑任务的地方(1 个或多个)
└─────────────┘
- Scheduler:心脏,解析 DAG、决定任务触发时机,交给 Executor。
- Webserver:Web UI,看 DAG、手动触发、看日志。
- 元数据库:存所有状态(DAG 运行、TaskInstance、Connections、变量)。
- Executor:决定任务「在哪个进程/节点执行」的策略。
- Worker:Executor 派发的实际执行单元。
按 Executor / 部署模式划分(5 种主流)
| 部署模式 | Executor | 架构 | 适用场景 | 元数据库 |
|---|---|---|---|---|
| 1. Standalone(单进程全内置) | Sequential | 一个进程包含 scheduler+webserver+worker,开箱即用 | 学习、Demo、本地开发(就是你目前在用的) | SQLite |
| 2. 单机多进程(Local) | LocalExecutor | 一台机器,scheduler/webserver/worker 是独立进程,任务并行跑 | 小规模生产、单机批处理 | Postgres/MySQL |
| 3. 分布式(Celery) | CeleryExecutor | 多台 worker 机器 + 消息队列(Redis/RabbitMQ)分发任务 | 中大规模生产、需要横向扩展 | Postgres/MySQL |
| 4. 容器化(Kubernetes) | KubernetesExecutor | 每个任务动态起一个 Pod,弹性伸缩 | 云原生、任务量大且不固定 | Postgres/MySQL |
| 5. 混合/托管 | CeleryKubernetes / 云托管(MWAA、Cloud Composer) | 集群策略混用,或完全托管不用运维 | 大型/企业、不想自运维 | 托管 |
另有一个 SequentialExecutor(Airflow 默认,串行,只适合 Demo);DaskExecutor 已基本废弃不再建议。
关键区别
- Standalone:全部塞进一个进程,
airflow standalone一条命令,零配置,只适合学。 - Local:一台机器的多进程,任务真能并行了,小型生产够用。
- Celery:任务丢到 Redis/RabbitMQ 队列,多台机器的 worker 抢着执行 → 可水平扩展。
- K8s:每个任务一个 Pod,按任务弹性伸缩、隔离最好,但部署复杂度最高。
升级路径建议
Standalone(学习/本地测验) ──▶ Local(单机小生产) ──▶ Celery(多机横向扩展) ──▶ K8s(云原生弹性)
最简单 最灵活/最复杂
假设你现在是 Standalone。
下一步如果要上「真正的生产」,建议至少切到 Local 或 Celery,并做两件事:把【元数据库】从 SQLite 换成 PostgreSQL(SQLite 不支持并发,多 worker 会出问题),以及把 DAG 从手动触发改为 schedule 定时。
2 安装部署篇
Docker & Standalone 版
Docker 中的 Airflow Standalone 是一种单容器、一键式启动的本地开发/测试环境,自动完成数据库初始化、用户创建并启动 Webserver、Scheduler、Triggerer 等核心服务
适合快速搭建和调试,但不适合生产环境
docker pull docker.mirrors.ustc.edu.cn/apache/airflow:2.10.5
//方式1
docker run -d -p 8080:8080 --name airflow-standalone -e AIRFLOW__CORE__LOAD_EXAMPLES=true -v ${PWD}/dags:/usr/local/airflow/dags --name airflow apache/airflow:2.10.5 standalone
//方式2
# 建 dags 目录 (powershell 窗口下)
cd E:\Program-Data\Docker-Data\docker-external-storage\airflow-standalone
New-Item -ItemType Directory -Force dags
docker run -d --name airflow-standalone -p 8080:8080 -v "${PWD}\dags:/opt/airflow/dags" -e AIRFLOW__CORE__LOAD_EXAMPLES=False apache/airflow:2.10.5 standalone
- 参数说明:(方式2)
-p 8080:8080:Web UI 端口。-v ...dags:/opt/airflow/dags:把本地 DAG 目录挂进去。AIRFLOW__CORE__LOAD_EXAMPLES=False:不加载官方示例,界面干净。- 等待约 30~60 秒初始化完成。
即 方式2的Docker容器外的外置目录:
DAG 文件所在目录挂载进容器,这样改代码不用重新构建镜像。
C:\airflow-demo\
└── dags\
├── etl_demo.py # 你的 DAG 文件
└── etl.db # SQLite 数据库(运行时生成)
如果容器日志里提示权限 / 挂载问题,用
docker logs airflow-standalone看报错再对症处理。
- 部署完成后
- 查看
admin用户密码的方式:
- 方式1 : container log 中查阅 / 方式2:
cat /opt/airflow/standalone_admin_password.txt

- 访问 webui: http://127.0.0.1:8080
3 主流程体验篇
- 如无特殊说明,本章默认采用方式1做的安装部署。
登录

DAGs
列表
- 部署方式1的效果:

- 部署方式2的效果:

详情
Tab:Graph

Tab:Detail

Tab:Code

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Example DAG for demonstrating the behavior of the Datasets feature in Airflow, including conditional and
dataset expression-based scheduling.
Notes on usage:
Turn on all the DAGs.
dataset_produces_1 is scheduled to run daily. Once it completes, it triggers several DAGs due to its dataset
being updated. dataset_consumes_1 is triggered immediately, as it depends solely on the dataset produced by
dataset_produces_1. consume_1_or_2_with_dataset_expressions will also be triggered, as its condition of
either dataset_produces_1 or dataset_produces_2 being updated is satisfied with dataset_produces_1.
dataset_consumes_1_and_2 will not be triggered after dataset_produces_1 runs because it requires the dataset
from dataset_produces_2, which has no schedule and must be manually triggered.
After manually triggering dataset_produces_2, several DAGs will be affected. dataset_consumes_1_and_2 should
run because both its dataset dependencies are now met. consume_1_and_2_with_dataset_expressions will be
triggered, as it requires both dataset_produces_1 and dataset_produces_2 datasets to be updated.
consume_1_or_2_with_dataset_expressions will be triggered again, since it's conditionally set to run when
either dataset is updated.
consume_1_or_both_2_and_3_with_dataset_expressions demonstrates complex dataset dependency logic.
This DAG triggers if dataset_produces_1 is updated or if both dataset_produces_2 and dag3_dataset
are updated. This example highlights the capability to combine updates from multiple datasets with logical
expressions for advanced scheduling.
conditional_dataset_and_time_based_timetable illustrates the integration of time-based scheduling with
dataset dependencies. This DAG is configured to execute either when both dataset_produces_1 and
dataset_produces_2 datasets have been updated or according to a specific cron schedule, showcasing
Airflow's versatility in handling mixed triggers for dataset and time-based scheduling.
The DAGs dataset_consumes_1_never_scheduled and dataset_consumes_unknown_never_scheduled will not run
automatically as they depend on datasets that do not get updated or are not produced by any scheduled tasks.
"""
from __future__ import annotations
import pendulum
from airflow.datasets import Dataset
from airflow.models.dag import DAG
from airflow.operators.bash import BashOperator
from airflow.timetables.datasets import DatasetOrTimeSchedule
from airflow.timetables.trigger import CronTriggerTimetable
# [START dataset_def]
dag1_dataset = Dataset("s3://dag1/output_1.txt", extra={"hi": "bye"})
# [END dataset_def]
dag2_dataset = Dataset("s3://dag2/output_1.txt", extra={"hi": "bye"})
dag3_dataset = Dataset("s3://dag3/output_3.txt", extra={"hi": "bye"})
#with DAG(
# dag_id="dataset_produces_1",
# catchup=False,
# start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
# schedule="@daily",
# tags=["produces", "dataset-scheduled"],
#) as dag1:
# # [START task_outlet]
# BashOperator(outlets=[dag1_dataset], task_id="producing_task_1", bash_command="sleep 5")
# # [END task_outlet]
# ---------------
from __future__ import annotations
from datetime import datetime
from airflow import DAG
from airflow.operators.bash import BashOperator
import pendulum
# 定义"数据集"(Data-aware Scheduling 的数据感知调度特性,Airflow 2.4+)
# 它代表一条"数据产物",可以是一个文件、一张表、一个对象存储路径等。
# 这里说明:本 DAG 运行完成后会"产出"这个数据集,供其他 DAG 消费。
dag1_dataset = Dataset("file:///opt/airflow/dags/orders_ready.csv")
# ---------------------------------------------------------------
# with ... as dag1: 用上下文管理器创建 DAG
# 所有在这个 with 块内创建的任务(Operator)会自动挂到这个 DAG 下,
# 无需手动 task.dag = dag 去绑定。
# 这个 DAG 的意义:它本身只 `sleep 5`,核心价值在 `outlets=[dag1_dataset]` —— 声明 "我产出数据集 X"。你需要再写一个【消费侧 DAG】 用 `inlets=[dag1_dataset]` 或 `schedule=[dag1_dataset]`,才能体现 "数据驱动调度"(上游产出数据 → 自动触发下游)
# ---------------------------------------------------------------
with DAG(
# dag_id := DAG 唯一标识 : DAG 在 Airflow 里的唯一标识(全局唯一)。 | 全局唯一,UI/CLI 都靠它引用,跨文件不能重名
# 它会显示在 UI 列表、CLI 命令里,跨文件也不能重名。
dag_id="dataset_produces_1",
# catchup=False: 关闭"回填追赶"。 | catchup = 是否回填追赶 : `False` 不补历史;`True` 会为错过的周期批量补跑
# 含义: 当 start_date 早于当前时间时,不自动补跑那些"错过的"调度周期。
# 若为 True,Airflow 会为 start_date 到当前之间的每个调度时间点都生成一次运行,
# 可能瞬间创建大量历史 DAG Run。Demo/常规场景建议 False。
catchup=False,
# start_date := 逻辑起始时间 : DAG 的"逻辑起始时间",调度起点。 | 调度起点,需带时区;不是首次运行时间
# pendulum.datetime(2021,1,1, tz="UTC") 生成带时区的 UTC 时间。
# 注意: 它不是"首次运行时间",而是调度计算的起点;
# 结合 schedule 才会产生运行计划。时区务必明确(这里用 UTC)。
# `start_date` vs 实际首次运行:`start_date` 不是 "几点几分开始跑",而是调度计算的起点。`@daily + start_date=2021-01-01`,在 2021-01-01 之后的每个自然日零点生成一个 scheduled DAG Run。配合 `catchup=False` 不会补历史,首次运行从当前时刻之后的下一个调度点开始。
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
# schedule="@daily": 调度频率,控制 DAG 何时被触发。 | `@daily` 等价 cron `0 0 * * *`;`None`= 仅手动
# "@daily" = 每天 0 点跑一次。等价写法:
# schedule_interval="@daily" (旧版参数名)
# schedule="0 0 * * *" (cron 表达式)
# 常见预设: @hourly / @daily / @weekly / @monthly / None(仅手动触发)
schedule="@daily",
# tags: DAG 标签,用于 UI 分类/过滤/搜索,纯辅助性质,不影响执行。 | UI 分类 / 搜索用,不影响执行
tags=["produces", "dataset-scheduled"],
) as dag1:
# [START task_outlet] ← 这只是文档化标记注释,无实际作用,可删除
# BashOperator: 执行一条 shell 命令的任务算子。
# outlets=[dag1_dataset]: 声明"本任务运行成功后,产出 dag1_dataset 这个数据集"。 | Data-aware 特性:任务成功→标记该数据集已更新→触发下游消费 DAG
# → 这是 Data-aware Scheduling 的"产出侧(outlet)"声明,
# 运行成功后 Airflow 会标记该 Dataset 为"最新已更新",
# 从而触发所有以它为 inlet(消费侧)的下游 DAG。
# task_id: 任务在 DAG 内的唯一标识(同一 DAG 内不能重名)。 | DAG 内唯一即可
# bash_command: 要执行的 shell 命令字符串,这里 sleep 5 模拟耗时 5 秒的 ETL。
BashOperator(
outlets=[dag1_dataset], # 产出数据集声明 : Data-aware 特性:任务成功→标记该数据集已更新→触发下游消费 DAG
task_id="producing_task_1", # 任务唯一标识 : DAG 内唯一
bash_command="sleep 5", # 执行命令: shell 命令字符串
)
# [END task_outlet]
# ---------------
with DAG(
dag_id="dataset_produces_2",
catchup=False,
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=None,
tags=["produces", "dataset-scheduled"],
) as dag2:
BashOperator(outlets=[dag2_dataset], task_id="producing_task_2", bash_command="sleep 5")
# 消费侧 DAG(示意):数据一旦更新就自动触发 | 下游是靠「上游任务执行成功」这个信号来感知数据集更新的,但有一个关键区别 ——它检测的是 "任务成功" 这个逻辑事件,并不真正去检查文件 / 表里数据有没有变
# [START dag_dep]
with DAG(
dag_id="dataset_consumes_1",
catchup=False,
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=[dag1_dataset], # ← 依赖该数据集,更新即触发
tags=["consumes", "dataset-scheduled"],
) as dag3:
# [END dag_dep]
BashOperator(
outlets=[Dataset("s3://consuming_1_task/dataset_other.txt")],
task_id="consuming_1",
bash_command="sleep 5",
)
with DAG(
dag_id="dataset_consumes_1_and_2",
catchup=False,
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=[dag1_dataset, dag2_dataset],
tags=["consumes", "dataset-scheduled"],
) as dag4:
BashOperator(
outlets=[Dataset("s3://consuming_2_task/dataset_other_unknown.txt")],
task_id="consuming_2",
bash_command="sleep 5",
)
with DAG(
dag_id="dataset_consumes_1_never_scheduled",
catchup=False,
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=[
dag1_dataset,
Dataset("s3://unrelated/this-dataset-doesnt-get-triggered"),
],
tags=["consumes", "dataset-scheduled"],
) as dag5:
BashOperator(
outlets=[Dataset("s3://consuming_2_task/dataset_other_unknown.txt")],
task_id="consuming_3",
bash_command="sleep 5",
)
with DAG(
dag_id="dataset_consumes_unknown_never_scheduled",
catchup=False,
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=[
Dataset("s3://unrelated/dataset3.txt"),
Dataset("s3://unrelated/dataset_other_unknown.txt"),
],
tags=["dataset-scheduled"],
) as dag6:
BashOperator(
task_id="unrelated_task",
outlets=[Dataset("s3://unrelated_task/dataset_other_unknown.txt")],
bash_command="sleep 5",
)
with DAG(
dag_id="consume_1_and_2_with_dataset_expressions",
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=(dag1_dataset & dag2_dataset),
) as dag5:
BashOperator(
outlets=[Dataset("s3://consuming_2_task/dataset_other_unknown.txt")],
task_id="consume_1_and_2_with_dataset_expressions",
bash_command="sleep 5",
)
with DAG(
dag_id="consume_1_or_2_with_dataset_expressions",
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=(dag1_dataset | dag2_dataset),
) as dag6:
BashOperator(
outlets=[Dataset("s3://consuming_2_task/dataset_other_unknown.txt")],
task_id="consume_1_or_2_with_dataset_expressions",
bash_command="sleep 5",
)
with DAG(
dag_id="consume_1_or_both_2_and_3_with_dataset_expressions",
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=(dag1_dataset | (dag2_dataset & dag3_dataset)),
) as dag7:
BashOperator(
outlets=[Dataset("s3://consuming_2_task/dataset_other_unknown.txt")],
task_id="consume_1_or_both_2_and_3_with_dataset_expressions",
bash_command="sleep 5",
)
with DAG(
dag_id="conditional_dataset_and_time_based_timetable",
catchup=False,
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
schedule=DatasetOrTimeSchedule(
timetable=CronTriggerTimetable("0 1 * * 3", timezone="UTC"), datasets=(dag1_dataset & dag2_dataset)
),
tags=["dataset-time-based-timetable"],
) as dag8:
BashOperator(
outlets=[Dataset("s3://dataset_time_based/dataset_other_unknown.txt")],
task_id="conditional_dataset_and_time_based_timetable",
bash_command="sleep 5",
)
Cluster Activity

我触发/运行一个任务后:
Datasets
Dependency Events

Dependency Graph
-
默认: Datasets(所有Dataset) 的 依赖图

-
选择其中1个Dataset后,则进入了 某一 Dataset 的详情页:
- Dependency Graph
- Detail
Datasets/数据集列表

Security/安全
用户列表

用户角色


用户统计

Actions/操作列表

Resources/资源列表

Permissions/权限列表 = { 权限 = Action : 资源 } 的映射集合、支持搜索


Browse/浏览
DAG Runs/DAG运行情况


- 选择该任务的运行记录的 DAG Id 列进入:
- Detail

- Graph
- Code
- EventLg
- RunDuration
- Calendar
Swagger/API 文档


4 功能介绍篇
Z FAQ for Apache Airflow
Q: Airflow 除了支持 python etl 脚本外,还支持java脚本吗?
- 能,但要说清楚界限:Airflow 的 DAG 本身只能用 Python 写(这是硬约束),但真正的 ETL 任务体可以用任何语言,包括 Java。也就是「Python 写流程编排 + Java 跑数据处理」。
原理
-
Airflow 里,
dags/*.py是 DAG 定义(调度、依赖、重试逻辑),必须 Python。 -
每个 task 用什么算子是关键:
BashOperator:直接执行 shell 命令 → 可以java -jar myetl.jarJavaOperator:Airflow 自带的专门跑 Java 类的算子SparkSubmitOperator:提交 Spark JAR(大数据 ETL 常用)
所以, Java 的定位是「被 Airflow 调用的可执行程序」,不是 DAG 本身。
在你现在的 Docker 环境里怎么加 Java ETL
假设你把 ETL 打成 myetl.jar,放在挂载目录里:
# dags/java_etl_demo.py
from datetime import datetime
from airflow import DAG
from airflow.operators.bash import BashOperator
with DAG(
dag_id="java_etl_demo",
start_date=datetime(2026, 9, 1),
schedule_interval=None,
catchup=False,
) as dag:
run_java_etl = BashOperator(
task_id="run_java_etl",
# jar 放在挂载的 dags 目录下,容器内路径为 /opt/airflow/dags
bash_command="java -jar /opt/airflow/dags/myetl.jar source_table target_table",
)
⚠️ 前提:容器里得有 JRE/JDK。默认 apache/airflow 镜像不带 Java。两种解决方式:
方式A:用带 JDK 的自定义镜像(推荐)
FROM apache/airflow:2.10.5
USER root
RUN apt-get update && apt-get install -y --no-install-recommends default-jdk-headless \
&& apt-get clean
USER airflow
然后 docker build 出镜像,再 docker run(同时挂载放 jar 的目录)。
方式B:不装 JDK,跑 Java 交给宿主机或外部 Spark 集群——一般做 Demo 不这么搞。
三种算子的选择
| 场景 | 用哪个算子 |
|---|---|
简单 java -jar xxx.jar |
BashOperator(最通用)或 JavaOperator |
| 大数据量、Spark/批处理 | SparkSubmitOperator(提交 JAR 到 Spark) |
| Java 里要读 Airflow 的 XCom / 变量 | 需要把参数通过 Bash 命令行或环境变量传进去 |
建议
- 开发 ETL Demo:能用 Python 就用 Python(不用管 JDK、镜像额外体积、打包),最快。
- 团队已有 Java 数据管道 / 涉及 Spark、Flink:才值得用 Java,写成
java -jar或 Spark JAR,Airflow 只负责调度。 - 一个 DAG 里也可以混合:A 任务跑 Python、B 任务跑 Java、C 任务跑 SQL,编排里用
>>串联即可。
Q: 如何基于 Docker 部署在本地电脑的 Apache Airflow(Standalone 版) 快速开发 ETL Demo(从 1 张表写道另 1 张表)?
本节示范一套基于 Airflow Standalone 镜像在本地(Docker Desktop)从零跑通「一张表 → 另一张表」的 ETL Demo 最快路径。
- 按最常见的假设来写:源/目标库用 SQLite(零额外容器、最快跑通),转换逻辑用
PythonOperator。
如果你实际要用 MySQL / PostgreSQL / SQLServer,只需要把 Python 里的连接对象换成对应驱动,或者用
PostgresOperator等现成算子,我可以再按你的库给你改。
1. 整体思路
Docker 容器 (apache/airflow: standalone)
│ 挂载 dags/ 目录
▼
DAG: source_orders 表 ──(抽取)──▶ Python 清洗/转换 ──(装载)──▶ target_orders 表
Standalone 版 = 官方开箱即用的单容器方案,内置了调度器 + Web UI + 元数据库,一条命令就能起来。
2. 前置准备
- 已安装 Docker Desktop 并启动(Windows 上依赖 WSL2)。
- 准备一个工作目录,比如
C:\airflow-demo。
3. 建立目录结构
C:\airflow-demo\
└── dags\
├── etl_demo.py # 你的 DAG 文件
└── etl.db # SQLite 数据库(运行时生成)
DAG 文件所在目录挂载进容器,这样改代码不用重新构建镜像。
4. 启动 Standalone 容器
在 C:\airflow-demo 下打开 PowerShell,执行:
# 建 dags 目录
New-Item -ItemType Directory -Force dags
# 启动容器(Windows 用 ${PWD},Linux 用 $(pwd))
docker run -d --name airflow-standalone `
-p 8080:8080 `
-v "${PWD}\dags:/opt/airflow/dags" `
-e AIRFLOW__CORE__LOAD_EXAMPLES=False `
apache/airflow:2.10.5 standalone
参数说明:
-p 8080:8080:Web UI 端口。-v ...dags:/opt/airflow/dags:把本地 DAG 目录挂进去。AIRFLOW__CORE__LOAD_EXAMPLES=False:不加载官方示例,界面干净。- 等待约 30~60 秒初始化完成。
登录:浏览器打开 [http://localhost:8080](http://localhost:8080),账号/密码都是 airflow(Standalone 默认)。
如果容器日志里提示权限/挂载问题,用
docker logs airflow-standalone看报错再对症处理。
5. 写 ETL DAG(源表 → 目标表)
新建 dags/etl_demo.py,内容如下:
from datetime import datetime
from airflow import DAG
from airflow.operators.python import PythonOperator
import sqlite3
DB_PATH = "/opt/airflow/dags/etl.db" # 放在挂载目录里,方便你在宿主机查看
def etl():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
# ---- 1) 建源表并造数据(模拟上游业务表)----
cur.execute("DROP TABLE IF EXISTS source_orders")
cur.execute("DROP TABLE IF EXISTS target_orders")
cur.execute("""
CREATE TABLE source_orders (
order_id INTEGER, amount REAL, status TEXT, ts TEXT
)
""")
cur.executemany(
"INSERT INTO source_orders VALUES (?, ?, ?, ?)",
[
(1, 100.5, "paid", "2026-09-01"),
(2, 299.0, "pending", "2026-09-01"),
(3, 50.0, "paid", "2026-09-02"),
(4, 1200.0,"refunded", "2026-09-02"),
],
)
conn.commit()
# ---- 2) 抽取 (Extract) ----
rows = cur.execute(
"SELECT order_id, amount, status, ts FROM source_orders"
).fetchall()
# ---- 3) 转换 (Transform):只保留已支付订单,金额加税 ----
TAX_RATE = 0.06
transformed = []
for order_id, amount, status, ts in rows:
if status == "paid":
transformed.append(
(order_id, round(amount * (1 + TAX_RATE), 2), ts)
)
# ---- 4) 装载 (Load):写入目标表 ----
cur.execute("""
CREATE TABLE target_orders (
order_id INTEGER, final_amount REAL, order_date TEXT
)
""")
cur.executemany(
"INSERT INTO target_orders VALUES (?, ?, ?)", transformed
)
conn.commit()
# 打印结果验证
result = cur.execute("SELECT * FROM target_orders").fetchall()
print(">>> target_orders:", result)
conn.close()
with DAG(
dag_id="etl_demo",
start_date=datetime(2026, 9, 1),
schedule_interval=None, # 手动触发;要定时可改成 '@daily' 等
catchup=False,
tags=["etl-demo"],
) as dag:
run_etl = PythonOperator(
task_id="run_etl",
python_callable=etl,
)
几个要点:
DB_PATH指向/opt/airflow/dags/etl.db,因为 SQLite 文件落在挂载目录里,宿主机C:\airflow-demo\dags\etl.db也能直接打开看结果。schedule_interval=None表示手动触发,适合 Demo;要模拟真实调度改成'@daily'即可。- 真正的项目里别用
DROP TABLE,这里只是为了 Demo 可重复跑。
6. 触发并验证
- 打开 http://localhost:8080 → 登录
airflow/airflow。 - 左侧 DAGs 列表找到 etl_demo,点击右边的 ▶ 播放按钮 触发,再点 >_ 图标查看 Graph / 日志。
- 看日志里打印的
>>> target_orders: [...],确认只有 3 条 paid 订单、金额已含 6% 税。 - 宿主机用任意 SQLite 工具打开
C:\airflow-demo\dags\etl.db,target_orders表就是 ETL 产物。
收尾:不跑时 docker stop airflow-standalone,重跑 docker start airflow-standalone;要彻底删除 docker rm -f airflow-standalone。

新增的 SQLLite 数据库及数据
E:\Program-Data\Docker-Data\docker-external-storage\airflow-standalone\dags\etl.db

7. 常见扩展
| 你的场景 | 怎么改 |
|---|---|
| 换成 MySQL / PostgreSQL | 用 pip install 驱动后改连接串;更推荐用 PostgresOperator/MySQLOperator 直接写 SQL |
| 源表和目标表都在 SQL 库里 | 定义两个 SQL 算子,中间用 PostgresOperator 跑 INSERT ... SELECT |
| 定时调度 | schedule_interval="@daily" |
| 多任务依赖 | 在 DAG 里用 >> 串联多个 PythonOperator |
| 用 Airflow 的变量/连接管理 | Web UI → Admin → Connections 里配置,DAG 里用 Variable/BaseHook 读取 |
Q: Airflow 在运行时,如何提交新增的、及待更新的DAG任务?
- 运行时新增/更新 DAG 的核心是:Airflow 的 Scheduler 会周期性「扫描 → 解析 → 序列化」DAG 目录里的 .py 文件,你不需要重启服务,改完文件它自己会刷出来。 下面分机制和操作讲。
运行时的加载机制(自动)
新增/修改 dags/xxx.py
│
▼
Scheduler 的 DagFileProcessorManager
│ ① 按 dag_dir_list_interval 扫描目录,发现新文件/文件mtime变化
│ ② 按 min_file_process_interval 重新解析该文件 (import 执行,生成 DAG 对象)
│ ③ 解析结果"序列化"写入元数据库 (Serialized DAG)
▼
Webserver 从元数据库读取 → UI 自动出现/更新,无需重启
关键默认参数(airflow.cfg 可调):
| 参数 | 默认值 | 作用 |
|---|---|---|
dag_dir_list_interval |
300s | 扫描 DAG 目录发现新增文件的间隔 |
min_file_process_interval |
30s | 发现文件变化后重新解析的最小间隔 |
dagbag_import_timeout |
30s | 单个文件解析超时 |
所以:新增文件最迟约 5 分钟内出现;修改已有文件约 30 秒后生效(取决于调度循环)。Airflow 2.x 里 Scheduler 默认持续解析,实际通常更快。
分别说明三种「提交」情况
1. 新增一个 DAG(新文件)
- 把
xxx.py放进挂载的dags/目录(你的容器里是/opt/airflow/dags)。 - 等目录扫描周期后,UI 的 DAGs 列表自动出现;若没出现,点 UI 右上角 刷新 或等下一个循环。
2. 更新一个 DAG(改同名的 .py)
- 直接改文件并保存,Scheduler 检测到 mtime 变化 → 重新解析 → 序列化更新 → UI 里任务结构、参数即时刷新。
- 注意:已运行的旧版本不会中断(Airflow 按文件在触发时刻的快照执行);改完只影响下一次运行的 DAG。
3. 删除一个 DAG
- 删掉 .py 文件 → 目录扫描后该 DAG 从 UI 消失;但历史运行记录仍留在元数据库(可查),若 DAG 之前是 paused,删除会保留在 DB。
手动强制刷新 / 触发运行(想"立即生效"时)
方式A:UI 操作
- 刷新 DAG:DAGs 列表点「刷新按钮」。
- 立即跑一次:DAG 行点 ▶ 触发(等同手动提交一个 DAG Run)。
方式B:CLI(在你的容器里执行)
# 列出当前已识别的所有 DAG
docker exec airflow-standalone airflow dags list
# 立即触发某个 DAG 跑一次(手动提交运行)
docker exec airflow-standalone airflow dags trigger etl_demo
# 强制让 Scheduler 立刻重新解析/序列化所有 DAG 文件
docker exec airflow-standalone airflow dags reserialize
让"改动实时生效"的实用建议
- 调小扫描间隔(Demo 时想快点看到改动):
docker exec airflow-standalone airflow config set scheduler dag_dir_list_interval 30 # 或改 airflow.cfg 后重启 scheduler - 文件命名规范:一个文件只放一个 DAG(
dag_id和文件名尽量一致),避免解析冲突。 - 改完文件建议先本地
python -c "import xxx"或看 UI 的「Code / Graph」确认解析成功,别等调度器去踩坑。
总结:Airflow 不需要你"提交"DAG,它是轮询文件目录自动加载的;你只需把文件放进 dags/(新增/覆盖即更新、删除即下线),其余交给 Scheduler,必要时用 UI 或 airflow dags trigger / reserialize 手动加速。
Q: 下游ETL任务如何知道 dag1_dataset 有更新呢?是通过该数据集的airflow上游任务的执行情况吗?
- 理解方向正确:下游是靠「上游任务执行成功」这个信号来感知数据集更新的,但有一个关键区别——它检测的是"任务成功"这个逻辑事件,并不真正去检查文件/表里数据有没有变。下面拆开讲。
感知机制(本质)
上游 producing_task_1 运行
│ ① 任务执行成功 (success)
▼
Scheduler 检测到该任务的 outlets 声明了 dag1_dataset
│ ② 在元数据库写入一条 "DatasetEvent"(数据集更新事件)
▼
下游 DAG(dataset_consumes_1) 的 schedule 引用了 dag1_dataset
│ ③ Scheduler 发现有新的 DatasetEvent
▼
自动为下游 DAG 创建一个新的 DAG Run 并执行
所以「知道有更新」的链条是:
上游任务成功 → 依据 outlets 声明 → 写 DatasetEvent → 下游按 schedule=[dataset] 被触发
它不是去比对文件 mtime、hash、表行数,也不是下游主动去轮询那个数据集。
关键结论
| 问题 | 答案 |
|---|---|
| 下游靠什么感知更新? | 靠元数据库里的 DatasetEvent(由上游任务成功产生) |
| 是看上游任务的执行情况吗? | 是,且只看「任务是否 success」,通过 outlets 声明关联 |
| 会真的检查数据变化吗? | 不会。即使 sleep 5 后数据没任何变化,只要任务 success,下游照样触发 |
| 上游失败会触发吗? | 不会。任务失败 → 不写 DatasetEvent → 下游不跑 |
| 事件存在哪? | 元数据库(UI → Admin → Datasets 可看事件历史) |
两个容易踩的坑(很重要)
坑1:任务成功 ≠ 数据真的变了
下游被触发是「按任务状态」而非「按数据内容」。如果上游 INSERT 是幂等的、或数据其实没新增,下游仍然会跑。若你必须在"数据确实变化"时才跑,就不能靠 dataset 调度,要用 Sensor:
FileSensor:轮询某个文件是否存在/更新- 自定义
PythonSensor:检查表行数/checksum/mtime 是否变化
总结:Datasets = 事件驱动(任务成功即触发);Sensors = 物理探测(真的去看数据/文件)。
坑2:多个数据集调度(schedule 是 list)的触发条件
schedule=[ds1, ds2]
- Airflow 会在每个 dataset 至少被更新过一次之后,才开始触发下游;
- 之后任一 dataset 再次更新都会触发新一轮运行。
- 不同版本(2.4→2.6+)对「多数据集需全部就绪」的判定细节略有差异,别把旧教程的行为当真理。
想看事件/排查链路
# 容器内查看 DAG 之间、与 dataset 的依赖关系
docker exec airflow-standalone airflow dags list-relationships
# 用 CLI 触发一下生产侧,观察是否产生事件
docker exec airflow-standalone airflow dags trigger dataset_produces_1
UI 路径:Admin → Datasets,能看到 dag1_dataset 的更新时间、由哪个 task 产出、触发了哪些下游。
Y 推荐文献
- https://github.com/apache/airflow
- https://airflow.apache.org/docs/apache-airflow/stable/tutorial/index.html | 官方教程(写第一个 DAG)
- https://airflow.apache.org/docs/apache-airflow/stable/index.html | 主文档(安装、概念、DAG、算子、CLI)
- https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/index.html | 核心概念(中文对照参考)
- https://airflow.apache.org/docs/apache-airflow/stable/_api/index.html | API 参考 / 算子列表
X 参考文献
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!









浙公网安备 33010602011771号