
凌晨三点脚本挂了,你第二天中午才知道。
摘要
Airflow 把一堆定时脚本变成一张有依赖、能重试、可回看的任务图。本文用 Airflow 3 从安装到跑通两条联动的管道(上游产出数据、下游自动触发),顺带列出老教程在 3.x 里会踩的坑。
crontab 的问题,不在"定时"
一开始,一条 crontab 就够了:每天两点拉数据,三点算报表。
脚本多到十几个以后,麻烦就来了:
- 依赖靠猜时间。 拉数据那天慢了半小时,三点的报表照样开跑,算的是昨天的数。
- 失败没人知道。 日志散在各台机器上,没有统一的地方看"今天哪些成功了"。
- 重跑全靠手。 补跑上周三的数据,得登机器、改参数、按顺序一条条敲。
这些都不是"定时"的问题,是"编排"的问题。Airflow 就是为这个生的:2014 年 Airbnb 的数据工程师 Maxime Beauchemin 发起了它,2019 年成为 Apache 顶级项目,今天在 GitHub 上有 4.7 万多颗星。
Airflow 是什么
一句话:用 Python 代码描述任务和依赖,由调度器按时间或事件去跑。
几个核心概念:
| 概念 | 意思 |
|---|---|
| DAG | 有向无环图,一条完整的管道,比如"拉数据 → 清洗 → 出报表" |
| Task | DAG 里的一个节点,一件具体要做的事 |
| Scheduler | 调度器,决定什么时候该跑哪个任务 |
| Asset | Airflow 3 的叫法(2.x 叫 Dataset),表示一份数据,可以拿来触发下游 |
DAG 只是一个放在 dags/ 目录里的 .py 文件,能写进 Git、能做代码评审,这是它和图形化拖拽调度工具最大的不同。
核心优势
- 依赖是显式的。 上游没成功,下游就不跑,不再靠"错开一小时"来赌。
- 失败自动重试,状态一目了然。 每个任务每次运行的状态和日志都在网页上,红绿一眼看清。
- 补跑和重跑是一等公民。 选中某天某个任务点"清除",它会带着当时的日期参数重新跑。
- 生态大。 数据库、云存储、Spark、dbt、Kubernetes 都有现成的 provider 包,不用自己写连接代码。
- 可以按数据触发。 可以说"这张表更新了就触发下游",不必再按钟点硬排(2.x 叫 Dataset,Airflow 3 改名 Asset 并继续加强)。
面向人群
- 手上有 5 个以上相互依赖的定时脚本、开始为"谁先谁后"头疼的人;
- 做 ETL、数据仓库、报表的数据工程师和分析师;
- 需要把模型训练、批量推理按流程串起来的算法工程师。
不太适合:毫秒级的实时流处理(那是 Flink、Kafka 的活),或者只有一两个互不相干的定时任务------这时候 crontab 依然是最省事的选择。
实践:从安装到两条管道联动
下面在本机跑通。需要 Python 3.10--3.14,Windows 请在 WSL2 里操作。
第 1 步:安装
Airflow 依赖很多,必须带官方的 constraint 文件安装,否则很容易装出一套互相冲突的版本:
bash
uv venv --python 3.12 && source .venv/bin/activate
AIRFLOW_VERSION=3.3.2
PYTHON_VERSION="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
CONSTRAINT_URL="https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt"
uv pip install "apache-airflow==${AIRFLOW_VERSION}" --constraint "${CONSTRAINT_URL}"
官方说明里写得很清楚:Poetry 和 pip-tools 不受支持,用 pip 或 uv。
第 2 步:一条命令启动
bash
airflow standalone
它会初始化数据库、创建管理员、启动全部组件。打开浏览器访问 localhost:8080,用户名 admin,自动生成的密码不一定会打在终端上,直接看这个文件:
bash
cat ~/airflow/simple_auth_manager_passwords.json.generated
standalone 只适合本地学习,生产环境要把各组件分开部署。
第 3 步:写第一个 DAG
在 ~/airflow/dags/ 下新建 daily_orders.py:
python
from datetime import timedelta
import pendulum
from airflow.sdk import Asset, dag, task
orders_report = Asset("file:///tmp/orders_report.json")
@dag(
schedule="0 2 * * *", # 每天凌晨两点
start_date=pendulum.datetime(2026, 10, 1, tz="Asia/Shanghai"),
catchup=False,
default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
tags=["demo"],
)
def daily_orders():
@task
def extract() -> dict:
# 实际项目里这里是查数据库或调接口
return {"1001": 301.27, "1002": 433.21, "1003": 502.22}
@task
def transform(orders: dict) -> float:
return round(sum(orders.values()), 2)
@task(outlets=[orders_report])
def load(total: float):
import json
with open("/tmp/orders_report.json", "w") as f:
json.dump({"total": total}, f)
print(f"今日订单总额:{total}")
load(transform(extract()))
daily_orders()
几个要点:
@task函数的返回值会自动传给下一个任务,load(transform(extract()))这一行同时定义了数据流和依赖顺序。retries=2表示任务失败会自动再试两次,间隔 5 分钟。outlets=[orders_report]告诉 Airflow:"这个任务成功后,orders_report这份数据更新了。"
第 4 步:不开调度器,先在命令行测一遍
bash
airflow dags test daily_orders
整条 DAG 会在当前进程里跑一遍,日志直接打到终端。写 DAG 时先用它调通,比在网页上反复点快得多。
第 5 步:让数据更新来触发下游
再建一个 notify.py,不写时间,而是"等报表更新":
python
import pendulum
from airflow.sdk import Asset, dag, task
orders_report = Asset("file:///tmp/orders_report.json")
@dag(schedule=[orders_report], start_date=pendulum.datetime(2026, 10, 1), catchup=False)
def notify_on_report():
@task
def send():
print("报表已更新,发通知")
send()
notify_on_report()
daily_orders 里的 load 一成功,notify_on_report 就会被触发。如果 schedule 里列了多个资产,Airflow 会等它们都更新过才跑。
回到网页,把两个 DAG 都打开(新 DAG 默认是暂停状态),手动触发一次 daily_orders,就能看到下游自己跟着跑起来。
老教程在 Airflow 3 里会踩的坑
网上大量教程还是 2.x 写法,照抄轻则弹弃用警告,重则直接报错:
| 2.x 写法 | Airflow 3 |
|---|---|
schedule_interval="@daily" |
参数已移除,统一用 schedule |
from airflow.decorators import dag, task |
改从 airflow.sdk 导入,旧路径已弃用 |
不写 catchup 默认补跑历史 |
默认不补跑,需要补跑要显式写 catchup=True |
任务里用 execution_date |
已移除,改用 logical_date |
| SubDAG | 已移除,用 TaskGroup 或资产代替 |
| 任务里直接连元数据库 | 不再允许,走官方提供的接口 |
存量代码可以先跑一遍 ruff:AIR301、AIR302 挑出 Airflow 3 里会直接报错的写法,AIR311、AIR312 挑出"还能跑但建议改"的写法(比如 airflow.decorators 旧导入)。
排错速查
- 新 DAG 在网页上看不到: 先跑
airflow dags list-import-errors,多半是文件里有语法错误或导入失败;没有报错就再等一会儿,目录默认每 5 分钟扫描一次。 - DAG 出现了但不跑: 检查是不是还处于暂停状态,以及
start_date是否在未来。 - 装完一启动就报依赖错误: 基本是没带 constraint 文件,删掉虚拟环境按第 1 步重装。
- 登录密码找不到: 看
$AIRFLOW_HOME/simple_auth_manager_passwords.json.generated。
收藏这三行
- 装:带 constraint 文件装,本地用
airflow standalone起。 - 写:
airflow.sdk里的@dag+@task,返回值即依赖,失败靠retries兜底。 - 测:先
airflow dags test调通,再在网页上打开 DAG 交给调度器;下游用schedule=[Asset(...)]跟着数据走。