未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用范围与待维护事项

本文主要按 Airflow 2.2+ 的 cron 数据区间调度模型讲解,不代表所有 2.x 小版本或 Airflow 3.x 均兼容;execution_date、next_ds、传统 SLA 和补跑命令须按部署版本核对,本文未提供环境实测记录。每天 02:00 的区间并非前一自然日,调度就绪也不等于任务准时启动。代码为片段,需补齐导入、函数与 provider,统一数据库时间字段和查询边界的时区;历史重跑前须验证幂等及重复写入处理。美国东部时区还需考虑夏令时。

调度策略与时间管理

abstract 本文核心内容

  • cron 表达式完整解析
  • execution_date 的真实含义(很多人理解错了)
  • 数据区间(data_interval_start/end)的概念
  • 时区配置与常见陷阱
  • SLA 超时监控配置

cron 表达式详解

cron 表达式是 5 个字段,用空格分隔:

┌───────────── 分钟 (0-59)
│ ┌─────────── 小时 (0-23)
│ │ ┌───────── 日期 (1-31)
│ │ │ ┌─────── 月份 (1-12)
│ │ │ │ ┌───── 星期几 (0-7,0和7都表示周日)
│ │ │ │ │
* * * * *

常用 cron 表达式示例

表达式 执行时间 数据工程场景
0 2 * * * 每天凌晨 2 点 每日全量 ETL
0 */6 * * * 每 6 小时 6 小时增量同步
0 2 * * 1-5 工作日凌晨 2 点 只处理工作日数据
0 2 1 * * 每月 1 日凌晨 2 点 月报汇总
30 1 * * * 每天凌晨 1:30 分钟偏移,避免整点拥堵
*/15 * * * * 每 15 分钟 实时性要求高的数据同步
0 8 * * 1 每周一上午 8 点 周报推送
0 0 1 1 * 每年 1 月 1 日 年报汇总

cron 特殊字符

字符 含义 示例
* 任意值 * * * * * = 每分钟
, 枚举多个值 0 2,14 * * * = 凌晨 2 点和下午 2 点
- 范围 0 2 * * 1-5 = 周一到周五
/ 步长 */5 * * * * = 每 5 分钟

Airflow 预设别名

别名 等价 cron 含义
@once - 只运行一次
@hourly 0 * * * * 每小时
@daily 0 0 * * * 每天,按 DAG 时区的零点划分区间;默认时区配置为 UTC 且 DAG 未指定其他时区时为 UTC 零点
@weekly 0 0 * * 0 每周
@monthly 0 0 1 * * 每月
@yearly 0 0 1 1 * 每年
None - 不自动调度

tip 避免整点拥堵

如果你有多个 DAG 都在凌晨 2 点运行,它们会同时抢占 Worker 资源,导致相互等待。 建议给不同 DAG 加 5-15 分钟的偏移:0 2 * * *、5 2 * * *、15 2 * * *


execution_date 的真实含义

这是 Airflow 中最容易误解的概念,理解错了会导致数据处理逻辑的 bug。

常见误解

很多人以为:execution_date = 2024-01-15 表示"这个 DAG 是在 2024-01-15 运行的"。

这是错的。

正确理解

execution_date 是旧名称,对应逻辑日期(logical_date),不是实际执行时间;在本文讨论的 cron 数据区间调度中,它等于数据区间的开始时间,其他 timetable 不宜直接套用这一等式。

用一个例子说明:

DAG 配置:
  schedule_interval = "0 2 * * *"(每天凌晨 2 点)
  start_date = 2024-01-01

以下按 cron 数据区间调度列出边界,时间均使用同一 DAG 时区;实际启动还受调度器及资源可用性影响。

execution_date(逻辑日期) 最早可调度时间 数据区间(左闭右开)
2024-01-01 02:00 2024-01-02 02:00 [2024-01-01 02:00, 2024-01-02 02:00)
2024-01-02 02:00 2024-01-03 02:00 [2024-01-02 02:00, 2024-01-03 02:00)
2024-01-14 02:00 2024-01-15 02:00 [2024-01-14 02:00, 2024-01-15 02:00)

关键规律:

  • 在 cron 数据区间调度中,execution_date = 数据区间的开始时间
  • 数据区间结束后才具备调度条件;实际执行可能因暂停、资源排队等延后
  • 对于 @daily 的计划运行,数据区间通常对应 DAG 时区的一个自然日;任务是否处理该日数据,仍取决于查询逻辑和数据字段时区

因此,对于上述区间在 1 月 15 日凌晨 2 点结束的计划运行,{{ ds }} 的值是 2024-01-14,不是 2024-01-15;它由逻辑日期决定,不能仅凭实际运行时刻推断,也不能仅凭 ds 认定区间覆盖前一完整自然日。

def process_data(**context):
    ds = context["ds"]
    # 当前实际时间是 2024-01-15 02:00
    # ds = '2024-01-14'(逻辑日期的日期部分;实际处理范围仍需看查询边界)
    print(f"处理日期:{ds}")

note 为什么这样设计?

对于 @daily 的 cron 数据区间调度,1 月 15 日零点之后可处理 1 月 14 日的完整区间;但每天 02:00 的 cron 区间是前一天 02:00 到当天 02:00,并非前一自然日。 本文讨论的 cron 数据区间调度模型基于"处理一个已经完成的时间区间的数据",不代表所有 timetable。


数据区间概念

Airflow 2.2+ 引入了更清晰的 数据区间(Data Interval) 概念,用于明确处理范围;它并不替代逻辑日期这一概念,execution_date 对应的新名称是 logical_date。

变量 含义 示例(@daily 每日调度)
data_interval_start 数据区间的开始时间 2024-01-14 00:00:00
data_interval_end 数据区间的结束时间 2024-01-15 00:00:00
execution_date 等于 data_interval_start 2024-01-14 00:00:00
ds execution_date 的日期字符串 2024-01-14
next_ds 下一个调度日期 2024-01-15
def process_with_interval(**context):
    # 推荐使用数据区间来确定处理范围(更语义化)
    start = context["data_interval_start"]  # pendulum.DateTime 对象
    end = context["data_interval_end"]

    print(f"处理数据区间:{start} 到 {end}")

    # 查询这个时间区间的数据(左闭右开)
    sql = f"""
        SELECT *
        FROM orders
        WHERE create_time >= '{start}'
          AND create_time < '{end}'
    """

在 SQL 模板中使用

query_task = SQLExecuteQueryOperator(
    task_id="query_interval_data",
    conn_id="mysql_prod",
    sql="""
        SELECT *
        FROM orders
        WHERE create_time >= '{{ data_interval_start }}'
          AND create_time < '{{ data_interval_end }}'
    """,
)

时区配置

Airflow 默认使用 UTC 时间。这对于国内数据工程师是个大坑,因为我们的业务数据通常是 UTC+8(北京时间)。

配置 Airflow 时区

# airflow.cfg 或环境变量
# [core]
# default_timezone = Asia/Shanghai

# 环境变量方式
# AIRFLOW__CORE__DEFAULT_TIMEZONE=Asia/Shanghai

或在 docker-compose.yaml 中:

environment:
  AIRFLOW__CORE__DEFAULT_TIMEZONE: "Asia/Shanghai"

在 DAG 中指定时区

import pendulum

# 方式 1:在 DAG 中指定时区(推荐)
local_tz = pendulum.timezone("Asia/Shanghai")

with DAG(
    dag_id="china_daily_etl",
    schedule_interval="0 2 * * *",
    start_date=datetime(2024, 1, 1, tzinfo=local_tz),  # 带时区的 start_date
    catchup=False,
) as dag:
    pass

时区坑:@daily 在北京时间几点执行

配置 schedule_interval = "@daily"
等价于 "0 0 * * *"(每天 0 点)
Airflow 默认时区 = UTC

结果:若 DAG 未显式指定其他时区,区间在 UTC 0 点结束,即北京时间早上 8 点,届时最早具备调度条件;实际任务启动可能延后。

解决方案:
1. 用带 Asia/Shanghai 时区的 start_date 明确 DAG 时区:若仍用 @daily,则在北京时间零点结束区间;若需要北京时间凌晨 2 点,则使用 "0 2 * * *"。修改全局默认时区不会覆盖已有显式时区。
2. 或者让 DAG 明确使用 UTC 时区,再写 schedule_interval = "0 18 * * *"(UTC 18 点 = 北京时间次日 2 点)。

warning 时区是数据工程中最隐蔽的 bug 来源之一

建议团队统一以下规范:

  1. 所有 Airflow 实例统一使用 Asia/Shanghai 时区
  2. 数据库中的时间字段注明时区(或统一 UTC 存储,查询时转换)
  3. DAG 文件的 start_date 明确传入时区对象

SLA:任务超时监控

SLA(Service Level Agreement) 定义了任务必须在规定时间内完成,超时时触发告警。

from datetime import timedelta

# 方式 1:在 Task 级别设置 SLA
long_running_task = PythonOperator(
    task_id="heavy_computation",
    python_callable=run_computation,
    # Airflow 2.x 传统 SLA:在本文的 cron 计划运行中,以该区间结束的计划调度时刻加 3 小时作为期限,而非实际启动后 3 小时
    sla=timedelta(hours=3),
)

# 方式 2:在 DAG 级别设置 SLA Miss 回调
def sla_miss_callback(dag, task_list, blocking_task_list, slas, blocking_tis):
    """当任务超过 SLA 时触发此函数"""
    print(f"SLA 超时警告!")
    print(f"超时任务:{task_list}")

    # 发送自定义告警(如钉钉或飞书)
    send_dingtalk_alert(
        f"Airflow SLA 告警:任务 {task_list} 超过 SLA 时间!"
    )

with DAG(
    dag_id="monitored_etl",
    schedule_interval="0 2 * * *",
    start_date=datetime(2024, 1, 1),
    sla_miss_callback=sla_miss_callback,
    # DAG 级别默认 SLA(可被 Task 级别覆盖)
    default_args={
        "sla": timedelta(hours=4),
    },
) as dag:
    pass

任务超时 vs SLA 的区别

参数 触发条件 触发后行为
execution_timeout 单次任务执行超过指定时间 抛出超时异常;按重试配置进入重试或失败,不保证外部作业同步终止
sla Airflow 2.x 传统 SLA:本文 cron 计划运行中的任务未在区间结束的计划调度时刻加 SLA 时长之前成功完成 在 SLA 检查启用且满足触发条件时记录 SLA miss 并调用已配置的回调,不中止任务
# 同时设置两个:
task = PythonOperator(
    task_id="important_task",
    python_callable=run_task,
    execution_timeout=timedelta(hours=2),  # 单次执行超过 2 小时触发超时异常,后续按重试配置处理
    sla=timedelta(hours=1),                # 本文 cron 计划运行中,计划调度时刻后 1 小时仍未成功完成可产生 SLA miss
)

其他调度相关配置

depends_on_past:依赖上一次运行

default_args = {
    "depends_on_past": True,  # 通常要求上一逻辑运行的同名 Task 成功或跳过;首个实例及忽略历史依赖等情形需按版本和参数确认
}

使用场景:增量数据处理,约束同名任务实例按逻辑运行顺序执行;它不保证所有自然日都有运行,也不能代替数据完整性检查。

warning depends_on_past 的风险

如果某次任务失败且未恢复,后续依赖它的任务实例可能持续阻塞;可通过重试、清除后重跑等方式恢复,清除状态本身并不保证后续立即可运行。 在高频调度(如每小时)的 DAG 中谨慎使用。

max_active_runs:限制并发 DAG Run 数量

with DAG(
    dag_id="safe_etl",
    max_active_runs=1,  # 同一时刻只允许 1 个 DAG Run 执行
    # 防止补数据时大量 DAG Run 并发,压垮下游系统
) as dag:
    pass

手动补数据

当需要重新处理历史数据时:

# 命令行补跑指定日期范围
airflow dags backfill \
    --start-date 2024-01-01 \
    --end-date 2024-01-31 \
    my_etl_dag

# 清除指定日期的任务(重新运行)
airflow tasks clear my_etl_dag \
    --start-date 2024-01-15 \
    --end-date 2024-01-15

# 在 Web UI 中:
# 1. 进入 DAG 详情页
# 2. 切换到 Calendar/Grid View
# 3. 选择要重跑的日期
# 4. 点击 "Clear" 清除状态,Scheduler 会重新调度

小结

概念 关键点
execution_date 逻辑日期,不是实际执行时间;本文 cron 数据区间模型中等于区间开始。@daily 区间在 1 月 15 日结束的计划运行通常有 ds = 1 月 14 日
data_interval_start/end 推荐用来表示数据处理的时间范围,比 execution_date 更直观
时区 默认 UTC,国内建议配置 Asia/Shanghai;@daily 在 UTC 下是北京时间早上 8 点
SLA Airflow 2.x 传统 SLA 在满足检查条件时记录超期并触发已配置告警,不中止任务;execution_timeout 限制单次执行时长,触发超时异常后按重试配置处理
depends_on_past 依赖上一次运行成功,慎用于高频 DAG

下一篇:08-变量连接与配置管理 — 安全地管理数据源连接和配置参数


所属系列:00-Airflow学习路线图 | 上一篇:06-任务依赖与编排 | 下一篇:08-变量连接与配置管理

#Airflow #cron表达式 #execution_date #数据区间 #时区 #SLA #调度策略


info 前置知识

  • 理解 DAG 的 start_date 和 schedule_interval 基本概念,参考 第一个 DAG
  • 了解 cron 表达式基本格式(5 个字段的含义)
  • 知道 UTC 时区和北京时间(Asia/Shanghai)的关系

掌握检查

  • [ ] 能准确解释为什么在默认时区为 UTC 且 DAG 未显式指定其他时区时,@daily 计划运行最早在北京时间早上 8 点具备调度条件(UTC 00:00 = 北京 08:00),而实际启动可能延后
  • [ ] 能写出"每天凌晨 2 点半"、"工作日每小时整点"、"每月 1 日和 15 日零点"这三个 cron 表达式
  • [ ] 能解释 execution_date 和 data_interval_start 的区别,以及为什么 Airflow 2.2+ 推荐用后者
  • [ ] 知道 Airflow 2.x 传统 SLA miss 和 execution_timeout 的行为区别:前者记录超期并在配置后告警,不中止任务;后者触发超时异常,按重试配置进入重试或失败
  • [ ] 能说出 depends_on_past=True 的危险场景:某次同名任务实例失败且未恢复时,后续依赖它的任务实例可能持续阻塞;首个实例及忽略历史依赖等例外需按版本和参数确认

实践练习

基础题:写一个 DAG,调度频率为"工作日(周一到周五)每天凌晨 1:30",时区设置为 Asia/Shanghai,start_date 为 2024-01-01,catchup=False。在 Task 中打印 {{ data_interval_start }} 和 {{ data_interval_end }},验证时间范围是否符合预期。

进阶题:解释这个场景:分别将美国东部标准时(EST,UTC-5)和北京时间(UTC+8)的凌晨 2 点换算为 UTC;再讨论同一个 DAG 使用固定调度时区时,两地同事应如何表示同一计划调度时刻。美国东部夏令时为 EDT(UTC-4),使用 America/New_York 时还须考虑切换日的不存在或重复本地时间。如果团队协作时需要统一沟通调度时间,应该以哪个时区为基准?