Airflow 2.X 新手生存指南:理解调度契约与状态机原理

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。这带来了三个质变:

  1. 真正的 Python 世界 :函数内部可以 import 任意包、调用任意函数、使用 for 循环和 if 判断,无需再把复杂逻辑硬塞进 python_callable 参数里。
  2. 自动输入/输出管理 @task 函数的返回值会自动序列化并存入 XCom(一种轻量级任务间通信机制),下游 @task 函数可以直接作为参数接收,无需手动 xcom_pull 。这极大简化了数据在任务间的传递。
  3. 声明式依赖更自然 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 图中有一个清晰的终点。

验证与观察

  1. 保存文件后,等待约 30 秒( min_file_process_interval )。
  2. 访问 http://localhost:8080 ,登录 admin/admin
  3. 在左侧菜单找到 DAGs ,你应该能看到 hello_state_machine
  4. 点击它,进入 DAG 概览页。你会看到一个由三个节点组成的图: prepare_data process_data end
  5. 点击右上角的 Trigger DAG 按钮(闪电图标)。这会立即创建一个 DagRun,状态为 running
  6. 点击 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 就会:

  1. dags_folder 加载对应的 hello_state_machine.py 文件,解析出 DAG 对象。
  2. 根据 DAG 的 schedule_interval execution_date ,计算出本次 DagRun 应该包含哪些 Task(即 task_ids 列表)。
  3. 为列表中的每一个 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,然后:

  1. 将它们的状态批量更新为 queued
  2. 将这些 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 后,它会做三件事:

  1. 重建上下文 :重新导入 Airflow 的核心模块,从数据库中根据 key 查询出完整的 TaskInstance 对象,确保它拥有所有元数据( dag_id , task_id , execution_date , try_number )。
  2. 准备执行环境 :设置好 PYTHONPATH ,确保能 import 到你的 DAG 文件;加载 Variables Connections ;根据 task.retries task.retry_delay 计算本次执行的超时时间。
  3. 调用你的函数 :对于 @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 也无输出。整个系统像死了一样。

排查实录

  1. 首先, ps aux | grep scheduler 确认 Scheduler 进程是否真的在运行。如果进程存在但无日志,说明它卡在了某个地方。
  2. 查看 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
    
  3. 定位到 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 ... ,然后超时。

排查实录

  1. 进入 ~/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 task
    
    没有任何 print 输出,也没有错误堆栈。
  2. 这说明 Worker 进程在 exec_module 之后、执行你的函数之前就挂了。最可能的原因是 import 失败。
  3. 检查 prepare_data() 函数,发现它开头有 import pandas as pd 。而我的虚拟环境中并没有安装 pandas
  4. 执行 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。

排查实录

  1. airflow dags list-runs -d hello_state_machine ,输出显示 execution_date 2020-01-01 开始,一直排到 2024-05-20
  2. 检查 DAG 文件, start_date=datetime(2020, 1, 1) ,且 catchup=True (默认值)。
  3. airflow dags pause hello_state_machine 暂停 DAG,阻止新 DagRun 创建。
  4. airflow dags delete hello_state_machine 彻底删除所有历史 DagRun(此操作不可逆,请确认)。
  5. 修改 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,而不是每小时一个。

排查实录

  1. 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 ……没错,确实是每小时一个。
  2. state 列全是 success ,没有 running 。说明它们都成功
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值