[数据编排/任务调度] 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

image

3 主流程体验篇

  • 如无特殊说明,本章默认采用方式1做的安装部署。

登录

image

DAGs

列表

  • 部署方式1的效果:

image

  • 部署方式2的效果:
    image

详情

Tab:Graph

image

Tab:Detail

image

Tab:Code

image

# 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

image

我触发/运行一个任务后:
image

Datasets

Dependency Events

image

Dependency Graph

  • 默认: Datasets(所有Dataset) 的 依赖图
    image

  • 选择其中1个Dataset后,则进入了 某一 Dataset 的详情页:

  • Dependency Graph
    image
  • Detail
    image

Datasets/数据集列表

image

Security/安全

用户列表

image

用户角色

image

image

用户统计

image

Actions/操作列表

image

Resources/资源列表

image

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

image

image

Browse/浏览

DAG Runs/DAG运行情况

image

image

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

image

  • Graph
    image
  • Code
    image
  • EventLg
    image
  • RunDuration
    image
  • Calendar
    image

Swagger/API 文档

image
image

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.jar
    • JavaOperator: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. 触发并验证

  1. 打开 http://localhost:8080 → 登录 airflow/airflow。
  2. 左侧 DAGs 列表找到 etl_demo,点击右边的 ▶ 播放按钮 触发,再点 >_ 图标查看 Graph / 日志。
  3. 看日志里打印的 >>> target_orders: [...],确认只有 3 条 paid 订单、金额已含 6% 税。
  4. 宿主机用任意 SQLite 工具打开 C:\airflow-demo\dags\etl.db,target_orders 表就是 ETL 产物。

收尾:不跑时 docker stop airflow-standalone,重跑 docker start airflow-standalone;要彻底删除 docker rm -f airflow-standalone。

image

新增的 SQLLite 数据库及数据

E:\Program-Data\Docker-Data\docker-external-storage\airflow-standalone\dags\etl.db

image

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 推荐文献

X 参考文献

posted @ 2026-09-22 01:58  千千寰宇  阅读(20)  评论(0)    收藏  举报