本节摘要:调度器把 cron 翻译成数据区间,在区间右端已经过去之后才创建 DagRun;文件处理子进程隔离用户代码,避免解析把调度主循环拖死。理解“窗口闭合才合法”,才能解释到点不跑、以及 catchup 为何能瞬间制造出大量运行。
阅读完本节,你应当能够:
传统 cron 是墙上时钟到了就拉起进程。Airflow 把一次运行对应到逻辑区间。日调度的某次运行,处理的是那一天(或文档所称的左闭右开一段)的数据承诺,实际进程可能在区间结束后才启动。所以“schedule 写的是两点,为什么两点零一分还没跑”——因为两点可能是窗口边界,调度器还要完成解析、写库、依赖检查。更常见的误解是把 start_date 当天当成第一跑:第一窗口往往要等原点之后的第一个完整间隔结束。
原文把调度重构为受约束的时序图可达:横轴逻辑时间,纵轴任务在图中的位置,计算最早合法启动时刻。三个支柱:时间模型显式化、依赖是偏序、状态可在库中原子观察。这不是学术装饰,它直接导出:补数是补区间,不是补“昨天忘了点的按钮”。
墙上 02:00 只是窗口边界附近 │ ▼ 区间已经结束? → 才允许建 DagRun │ ▼ 任务依赖满足? → 才标 queued
手动触发绕过“下一个窗口”,但仍有一个逻辑日期。你在界面上选的运行日期,决定模板 ds 和数据区间,不决定函数里的 datetime.now()。这就是为什么手动重跑必须让代码读上下文而不是读墙钟。
| 你看见的现象 | 先查哪一列 | 不要先做 |
|---|---|---|
| 到点不跑 | 窗口是否闭合 | 重启调度器 |
| 图不出现 | 导入是否成功 | 改业务 SQL |
| 不建新 Run | 是否暂停 | 加 Worker |
| Run 在但任务不入队 | 依赖与池 | 改执行器 |
Scheduler 不在主线程里直接 exec 用户 DAG。它启动 DagFileProcessor 一类子进程去 import 文件,成功后再把序列化后的 DAG 信息写入 DagModel:dag_id、文件位置、是否暂停、调度表达式、标签、最近解析时间等。原文强调用户代码的死循环或泄漏不该带走调度服务。你在 DAG 顶层做重计算,伤害的是解析节奏,所有 DAG 的调度都会变钝。
解析失败进入导入错误,不进入“暂停”。两者都是“不跑”,原因完全不同。排障先看导入错误列表,再看 is_paused。
建 Run 的条件大致是:未暂停;存在已闭合且尚未创建的区间;catchup 为假时通常只关心最近的区间而不是全部历史。然后为每个任务生成实例,状态从无到 scheduled/queued,再按边推进。
推进不是每次把所有 DAG 全图盲扫到死。实现上会结合周期心跳与“有任务结束了”这类事件,对受影响的子图做检查。你可以把它理解成:任务成功是发令枪,催下游评估,而不是等下一轮整点巡视。传感器与可延迟任务则把“外部就绪”也变成事件。把所有等待都写成长 poke,会把调度器与 Worker 一起拖成轮询系统——那正是高级调度里 Dataset 与 deferrable 要打破的形态。
时区:naive datetime 依赖配置里的默认时区。夏令时切换日,cron 可能对不齐。统一用带时区的 start_date,或全公司锁定一个时区并写进规范。
短间隔:每分钟一个 DAG,解析和写库开销会接近任务本身。调度器会表现为 CPU 高、数据库时间高、任务却很简单。合并窗口,或把高频部分移出 Airflow。
catchup 与 start_date 过远:历史区间全部合法且闭合,于是一次性建海量 Run。原文金融客户案例是超 180 个并发运行抽干连接池。max_active_runs 能削峰,但建 Run 本身也写库,仍要克制。
depends_on_past 让时间轴上的实例互相卡住,调度器会正确地不放行,看起来像“调度坏了”。先看昨天同一任务是否 success。
⚠️ 常见坑:在任务里用当前时间判断“是不是周一”。补数星期四重跑周一窗口时,函数会以为今天星期四。用区间起始的星期几。
💡 关键直觉:调度器是时间语义翻译官加状态守门人,不是闹钟。它问的是“这个区间的账该不该入账”,不是“现在几点该不该响”。
SLA miss 在调度器观察时长超限时记账。它不自动改执行器行为。要杀长尾,用 execution_timeout。两者同时配:一个给人看合同,一个真正停手。
DagRun 的 conf 与逻辑日期一起构成这次运行的身份。API 触发时传入 conf,任务读 conf。不要把 conf 写成几兆 JSON,理由与 XCom 相同。
最后,调度器水平扩展不能替代数据库索引与解析减负。性能章会展开锁与查询;这里先立住:时间语义正确,比把扫描间隔从 30 秒改成 5 秒更优先。间隔改短只会让错误的 catchup 更快打满库。
写四列:墙上时间、窗口是否闭合、DAG 是否暂停、导入是否成功。只有四列都允许,才轮得到建 Run。缺任一列,调度器是对的。把这张表贴在值班文档,能消掉一半“调度坏了”工单。第五列才是依赖与池:Run 建了但任务不 queued。第六列才是执行器。人们总从第六列查起,所以总觉得 Airflow 神秘。
解析节奏:文件更新后不是瞬间出现在所有决策里,子进程要扫到、序列化、写入 DagModel。频繁保存半成品文件会造成导入错误闪烁。CI 同步应用整版本,不要让人在生产目录里实时编辑。last_parsed_time 一类字段能告诉你调度器是不是还在看旧图。排障“我改了重试次数怎么没生效”,先看解析时间,再看是不是改错了文件、调度器根本没扫到。
原文把调度从盲扫改成事件加周期。任务成功应尽快催下游,而不是等下一个整点心跳。若你观察到下游总是固定延迟一个扫描间隔,才去调间隔;否则先看是不是传感器在 poke、是不是依赖根本没满足。把扫描间隔调到极短,错误的 catchup 会更快打满库,这是用油门代替方向盘。
夏令时与时区:选一个业务时区写进规范,start_date 带时区或全员理解默认时区。跨区团队不要每人按笔记本本地时间读 ds。手动触发时界面选的日期是逻辑日期,必须和业务窗口一致,否则补的是另一天的账。
@daily 和 0 0 * * * 一样吗?常常等价于“按天切窗”,但原点小时取决于 start_date 与时区,不要假设一定是午夜。需要“每天两点切”就写 cron 两点,并把 start_date 的小时对齐。用自然语言预设是为了少写,不是为了模糊。模糊的窗口是补数事故的温床。
界面会让你选逻辑日期。你选的那一窗就是账。可以补一个尚未被计划创建的窗,也可以重跑旧窗。计划调度仍然遵守闭合规则。两者并存时,max_active_runs 仍然生效:手动补数会和在线窗口抢名额。所以补数要进单独池或单独时间,规范里写明,不要靠运气。
补数不是 catchup 的礼貌说法,是带范围的变更。写下起止窗口、并发、池、是否与在线重叠、失败是否继续后面的窗。排在业务低峰。max_active_runs 对补数仍然有效,必要时临时提高到经过计算的值,补完改回。代码必须读区间。补数日历与代码发版日历错开:先发幂等代码,再补历史,不要同一小时既改 SQL 又补三年。调度器会忠实执行你写下的合法闭合窗口,它没有“这次是补数请温柔”的情绪。温柔来自你的池与日历。原文金融案例是情绪为零的反面教材:原点过远加 catchup,调度器只是按合同办事。
主调度循环 CPU 打满、心跳变老、所有 DAG 推迟、导入错误列表闪烁、某份文件顶层在打外部 API。这一包同时出现,几乎肯定是解析副作用,不是业务变慢。处理:找到新上线的文件,暂停或移出目录,恢复心跳,再修文件。不要在症状包期间加 Worker。加 Worker 会让更多进程去问已经很忙的库。子进程隔离的意义在此显现:理想情况下坏文件只弄死处理器子进程,主循环仍在。若你的部署把解析与决策焊在一起,坏文件会带走一切。检查隔离是否真的存在,比背 DagModel 字段有用。字段用于排障“改了为何不生效”:看 last_parsed_time。时间不更新,调度器没吃到新图。吃到了仍旧行为,才去看是不是改错对象、是不是 Run 已按旧契约创建。已创建的 Run 不会因为你改了 retries 而时光倒流。新契约作用于新实例。
墙上时间、窗口是否闭合、是否暂停、导入是否成功,四列可以按 dag_id 人工勾,也可以做成检查命令的输出对照。对照失败不要先重启 Scheduler。重启会让解析风暴更难看。难看时先移出新文件。新文件常是症状包的源。源移走,心跳恢复,再修文件。修完再放回。放回后看 last_parsed_time。时间更新才算吃到。吃到后才看新窗是否按闭合规则创建。创建多了,查 catchup。创建没有,查暂停与闭合。闭合没到,等待。等待是正确。正确的等待不要被“到点了”的口语打断。口语来自 cron 思维。cron 思维在 1.1 清过,在 3.3 会复发。复发时拿出四列。四列是药。药要短。短才能在群里贴。群里贴四列,比贴重启截图有用。截图会引发更多重启。重启会制造幽灵。幽灵会让 3.2 背锅。背锅的执行器其实没坏。坏的是对时间语义的不耐烦。不耐烦会在夏令时那天爆发。爆发前用四列练习。练习在低峰。低峰练会了,高峰才不会用手去按调度器。手按调度器,是本章最想禁止的动作。禁止因为调度器是翻译官。翻译官正在算区间。算的时候被杀,区间会乱。乱了要修账。修账比等闭合贵。贵的操作后置。后置需要耐心。耐心来自理解闭合。理解来自本节。本节读完,应能向催促的人解释右端。解释不了,重读窗口那一节。窗口比心跳间隔重要。间隔可以调。窗口必须对。对了,到点不跑不再神秘。不神秘就可以值班。值班需要不神秘。神秘会催重启。重启会制造神秘。循环要打断。打断用四列。四列用完,3.3 毕业。毕业去部署。部署仍遵守闭合。闭合不因容器而改变。容器没有自己的时间语义。时间语义只在调度器。调度器只认区间。区间只认右端。右端过了才建 Run。建 Run 才有实例。实例才有绿。绿在最后。最后不要提前用重启换绿。换来的绿是假账。假账在补数时爆炸。爆炸形态见金融案例。案例的药就是四列加 catchup 关闭。药方在本节。照方抓。抓了少爆炸。少爆炸是时间语义的全部意义。意义不是理论。是少一次抽干连接池。连接池在库。库在 3.1。库怕补数。补数怕原点远。原点远怕 catchup。catchup 怕口语到点。口语到点怕四列缺失。缺失补上。补上就结束。
下一章把这些进程放到单机、虚拟机或集群上,并让它们活过故障。