- Published on
Airflow 2 EOL 之后 — 从 2 迁移到 3 的实际工作清单,以及走到 3.3 的 3.x 现状
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — 悄悄过去的 EOL
- 这不是升级,而是架构转变
- DAG 代码 — ruff 管一半,语义变化管另一半
- 最痛的部分 — 曾直接读元数据 DB 的自定义代码
- 部署者清单 — 这是一件有顺序的工作
- SLA 的五个月空窗期 — 移除先于替代方案到来
- 2.11 被设计成一座桥 — 还在 2.x 就从这里开始
- 3.x 的现状 — 2026 年 7 月,走到了 3.3.0
- 所以现在该做什么
- 结语
- 参考资料
引言 — 悄悄过去的 EOL
2026 年 4 月 22 日,Apache Airflow 2 迎来了 EOL(生命周期终止)。项目 README 的版本生命周期表给出的官方事实是这样的 — Airflow 2 从 2025 年 10 月 22 日起进入有限维护(仅安全和严重 bug 修复),到 2026 年 4 月 22 日状态变为 EOL。照搬 README 原话,EOL 版本不再获得任何修复,也不再获得任何支持。endoflife.date 的数据显示的也是同一个日期。
写这篇文章的今天(2026-07-17)算起,已经过去三个月。把日期排成一条时间线是这样的(均为在 GitHub Releases 和 PyPI 发行历史上核实过的日期)。
2020-12-17 Airflow 2.0.0 首个正式版
2025-04-22 Airflow 3.0.0 (移除 SLA、SubDAG、pickling、REST v1)
2025-05-20 Airflow 2.11.0 (桥接版本 — 回移植迁移工具)
2025-09-25 Airflow 3.1.0 (Deadline Alerts 以 experimental 姿态抵达)
2025-10-22 Airflow 2.x 进入有限维护 (仅安全、严重 bug 修复)
2026-03-14 Airflow 2.11.2 (截至目前最后一个 2.x 补丁)
2026-04-07 Airflow 3.2.0 (asset partitioning、multi-team)
2026-04-22 Airflow 2.x EOL (此后不再有任何修复)
2026-07-06 Airflow 3.3.0 (state store、可插拔重试、Java/Go SDK)
对一个存活了 5 年 4 个月的主版本来说,这是一次安静的退场。而对现在还留在 2.x 上的团队来说,这些日期意味着一件很简单的事 — 下一个 CVE 出现时,原则上不会再有 2.x 的补丁。问题在于,从 2 到 3 的路并不短。本文不加渲染地梳理这条路上真正需要动手的工作,依据的是官方升级指南和发行说明的原文。
这不是升级,而是架构转变
2 和 3 的区别可以浓缩成一句话 — 在 2 里,每个组件都直接挂在元数据 DB 上;在 3 里,API 服务器是任务和 worker 唯一的接触点。
升级指南自己列出的 2.x 的问题是坦率的。任务代码和任务执行代码曾经跑在同一个进程里,worker 直接挂在 DB 上执行所有用户代码,用户代码可以导入 DB session 从而对元数据 DB 做恶意操作,DB 连接数过多导致难以扩展。Airflow 3 用 Task Execution API 切断了这一切 — 状态迁移、心跳、XCom、资源查询全部经过专门的 API,任务代码不能再触碰 DB session。
这个转变是迁移工作量的根源。DAG 文件里的导入路径修复,工具能自动完成一半左右,但「worker 里那些原本直接读元数据 DB 的代码」需要改设计。后文会单独讨论这一点。
有个细微之处也照文档原样搬过来 — DAG 作者代码在 Dag 文件处理器和 triggerer 内部,仍然可能以直接访问 DB 的状态运行。隔离针对的是任务执行路径,还不是所有路径。
DAG 代码 — ruff 管一半,语义变化管另一半
机器能帮你抓住的部分
官方指南首先引导使用的工具是 Ruff 的 AIR 规则。AIR301 和 AIR302 指出在 Airflow 3 上会跑不通的代码,AIR311 和 AIR312 指出现在还能跑、但应该迁移的代码。文档要求 ruff 版本至少 0.13.1。
ruff check dags/ --select AIR301 --show-fixes # 预览会怎么改
ruff check dags/ --select AIR301 --fix # 安全的自动修复
ruff check dags/ --select AIR301 --fix --unsafe-fixes # 连导入路径替换也一起做
这里不需要被 unsafe 这个词吓到 — 文档解释说,在 AIR 规则里,unsafe 修复大多数时候只是指「导入的成员名保持不变,只改路径」这样的修改。
导入迁移的方向只有一个。用来编写 DAG 的一切都会迁到 airflow.sdk 命名空间下。
airflow.models.dag.DAG -> airflow.sdk.DAG
airflow.models.baseoperator.BaseOperator -> airflow.sdk.BaseOperator
airflow.decorators.task -> airflow.sdk.task
airflow.datasets.Dataset -> airflow.sdk.Asset (连名字都变了)
airflow.models.variable.Variable -> airflow.sdk.Variable
airflow.models.connection.Connection -> airflow.sdk.Connection
airflow.hooks.base.BaseHook -> airflow.sdk.BaseHook
官方时间线说得很明确 — 在 3.1 里旧的导入仍然能用,只是会发出警告,未来版本会移除它们。现在就修是对的。
还有一点,像 BashOperator、PythonOperator、ExternalTaskSensor、FileSensor 这类原本捆绑在核心里的常用算子,已经被拆到一个叫 apache-airflow-providers-standard 的独立包里。这个包在 2.x 上也能安装,所以可以在升级之前,提前把导入换掉,作为一项先行工作。
机器抓不住的部分 — 语义变了
比导入更危险的,是那些照样能跑、但跑法不一样的变化。这里只从升级指南的 Breaking Changes 一节中,挑出实务中真正会踩到的部分。
execution_date 及其派生键从上下文中消失了。 execution_date、prev_execution_date、next_execution_date、prev_ds、next_ds、tomorrow_ds、yesterday_ds 这一整个系列都在移除清单上。在模板或任务代码里引用这些键会导致 DAG 出错。替代方案是 logical_date 以及 data interval 系列。
cron 字符串的默认语义变了。 随着 create_cron_data_intervals 配置的默认值变成 False,像 schedule="0 0 * * *" 这样直接传入 cron 字符串的 DAG,现在会被 CronTriggerTimetable 而不是 CronDataIntervalTimetable 解释。显式传入 timetable 实例的 DAG 不受影响。问题在于 data_interval_start、ds、ts 这些模板值是从 logical_date 派生出来的,而这些值在两种 timetable 之间会发生偏移。如果你依赖这些值,文档的指示是在升级前显式设置 create_cron_data_intervals=True;要是在 3.x 的运行已经产生之后才把这个标志改回去,为了避免和前一次运行的 logical_date 冲突,会跳过一次计划中的运行。
不要假设手动触发运行的 data interval。 文档明确指出,在 3 里不应假设手动触发的运行,其 data_interval 是从用户传入的 logical_date 派生出来的。如果需要用户指定的日期,必须直接读取 logical_date — 这一点在使用 TriggerDagRunOperator 触发下游 DAG 的工作流里尤其容易踩到。
xcom_pull() 的默认行为变了。 在 2 里,不带 task_ids 调用会在整个 DAG 运行范围内查找该键的最新值,而在 3 里只在当前任务内查找。如果需要其他任务的 XCom,现在必须传入 task_ids。这类代码会开始悄悄地收到 None,所以提前用 grep 排查一遍会更稳妥。
catchup_by_default 变成了 False。 如果你依赖 catchup 来补齐调度空档,现在需要在 DAG 上显式声明。
再加上功能移除清单 — SubDAG(由 TaskGroup 和 asset 调度取代)、SequentialExecutor、CeleryKubernetesExecutor 和 LocalKubernetesExecutor(由多 executor 配置取代)、DAG/XCom 的 pickling、CLI 的 --subdir 参数(由 Dag bundle 取代),以及 REST API v1,全都消失了。API 客户端需要迁移到基于 FastAPI 的 v2。
最痛的部分 — 曾直接读元数据 DB 的自定义代码
如果你的团队有自定义算子或任务代码,曾经通过 Airflow 的 DB session 直接读写元数据,这就是迁移工作量里最大的一项。这类代码在 3 上无法运行,官方指南给出了两条路径。
推荐路径是通过 Airflow Python Client 使用 REST API。 DagRun、TaskInstance、Variable、Connection、XCom 等大多数使用场景都被 API 覆盖了。不过文档也如实写下了它的缺点 — 你得通过调用 /auth/token 获取令牌并做轮换,会因此依赖 API 服务器的可用性和网络路径,而且并非所有 DB 操作都以 API 端点的形式暴露出来。社区的方针是:缺失的功能应该去申请补充 API,而不是退回直接访问 DB。
变通做法是用 PostgresHook 这类 DbApiHook,对元数据 DB 建立一个普通的 DB 连接,但文档自己明确写着不推荐这么做。 理由也说得很明确 — 这种方式会在 Airflow 3.2 之后的版本中失效,元数据 DB 的 schema 不是公开 API,可能毫无预兆地改变,它和任务隔离这个 3 的核心设计相冲突,还会退回到 2 时代那种每个任务都要开一次 DB 连接的性能特征。就算临时使用,也应该用只读账号,并配上迁移计划。真实的转换案例集中在 issue #49187 里。
从估算工作量的角度给一条建议:在给迁移报价之前,先数清楚两个数字 — ruff AIR301 违规的数量,以及直接操作 DB session 的自定义算子数量。前者大多能机械地解决,后者每一个都是一次设计变更。
部署者清单 — 这是一件有顺序的工作
运维这一侧的工作,照着指南的步骤走大体就行,但有几点需要提前知道,否则时间表会被打乱。
airflow config update # 检查哪些地方需要改 (也回移植到了 2.11)
airflow config update --fix # 自动修复
airflow db migrate # schema 迁移 — 耗时最长的一步
airflow api-server # 不再有 webserver 命令
airflow dag-processor # 现在必须单独启动 (本地开发也一样)
- 前置条件。 最低要求 Airflow 2.7,推荐的路径是经过最新的 2.x 再走到 3。
airflow dags reserialize必须无错误运行,DAG 处理相关的错误需要在升级前的旧版本上先解决掉。 - 清理 DB 和备份。 schema 变更所需的时间和 DB 大小成正比。文档建议先用
airflow db clean清理掉过期的 XCom 等数据再开始,并且警告说如果没有备份就贸然进行、迁移中途又中断,可能会陷入迁移到一半的状态。 - 组件构成会变。 webserver 变成了一个通用的 API 服务器,Dag 处理器必须始终作为独立进程启动。如果使用 Helm chart,webserver 下的所有值都需要迁到 apiServer 下,而且在 chart 1.16.0 到 1.18.0 之间有不少键名发生了变化。
- 认证方式会变。 默认的 auth manager 变成了 Simple Auth,如果要保留基于 FAB 的认证,需要安装 FAB provider 并显式指定 auth_manager。OAuth 重定向 URL 会带上
/auth前缀 — 也就是说,注册在 IdP 那一侧的 redirect URL 也需要同步修改。 - 插件。 依赖 Flask-AppBuilder 视图和蓝图的插件,需要迁到 FastAPI 应用上,或者搭在 FAB provider 的兼容层上。
SLA 的五个月空窗期 — 移除先于替代方案到来
这次主版本转变里最有教训意义的事件是 SLA。3.0.0(2025-04-22)移除了 SLA 的回调和指标,同时在发行说明里写道「一个更灵活的替代机制 DeadlineAlerts 计划在未来版本中推出」。那个 Deadline Alerts(AIP-86)直到 3.1.0(2025-09-25)才抵达,而且还带着 experimental 标记,只支持异步回调。同步回调(SyncCallback)在 3.2.0(2026-04-07)才被加入,即便到了 3.2.0 的发行说明,也依然标着 experimental。3.3.0 在 Browse 菜单里加入了 Deadlines 页面。
归纳一下 — 对早早迁到 3.0 的 SLA 用户来说,以官方原生功能为基准,存在一个五个月的空窗期;而替代方案抵达至今已经十个月,却依然贴着 experimental 的标签在慢慢成熟。3.0 发行说明给出的临时对策,是任务级别的成功/失败钩子或外部监控。对深度依赖某个特定功能的团队来说,这也是一个提醒 — 在规划主版本升级时,需要养成直接去发行说明里核实「那项功能的替代方案在哪个版本、处于什么状态」的习惯。
2.11 被设计成一座桥 — 还在 2.x 就从这里开始
2.11.0(2025-05-20)与其说是新功能版本,不如说是一个过渡装置。发行说明「Ease migration to Airflow 3」一节揭示的内容是这样的。
airflow config lint和airflow config update被回移植到了 2.11,可以在迁到 3 之前提前检查、修正配置。- 所有原本使用
execution_date的模型,都并行加上了logical_date字段。3 会彻底丢弃execution_date。 - 提前翻转
create_delta_data_intervals标志(2.11 默认 True,3.0 默认 False),可以让你在还在 2.x 上时,就提前体验 timedelta 调度的新解释方式(DeltaTriggerTimetable)。这和 cron 那边的create_cron_data_intervals是同一种彩排装置。 - 用来统一计时指标单位的
timer_unit_consistency也是同样的模式 — 在 2.11 里可以先打开试试,到 3.0 就会一直处于开启状态。
也就是说,官方路径很清楚:升到最新的 2.11.x,提前把标志往 3.0 的默认值方向翻转,用 ruff 和 config lint 把违规消化掉,然后再迁到 3。由于 2.11 支持 Python 3.9 到 3.12,Python 版本的对齐工作也可以在这一步顺便解决。
3.x 的现状 — 2026 年 7 月,走到了 3.3.0
迁过去之后的世界,也需要诚实地看一看。有两点很重要。
第一,即便在 3.x 内部,追踪最新的小版本也几乎是硬性要求。 从发行历史看,3.0.x 的最后一个补丁是 3.0.6(2025-08-29),正好在 3.1.0 之前结束;3.1.x 是 3.1.8(2026-03-11),在 3.2.0 之前结束;3.2.x 是 3.2.2(2026-05-29),在 3.3.0 之前结束。为上一个小版本发补丁,会在下一个小版本发布后事实上停止,这是一个固定模式,README 也建议使用当前主版本下最新的小版本。没有类似 LTS 的东西。这意味着大约每半年一次的小版本升级,需要被排进运维日历里。
第二,代码的迁移在 3.x 内部依然在继续。 3.2.0 把任务使用的异常迁到了 airflow.sdk.exceptions(原有的 airflow.exceptions 导入作为会发出警告的代理保留了下来),序列化(serde)逻辑也迁到了 Task SDK — 文档明确写着这两层兼容层都计划在 Airflow 4 中移除。从 2 迁到 3 并不代表搬家结束了,向 airflow.sdk 的迁移仍在进行中。把这部分后续工作也纳入迁移预算,是比较现实的做法。
功能这一侧的脉络只简单带过。3.1(2025-09-25)带来了 Deadline Alerts、Human-in-the-Loop、UI 多语言化;3.2(2026-04-07)的头条是 asset partitioning — 不需要整个 asset 变化,只靠特定分区的变化就能触发下游 DAG,和按日期分区的数据湖很搭 — 以及 multi-team 部署(在一个 Airflow 实例内按团队隔离 DAG、连接、池,标记为 experimental)。3.3.0(2026-07-06)用 RollupMapper、FanOutMapper 这类 mapper 和 wait_policy 扩展了那套分区能力,让重试策略变得可插拔(AIP-105),加入了任务/asset 状态存储(AIP-103),并以 experimental 姿态搭载了一个可以用非 Python 语言编写任务的 Coordinator 层(AIP-108)— Java SDK 在 2026-07-13 打上了 1.0.0-beta1 的标签。相当一部分新功能都带着 experimental 标记,这一点照实读就好。这些功能本身不是需要着急的理由,真正需要着急的理由,始终只是 2.x 的 EOL。
所以现在该做什么
按情况分类整理如下。
如果还在运行 2.x。 你正在用调度器权限运行一套没有安全补丁的软件。第一步不是 3,而是 2.11.2 — 在那里先做三项测量:airflow config lint、ruff AIR301/AIR302,以及直接访问 DB 的代码清单。没有这些测量,这次迁移的估算根本无从谈起。像把标准算子切换到 provider 包这类可以在 2.x 上先做的工作,也可以在这个阶段一并完成。
如果深度依赖 SLA、SubDAG 或直接 DB 访问。 直接在发行说明里核实每个功能替代方案的状态,先把迁移设计做出来。SLA 需要向 Deadline Alerts 重新设计(包括它至今仍标着 experimental 这一点),SubDAG 需要改成 TaskGroup、asset 调度,DB 访问需要重写为基于 Python Client。如果没有这三项,迁移会比想象中更机械化。
如果已经在 3.x 上。 建立一个追踪最新小版本的半年周期日历,现在就着手消化 airflow.exceptions 和旧 serde 路径产生的 DeprecatedImportWarning — 这些都是已经公告会在 Airflow 4 中移除的项目。
最后做一个层次上的区分。Airflow 是批数据流水线的控制面,而应用代码的持久执行(durable execution)是另一个层次的问题 — 那个话题在Temporal Worker Versioning GA一篇里讨论过。编排器所指挥的执行引擎一侧的近期变化,可以参考PySpark 4.2 默认启用 Arrow UDF一篇;整个编排工具版图的对比,可以参考数据编排指南和工作流引擎 2026两篇。
结语
归纳一下。Airflow 2 已在 2026 年 4 月 22 日终止,最后一个补丁是 2.11.2(2026-03-14)。从 2 到 3 的路不是导入替换,而是架构转变 — 核心是移除 worker 对 DB 的直接访问,在此之上还叠加着 execution_date 系列移除、cron 语义变化、xcom_pull 行为变化这样的语义地雷。官方工具(ruff 的 AIR 规则、config lint/update、2.11 这座桥)能相当程度地削减机械性的部分,但直接访问 DB 的代码和对 SLA 的依赖,只能靠重新设计来解决。
而 3.x 不是终点,是一列还在开的火车 — 小版本大约每半年发一次,上一个小版本的补丁会停下来,向 airflow.sdk 的迁移仍在继续,并伴随着 4.0 会移除的预告。所以需要两份计划 — 一份是从 2 迁到 3 的一次性项目,另一份是迁过去之后追踪最新小版本的日常运维节奏。前者可以拖延的时间,已经在这个 4 月结束了。
参考资料
- Upgrading to Airflow 3 — 官方升级指南
- Apache Airflow 发行说明 (3.0.0-3.3.0、2.11.x)
- apache/airflow README — Version Life Cycle 表
- apache/airflow GitHub Releases
- apache-airflow PyPI 发行历史
- endoflife.date — Apache Airflow
- Ruff AIR 规则 (Airflow 迁移 lint)
- apache/airflow-client-python — 官方 Python 客户端
- issue #49187 — 直接 DB 访问代码的转换案例集