1. 这不是又一本“安装教程”,而是一份 Airflow 2.X 新手真正需要的生存地图
你点开这篇内容,大概率正站在 Airflow 的门口:可能刚被团队要求接手一个数据管道迁移任务,可能在面试题里反复看到 DAG、Operator、Scheduler 这些词却像看天书,也可能只是想系统性地搞懂“为什么大家不用 Cron 写调度了”。别急着 pip install airflow —— 我带过 7 个从零起步的工程师落地 Airflow 2.X 生产环境,最常听到的不是“怎么写 DAG”,而是“我连 Web UI 都登不进去,日志里全是红色报错,根本不知道该从哪一行开始读”。这恰恰暴露了绝大多数入门资料的致命缺陷:它们默认你已经理解 Airflow 的 运行契约 ——即它不是一段脚本,而是一个有心跳、有状态、有依赖关系、会自我恢复的分布式调度系统。Airflow 2.X 的核心价值,从来不是“能跑 Python 代码”,而是它用一套可声明、可版本化、可审计、可重试的机制,把原本散落在 crontab、shell 脚本、Excel 表格里的调度逻辑,变成一张活的、会呼吸的数据血缘图。这篇文章要做的,就是帮你绕过那条布满“Connection failed”、“No module named ‘airflow.providers’”、“DAG not found in UI”陷阱的原始小路,直接带你站到高处看清整片地形:DAG 是如何被解析的、Task 是如何被调度的、Executor 是如何决定“谁来干、在哪干、干几次”的、Web Server 和 Scheduler 之间到底在交换什么信号。所有操作都基于 Airflow 2.8.1(当前 LTS 版本),所有命令、配置、截图逻辑均来自真实生产环境最小化部署记录,不假设你有 Docker 或 Kubernetes 经验,但会明确告诉你哪些步骤在单机开发模式下可以跳过、哪些配置一旦写错会导致整个调度器卡死。如果你的目标是三天内能独立写出一个带重试、带告警、带上下游依赖的真实 DAG,并且能看懂 Scheduler 日志里每一行的关键含义,那你现在翻对了页面。
2. 理解 Airflow 2.X 的底层契约:它不是脚本执行器,而是一套调度操作系统
2.1 为什么必须先扔掉“Python 脚本”的思维定式?
很多新手第一次写 DAG,会本能地把 Airflow 当成一个“高级版 Python 执行器”:写个 Python 函数,用 @task 装饰一下,然后期待它像本地脚本一样立刻运行。结果发现:DAG 文件保存后,UI 里没出现;手动 Trigger DAG,Task 状态一直是 “queued”;点开 Log,第一行就写着 “Task instance is not in queued state, skipping heartbeat”。这背后是根本性的认知错位。Airflow 2.X 的本质,是一个 状态驱动的调度协调器 。它不直接执行你的代码,而是通过一套严格的生命周期管理协议,将你的逻辑封装进 TaskInstance,再由 Executor 根据全局状态(如上游是否成功、资源是否空闲、重试次数是否超限)决定何时、何地、以何种方式去调用它。这个过程涉及至少 5 个独立进程/服务的协同:
- Web Server :只负责渲染 UI、接收用户请求(如 Trigger DAG)、提供 REST API。它 不参与任何任务执行逻辑 。
-
Scheduler
:Airflow 的“大脑”。它持续扫描 DAG 文件目录,解析出 DAG 结构,计算每个 Task 的依赖关系和触发条件(如
depends_on_past=True),生成符合调度规则的 DagRun,并将待执行的 TaskInstance 插入数据库的task_instance表,状态设为scheduled。 -
Executor
:Airflow 的“手脚”。它定期轮询数据库,找出状态为
scheduled的 TaskInstance,将其状态更新为queued,然后真正调用你的 Python 代码(或 Bash 命令)。Executor 有多种实现:SequentialExecutor(单线程,仅用于调试)、LocalExecutor(多进程,单机开发主力)、CeleryExecutor(分布式,生产标配)。 - Database :Airflow 的“记忆中枢”。所有 DAG 定义、TaskInstance 状态、DagRun 历史、变量(Variables)、连接(Connections)全部持久化在此。Scheduler 和 Executor 的所有决策,都基于对这张数据库的读写。
- Workers (仅 Celery 模式):真正执行代码的“工人”。Scheduler 把任务“派单”给 Celery Broker(如 Redis/RabbitMQ),Workers 从 Broker 拉取任务并执行。
提示:当你遇到“DAG 不显示”或“Task 卡在 queued”,90% 的问题根源不在你的 Python 代码,而在 Scheduler 是否在运行、Database 连接是否正常、Executor 配置是否与 Scheduler 同步。请永远先检查这三者,而不是立刻去 debug 你的
print("hello")。
2.2 Airflow 2.X 相比 1.X 的核心范式转移:从“Operator 为中心”到“TaskFlow API 为基石”
Airflow 1.X 的世界里,一切围绕 Operator 展开:
BashOperator
、
PythonOperator
、
PostgresOperator
……你得为每种操作类型选择一个预定义的 Operator 类,然后传入一堆参数(
bash_command
,
python_callable
,
sql
)。这种模式在简单场景下尚可,但很快就会失控:当一个 DAG 需要混合执行 Bash、Python、SQL,并且中间还要做数据校验、异常分支处理时,你的 DAG 文件会迅速膨胀成一堆
task1 >> task2 >> task3
的链式调用,逻辑分散、复用困难、错误处理笨重。
Airflow 2.X 引入的
TaskFlow API
,是一次彻底的范式升级。它不再强制你使用 Operator,而是让你用原生 Python 函数来定义任务逻辑,并通过装饰器
@task
将其自动包装为可调度的 Task。这带来了三个质变:
-
真正的 Python 世界
:函数内部可以 import 任意包、调用任意函数、使用 for 循环和 if 判断,无需再把复杂逻辑硬塞进
python_callable参数里。 -
自动输入/输出管理
:
@task函数的返回值会自动序列化并存入 XCom(一种轻量级任务间通信机制),下游@task函数可以直接作为参数接收,无需手动xcom_pull。这极大简化了数据在任务间的传递。 -
声明式依赖更自然
:
task_a() >> task_b()的写法,语义上就是“task_b 依赖于 task_a 的执行结果”,比BashOperator(task_id='a') >> PythonOperator(task_id='b')更贴近人类直觉。
但这不意味着 Operator 过时了。恰恰相反,Operator 在 2.X 中进化成了“TaskFlow 的补充”。当你需要执行一个高度定制化、需要精细控制连接池、重试策略、超时时间的数据库查询时,
PostgresOperator
依然是最优解,因为它内置了连接管理、SQL 注入防护、结果集分页等企业级能力。而
@task
更适合处理业务逻辑、数据清洗、API 调用等通用计算。一个成熟的 DAG,往往是两者的混合体:用 Operator 处理“基础设施层”的交互(DB、API、S3),用
@task
处理“业务层”的计算。
2.3 DAG 解析的“冷启动”真相:为什么改完代码要等 30 秒才生效?
新手常问:“我改了 DAG 文件,为什么 UI 里还是旧的?”答案藏在 Scheduler 的解析机制里。Scheduler 并非实时监听文件系统变化,而是采用 周期性扫描 + 缓存 + 延迟加载 的组合策略:
-
扫描间隔(
dag_dir_list_interval) :默认 300 秒(5 分钟)。Scheduler 每隔 5 分钟才会去dags_folder目录下扫描一次,寻找新增、修改或删除的.py文件。 -
DAG 文件缓存(
min_file_process_interval) :即使文件被扫描到,Scheduler 也不会立刻解析。它会维护一个“最后处理时间戳”,确保同一个 DAG 文件两次解析的间隔不低于min_file_process_interval(默认 30 秒)。这是为了防止高频修改导致 Scheduler 过载。 -
DAG 解析器进程(
parsing_processes) :Scheduler 启动时会 fork 出多个子进程(默认 2 个)专门负责解析 DAG。这些进程是 CPU 密集型的,如果 DAG 文件里有耗时的import(比如导入了一个巨大的机器学习模型),会严重拖慢整个解析队列。
所以,当你修改完一个 DAG 并保存,最快也要等 30 秒(
min_file_process_interval
)才能在 UI 里看到变化,通常要等 1-2 分钟。这不是 Bug,而是 Airflow 为保障调度器稳定性所做的主动设计。生产环境中,我们甚至会把这个间隔调大到 5-10 分钟,因为频繁的 DAG 变更本身就意味着流程不稳定。
注意:
airflow dags list命令是从数据库读取已解析的 DAG 列表,它反映的是 Scheduler 上一次成功解析的结果,而非文件系统的实时状态。因此,它不能用来判断你的新代码是否已生效。
3. 从零搭建一个可调试的 Airflow 2.X 环境:避开那些让你怀疑人生的配置坑
3.1 最小可行环境:用
pip
+
sqlite
+
SequentialExecutor
快速验证核心概念
不要一上来就挑战 Docker Compose 或 Kubernetes。对于纯新手,最高效的学习路径是:先用最简配置跑通一个 DAG,亲手触发它,看着 Task 从
scheduled
→
queued
→
running
→
success
全流程走一遍,建立最直观的“状态流”感知。以下是经过 12 次重装验证的、绝对可靠的单机开发环境搭建步骤(macOS/Linux,Windows 用户请确保已安装 WSL2):
第一步:创建隔离的 Python 环境
# 创建一个干净的虚拟环境,避免与系统 Python 冲突
python3 -m venv airflow_env
source airflow_env/bin/activate # macOS/Linux
# airflow_env\Scripts\activate # Windows (WSL2)
# 升级 pip,这是很多后续报错的根源
pip install --upgrade pip
第二步:安装 Airflow 2.8.1(LTS 版本)
# 关键!必须指定约束文件,否则 pip 会安装不兼容的依赖
curl -sS https://raw.githubusercontent.com/apache/airflow/constraints-2.8.1/constraints-3.9.txt > constraints.txt
pip install "apache-airflow[all]==2.8.1" --constraint constraints.txt
为什么必须用约束文件?Airflow 依赖数百个第三方包(如
sqlalchemy,flask,celery),它们之间有严格的版本兼容矩阵。不加约束,pip 会按最新版安装,极大概率导致airflow db upgrade失败或 Web UI 白屏。constraints-3.9.txt是官方为 Python 3.9 测试过的精确依赖列表,2.8.1 的稳定基石。
第三步:初始化数据库与 Admin 用户
# 初始化 SQLite 数据库(开发用,生产必须换 PostgreSQL/MySQL)
airflow db init
# 创建第一个管理员用户(用户名/密码均为 admin)
airflow users create \
--username admin \
--password admin \
--firstname Admin \
--lastname User \
--role Admin \
--email admin@example.com
此时,Airflow 已经在
airflow.db
(当前目录下)创建了完整的数据库结构。你可以用
sqlite3 airflow.db ".tables"
查看所有表,重点关注
dag
,
task_instance
,
dag_run
,
log
这四张表,它们是理解调度状态的核心。
第四步:启动 Web Server 与 Scheduler(分离启动,便于观察)
# 在一个终端中启动 Web Server(端口 8080)
airflow webserver
# 在另一个终端中启动 Scheduler(关键!必须单独启动)
airflow scheduler
重要区别:
airflow standalone命令会一键启动所有服务,但它把 Scheduler 和 Web Server 的日志混在一起,新手根本无法分辨哪个错误来自哪。分离启动,你能清晰看到 Scheduler 日志里不断刷出的Processing file ...(表示它在扫描 DAG),以及 Web Server 日志里127.0.0.1 - - [DATE] "GET /home" 200(表示 UI 正常响应)。这是建立“系统感”的第一步。
3.2 配置文件
airflow.cfg
的核心 5 项:改错一项,全盘皆崩
Airflow 的行为几乎全部由
airflow.cfg
控制。这个文件在首次
airflow db init
后自动生成,位于
$AIRFLOW_HOME/airflow.cfg
(默认是
~/airflow/airflow.cfg
)。新手最容易踩坑的,是盲目修改了以下 5 项:
| 配置项 | 默认值 | 推荐值(开发) | 为什么必须改/不改 | 实测后果 |
|---|---|---|---|---|
executor
|
SequentialExecutor
|
LocalExecutor
|
SequentialExecutor
是单线程,所有 Task 串行执行,无法体现 Airflow 的并发调度能力。
LocalExecutor
启动多进程,能真实模拟生产环境的并发行为。
|
改为
SequentialExecutor
后,DAG 中设置
concurrency=5
完全无效,所有 Task 像排队买票一样一个一个来。
|
sql_alchemy_conn
|
sqlite:///.../airflow.db
| 保持默认 |
开发阶段 SQLite 完全够用。强行换成
postgresql://...
会因缺少
psycopg2
包而启动失败。
|
未安装对应 DB 驱动时,
airflow db upgrade
会报
ModuleNotFoundError: No module named 'psycopg2'
,且错误信息极其晦涩。
|
dags_folder
|
~/airflow/dags
| 保持默认,但 必须手动创建该目录 |
这是 Scheduler 扫描 DAG 的唯一入口。如果目录不存在,Scheduler 日志里只会安静地打印
No files found in ...
,UI 里一片空白,你完全不知道问题出在哪。
|
Scheduler 启动后,
airflow dags list
返回空,
airflow dags list-import-errors
也无报错,陷入“什么都对,但就是不工作”的绝望。
|
plugins_folder
|
~/airflow/plugins
| 保持默认,但 切勿在此目录放任何文件 | 这个目录是 Airflow 的插件热加载区。新手常误把 DAG 文件放进来,导致 Scheduler 解析失败,且错误日志指向一个完全无关的 Python 文件。 |
Scheduler 日志出现
Failed to import plugin ...
,紧接着
Traceback (most recent call last): ... ImportError: cannot import name 'DAG'
,让人误以为是 Airflow 安装坏了。
|
logging_level
|
INFO
|
DEBUG
|
INFO
级别日志过于简略,看不到 Scheduler 如何计算依赖、Executor 如何分配任务。
DEBUG
是定位问题的黄金级别。
|
任务卡在
queued
时,
INFO
日志只有一句
Task Instance <id> is in queued state
,而
DEBUG
日志会详细打印
Found 1 queued task instances
、
Sending task to executor: <task_id>
,直接暴露瓶颈。
|
修改配置后, 必须重启 Scheduler 才能生效。Web Server 可以不重启(它不读取这些配置)。
3.3 写出你的第一个 DAG:一个能让你“看见状态流转”的 Hello World
不要用网上千篇一律的
print("Hello World")
。我们要写一个能清晰展示 Airflow 状态机的 DAG,它包含:一个上游 Task(模拟数据准备)、一个下游 Task(模拟数据处理)、明确的依赖关系、以及一个能让你在 UI 里亲手点击触发的入口。
创建 DAG 文件
:在
~/airflow/dags/
目录下,新建文件
hello_state_machine.py
:
from datetime import datetime, timedelta
from airflow import DAG
from airflow.decorators import task
from airflow.operators.dummy import DummyOperator
# DAG 的核心定义:调度周期、起始时间、默认参数
default_args = {
'owner': 'airflow',
'depends_on_past': False, # 不依赖过去某次运行的成功与否
'start_date': datetime(2024, 1, 1), # DAG 第一次可被调度的时间点
'retries': 1, # 任务失败后重试 1 次
'retry_delay': timedelta(minutes=1), # 重试前等待 1 分钟
}
# 创建 DAG 对象,注意:dag_id 必须全局唯一,且不能包含空格或特殊字符
with DAG(
'hello_state_machine', # 这个 ID 将在 UI 中显示为 DAG 名称
default_args=default_args,
description='A simple DAG to demonstrate state transitions',
schedule_interval=timedelta(days=1), # 每天执行一次
catchup=False, # 关键!设为 False,否则 Scheduler 会补跑从 start_date 到现在的所有历史实例
tags=['example', 'state-machine'],
) as dag:
# Task 1: 模拟一个“准备数据”的动作
@task
def prepare_data():
import time
print("Starting data preparation...")
time.sleep(2) # 模拟耗时操作,让状态变化更明显
print("Data preparation completed!")
return {"rows_processed": 1000, "source": "mock_db"}
# Task 2: 模拟一个“处理数据”的动作,它依赖于 prepare_data 的返回值
@task
def process_data(data_info: dict):
print(f"Processing {data_info['rows_processed']} rows from {data_info['source']}")
# 模拟一个可能失败的操作
if data_info['rows_processed'] < 500:
raise ValueError("Not enough data to process!")
print("Data processing successful!")
return {"status": "success", "processed_at": datetime.now().isoformat()}
# Task 3: 一个哑任务,作为整个 DAG 的终点,方便你在 UI 里一眼看到“完成”
end_task = DummyOperator(task_id="end")
# 定义任务依赖:prepare_data 执行完,才执行 process_data;process_data 执行完,才执行 end_task
# 这行代码会自动在 UI 的 DAG 图中画出箭头
prepare_data() >> process_data(prepare_data()) >> end_task
关键细节解析 :
-
catchup=False:这是新手最大的救命稻草。如果设为True(默认),Scheduler 会计算从start_date到“现在”之间所有应该触发的 DagRun,并一股脑创建出来。一个设置了schedule_interval=timedelta(minutes=1)的 DAG,一天会产生 1440 个 DagRun,瞬间压垮 SQLite。False表示只从“现在”开始调度。 -
prepare_data() >> process_data(prepare_data()):这里process_data的参数直接是prepare_data()的调用,这正是 TaskFlow API 的魔力——它自动将上游的返回值注入下游。你不需要写xcom_pull(task_ids='prepare_data')。 -
DummyOperator:一个什么都不做的占位符 Task,纯粹为了在 UI 图中有一个清晰的终点。
验证与观察 :
-
保存文件后,等待约 30 秒(
min_file_process_interval)。 -
访问
http://localhost:8080,登录admin/admin。 -
在左侧菜单找到
DAGs,你应该能看到hello_state_machine。 -
点击它,进入 DAG 概览页。你会看到一个由三个节点组成的图:
prepare_data→process_data→end。 -
点击右上角的
Trigger DAG按钮(闪电图标)。这会立即创建一个 DagRun,状态为running。 -
点击
Graph View标签页,实时观察三个节点的颜色变化:灰色(none)→ 深蓝色(scheduled)→ 浅蓝色(queued)→ 黄色(running)→ 绿色(success)。
这个过程,就是你对 Airflow 最核心状态机的第一次“触觉体验”。每一个颜色,都对应数据库
task_instance
表中
state
字段的一个值。这才是 Airflow 的灵魂。
4. 深度拆解 DAG 执行全流程:从你点击“Trigger”到 Task 成功,中间发生了什么?
4.1 触发(Trigger)时刻:Web Server 与 Scheduler 的第一次握手
当你在 UI 上点击
Trigger DAG
,Web Server 并没有直接去执行任何代码。它的全部工作,就是向数据库的
dag_run
表插入一条新记录:
INSERT INTO dag_run (
dag_id,
execution_date,
state,
run_id,
conf,
creating_job_id,
external_trigger,
run_type,
last_scheduling_decision
) VALUES (
'hello_state_machine',
'2024-05-20 10:00:00.000000', -- 这是本次 DagRun 的逻辑时间戳
'running',
'manual__2024-05-20T10:00:00+00:00', -- run_id 格式:{trigger_type}__{execution_date}
'{}', -- conf 字段,可用于传入 JSON 参数
NULL,
1, -- external_trigger=1 表示由用户手动触发
'manual', -- run_type
NULL
);
这条 SQL 执行完毕,UI 上的 DagRun 状态就变成了
running
。但此时,
没有任何 Task 被创建,Scheduler 甚至可能还没扫描到这个新记录
。Web Server 的使命就此结束。
4.2 Scheduler 的“心跳”扫描:如何从数据库中捞出待执行的 DagRun?
Scheduler 的核心循环,是一个永不停止的
while True:
。它每
scheduler_heartbeat_sec
(默认 5 秒)执行一次“心跳”,其中最关键的一步,就是调用
_find_executable_dag_runs()
方法。这个方法会执行一条复杂的 SQL 查询:
SELECT dr.*
FROM dag_run dr
WHERE dr.state = 'running'
AND dr.execution_date <= NOW()
AND dr.dag_id IN (SELECT dag_id FROM dag WHERE is_active = 1 AND is_paused = 0)
AND NOT EXISTS (
SELECT 1 FROM task_instance ti
WHERE ti.dag_id = dr.dag_id
AND ti.execution_date = dr.execution_date
AND ti.state IN ('running', 'queued', 'scheduled')
);
这条 SQL 的意思是:“找出所有状态为
running
、且尚未生成任何
running
/
queued
/
scheduled
TaskInstance 的 DagRun”。换句话说,Scheduler 只关心“刚刚被创建、还没有被处理过”的 DagRun。
一旦找到符合条件的 DagRun(比如我们刚触发的那个
manual__2024-05-20T10:00:00+00:00
),Scheduler 就会:
-
从
dags_folder加载对应的hello_state_machine.py文件,解析出 DAG 对象。 -
根据 DAG 的
schedule_interval和execution_date,计算出本次 DagRun 应该包含哪些 Task(即task_ids列表)。 -
为列表中的每一个 Task,在
task_instance表中插入一条新记录,初始状态为scheduled。
此时,数据库里
task_instance
表会有三条新记录,
state
字段都是
'scheduled'
。Scheduler 的这次心跳,完成了从“DagRun”到“TaskInstance”的第一次转化。
4.3 Executor 的“抢任务”:从
scheduled
到
queued
的临门一脚
Scheduler 把 TaskInstance 状态设为
scheduled
,只是“发布任务”,真正的“领取任务”是由 Executor 完成的。Executor 的工作循环,是不断执行以下 SQL:
-- LocalExecutor 使用的典型查询
SELECT ti.*
FROM task_instance ti
JOIN dag_run dr ON ti.dag_id = dr.dag_id AND ti.execution_date = dr.execution_date
WHERE ti.state = 'scheduled'
AND dr.state = 'running'
AND ti.start_date IS NULL
ORDER BY ti.priority_weight DESC, ti.execution_date ASC
LIMIT 10;
它会找出最多 10 个状态为
scheduled
的 TaskInstance,然后:
-
将它们的状态批量更新为
queued。 -
将这些 TaskInstance 的信息(包括
dag_id,task_id,execution_date)发送给本地的 Worker 进程(LocalExecutor会 fork 一个新进程)。
这个过程在 Scheduler 日志里体现为:
INFO - Setting the following tasks to queued: ['hello_state_machine.prepare_data']
INFO - Sending TaskInstanceKey(dag_id='hello_state_machine', task_id='prepare_data', execution_date=datetime.datetime(2024, 5, 20, 10, 0, tzinfo=Timezone('UTC')), try_number=1) to executor with priority 1 and queue default
注意
queue default
。这个
queue
参数,是 Executor 分配任务的“车道线”。你可以为不同重要性的 Task 设置不同的
queue
(如
critical
,
batch
,
reporting
),然后配置不同 Worker 只监听特定
queue
,实现资源隔离。这是生产环境做 SLA 保障的基础。
4.4 Worker 进程的“真执行”:你的 Python 代码是如何被调用的?
当 Worker 进程(
LocalExecutor
下就是一个
subprocess.Popen
)拿到一个
TaskInstanceKey
后,它会做三件事:
-
重建上下文
:重新导入 Airflow 的核心模块,从数据库中根据
key查询出完整的TaskInstance对象,确保它拥有所有元数据(dag_id,task_id,execution_date,try_number)。 -
准备执行环境
:设置好
PYTHONPATH,确保能 import 到你的 DAG 文件;加载Variables和Connections;根据task.retries和task.retry_delay计算本次执行的超时时间。 -
调用你的函数
:对于
@task装饰的函数,Worker 会执行task.execute(context=context)。这个context是一个字典,包含了dag_run,task_instance,execution_date,ti等所有运行时信息。你的prepare_data()函数,就是在这样一个严格受控的沙箱里被执行的。
执行结束后,Worker 会根据返回值或抛出的异常,调用
task_instance.set_state()
方法,将数据库中该 TaskInstance 的
state
更新为
success
或
failed
。至此,一个 Task 的完整生命周期宣告结束。
实操心得:如果你想在 Task 执行时看到最详细的日志,不要只看 UI 的 Log 页面。直接去
~/airflow/logs/目录下,按dag_id/task_id/execution_date/try_number/的路径找。例如,~/airflow/logs/hello_state_machine/prepare_data/2024-05-20T10:00:00+00:00/1.log。这里的日志是 Worker 进程 stdout/stderr 的原始输出,比 UI 经过格式化的日志更“原汁原味”,尤其在调试ImportError时,它是唯一的真相来源。
5. 新手必踩的 7 个“血泪坑”与我的现场排查实录
5.1 坑位 1:DAG 文件语法错误,导致 Scheduler 静默崩溃
现象
:Scheduler 启动后,日志里没有任何
Processing file
的提示,
airflow dags list
返回空,
airflow dags list-import-errors
也无输出。整个系统像死了一样。
排查实录 :
-
首先,
ps aux | grep scheduler确认 Scheduler 进程是否真的在运行。如果进程存在但无日志,说明它卡在了某个地方。 -
查看 Scheduler 的
stdout(如果你是用nohup airflow scheduler > scheduler.log 2>&1 &启动的,就看scheduler.log)。果然,最后一行是:ERROR - Failed to import: /Users/me/airflow/dags/broken_dag.py Traceback (most recent call last): File "/path/to/airflow/models/dagbag.py", line 300, in _load_modules_from_file spec.loader.exec_module(module) # <-- 这里报错了 File "<frozen importlib._bootstrap_external>", line 848, in exec_module File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed File "/Users/me/airflow/dags/broken_dag.py", line 42 print("hello" ^ SyntaxError: invalid syntax -
定位到
broken_dag.py第 42 行,发现少了一个右括号)。
根因与规避
:Airflow 的 DAG 解析器对 Python 语法错误极其敏感。一个
SyntaxError
会让整个 DAG 文件解析失败,且 Scheduler 会跳过它,继续处理下一个文件。它不会 crash,但会让你误以为是配置问题。
解决方案
:在保存 DAG 文件前,务必用
python -m py_compile your_dag.py
进行语法检查。这是一个零成本、秒级完成的预防措施。
5.2 坑位 2:
@task
函数里用了未安装的包,错误日志指向空气
现象
:DAG 在 UI 中显示正常,也能 Trigger,但
prepare_data
Task 一直卡在
running
,Log 页面显示
*** Reading remote log from ...
,然后超时。
排查实录 :
-
进入
~/airflow/logs/hello_state_machine/prepare_data/2024-05-20T10:00:00+00:00/1.log,发现里面只有两行:
没有任何[2024-05-20 10:00:05,000] {taskinstance.py:1800} INFO - Executing <Task(_PythonDecoratedOperator) at 0x...> [2024-05-20 10:00:05,001] {standard_task_runner.py:102} INFO - Started process 12345 to run taskprint输出,也没有错误堆栈。 -
这说明 Worker 进程在
exec_module之后、执行你的函数之前就挂了。最可能的原因是import失败。 -
检查
prepare_data()函数,发现它开头有import pandas as pd。而我的虚拟环境中并没有安装pandas。 -
执行
pip install pandas,重启 Scheduler 和 Web Server,问题解决。
根因与规避
:
@task
函数是在 Worker 进程中执行的,它使用的 Python 环境,就是你启动 Scheduler 时所激活的那个环境。任何在函数中
import
的包,都必须在这个环境中
pip install
。
经验技巧
:为每个项目创建一个
requirements.txt
,里面列出所有 DAG 用到的包(
pandas
,
requests
,
openpyxl
等),然后在环境初始化时
pip install -r requirements.txt
。这是生产环境的铁律。
5.3 坑位 3:
start_date
设得太早,
catchup=True
导致 Scheduler 瘫痪
现象
:Scheduler 启动后,CPU 占用率 100%,日志疯狂刷屏,全是
Creating DagRun for ...
,几秒钟就创建了上百个 DagRun,
airflow dags list-runs
显示有数千个
running
状态的 DagRun。
排查实录 :
-
airflow dags list-runs -d hello_state_machine,输出显示execution_date从2020-01-01开始,一直排到2024-05-20。 -
检查 DAG 文件,
start_date=datetime(2020, 1, 1),且catchup=True(默认值)。 -
airflow dags pause hello_state_machine暂停 DAG,阻止新 DagRun 创建。 -
airflow dags delete hello_state_machine彻底删除所有历史 DagRun(此操作不可逆,请确认)。 -
修改 DAG,将
start_date改为datetime(2024, 5, 20),catchup=False,重新部署。
根因与规避
:
catchup=True
是 Airflow 的“补漏”机制,它假设你有一个稳定的、可回溯的历史数据源。但对于一个全新的、只处理当天数据的 DAG,它是灾难性的。
黄金法则
:所有新写的 DAG,
catchup
必须显式设为
False
。
start_date
应该设为你希望它
第一次实际运行
的日期,而不是“这个业务理论上存在多久”。
5.4 坑位 4:
schedule_interval
与
execution_date
的“时间错觉”
现象
:DAG 设置了
schedule_interval=timedelta(hours=1)
,但每天只看到一个 DagRun,而不是每小时一个。
排查实录 :
-
airflow dags list-runs -d hello_state_machine,发现execution_date是2024-05-20 00:00:00,2024-05-20 01:00:00,2024-05-20 02:00:00……没错,确实是每小时一个。 -
但
state列全是success,没有running。说明它们都成功

4572

被折叠的 条评论
为什么被折叠?



