Data Pipelines with Apache Airflow机翻-6.触发工作流程

6.触发工作流程

本章内容包括:

  • 等待传感器满足某些条件
  • 确定如何设置不同DAG中任务之间的依赖关系
  • 通过CLI和REST API执行工作流程

在第三章中,我们探讨了如何基于时间间隔来安排工作流。时间间隔可以作为方便字符串给出,例如“@daily”、 time delta 对象,例如 timedelta (days = 3) ,以及 cron 字符串,例如“3014 * * *”,在每天14:30触发。这些都是指示工作流在特定时间或间隔触发的符号。 在给定间隔的情况下,Airflow将计算下一次运行工作流的时间,并在下一个日期和时间启动工作流中的第一个任务。

在本章中,我们将探讨触发工作流的其他方法。与基于时间的时间间隔不同,通常需要遵循特定的操作,而基于时间的时间间隔会在预定义的时间启动工作流。触发器动作通常是外部事件的结果; 想象一下一个文件被上传到一个共享驱动器,一个开发人员把他的代码推送到一个存储库,或者一个 Hive 表中存在一个分区,这可能是开始运行你的工作流的一个原因

6.1 带传感器的轮询条件

启动工作流的一个常见用例是新数据的到来——想象一下,一个第三方每天将其数据转储到您的公司和第三方之间的共享存储系统上。 假设我们正在开发一种流行的移动优惠券应用程序,并与所有超市品牌联系,以提供其促销活动的每日出口,并显示在我们的优惠券应用程序中。目前,促销活动主要是手动过程-大多数超市都聘用价格分析师来考虑许多因素并提供准确的促销活动。 有些促销活动是提前几周深思熟虑的,而有些促销活动是自发的一日促销。 定价分析师会仔细研究竞争对手,有时会在深夜进行促销。 因此,每日促销数据通常在随机时间到达。我们已经看到数据在第二天的下午4点到凌晨2点之间到达共享存储,尽管每天的数据可以在一天中的任何时间传递。

让我们为这种工作流程开发初步的逻辑:

图6.1处理超市促销数据的初始逻辑

img

在此工作流程中,我们将超市1至4交付的数据复制到我们自己的“原始”存储中,我们可以始终从中复制结果。然后,process_ {1,2,3,4}任务将所有原始数据转换并存储在结果数据库中,应用程序可从该数据库中读取原始数据。 最后,create_metrics任务计算并汇总了许多指标,这些指标可为促销提供见解,以进行进一步分析。

由于来自超市的数据在不同的时间到达,因此此工作流程的时间表可能如下所示:

图6.2处理超市促销数据的时间表

img

在这里,我们可以看到超市的数据传递时间和工作流程的开始时间。因为我们曾经有过超市在凌晨2点才发送数据的经历,所以一个稳妥的做法是在凌晨2点开始这个工作流程,以确保所有的超市都已经发送了数据。但是,这导致大量时间等待。 超级市场1在16:30交付了数据,而工作流程在凌晨2点开始处理,而在9.5个小时内什么也不做。

图6.3带有等待时间的超市促销工作流时间表

img

在Airflow中解决此问题的一种方法是借助sensorssensors是一种特殊的operators类型(子类)。传感器不断轮询某些条件是否成立,如果满足,则成功。 如果为false,则sensors将等待并重试,直到条件为真或最终达到超时为止:

Listing 6.1

from airflow.contrib.sensors.file_sensor import FileSensor
 
wait_for_supermarket_1 = FileSensor(
   task_id="wait_for_supermarket_1",
   filepath="/data/supermarket1/data.csv",
)

FileSensor [16]将检查/data/supermarket1/data.csv是否存在,如果文件存在,则返回True。 如果不是,它将返回False,并且sensor 将等待给定的时间段(默认为60秒),然后重试。operators(sensors 也是operators)和DAG都具有可配置的超时,并且sensors 将一直检查情况,直到达到超时为止。 我们可以在任务日志中检查传感器的输出:

[2019-10-13 11:39:09,757] {file_sensor.py:60} INFO - Poking for file /data/supermarket1/data.csv
[2019-10-13 11:40:09,752] {file_sensor.py:60} INFO - Poking for file /data/supermarket1/data.csv
[2019-10-13 11:41:09,713] {file_sensor.py:60} INFO - Poking for file /data/supermarket1/data.csv
[2019-10-13 11:42:09,714] {file_sensor.py:60} INFO - Poking for file /data/supermarket1/data.csv
[2019-10-13 11:43:09,711] {file_sensor.py:60} INFO - Poking for file /data/supermarket1/data.csv

在这里,我们看到sensor 大约每分钟一次[17],为给定文件的可用性“pokes”。 “pokes”是Airflow的名称,用于运行sensor 并检查sensor 状况。

将sensor 纳入此工作流程时,应进行一次更改。 现在我们知道我们不会等到凌晨2点才假定所有数据可用,而是开始连续检查数据是否可用,DAG的开始时间应该放在数据到达边界的起点:

图6.4带有sensor 的超市促销时间表

img

相应的 DAG 将在处理每个超市数据的开始时添加一个任务(FileSensor) ,它看起来如下:

图6.5气流中带有Sensor的超市促销DAG

img

在图6.5中,在DAG的开始处添加了Sensor,并将DAG的schedule_interval设置为在预期的数据传递之前开始。 这样,在DAG的开始处的Sensor将连续轮询数据的可用性,并在满足条件时(即在给定路径中数据可用时)继续执行下一个任务。

这里我们看到超市 # 1已经发送了数据,该数据将其相应的Sensor的状态设置为成功,并继续处理其下游任务。 结果,数据在交付后直接处理,而不必不必要地等待到预期的交付时间结束。

6.1.1 轮询自定义条件

一些数据集的大小很大,并且包含多个文件,例如 data-01.csv,data-02.csv,data-03.csv等。虽然Airflow的FileSensor确实支持通配符以匹配例如 data-*.csv,但它将匹配任何与模式匹配的文件。如果当第一个文件data-01.csv被提交,超市还在将其他文件上传到共享存储中时,FileSensor将返回True,工作流将继续执行copy_to_raw任务,这是不可取的。

因此,我们与超市达成了协议,写了一个名为_SUCCESS的文件作为上载的最后一部分,以指示每天的完整数据集已被上载。数据团队决定他们要检查是否存在一个或多个名为data-*.csv的文件和一个名为_SUCCESS的文件。FileSensor在后台使用globbing [18]将模式与文件或目录名称进行匹配。尽管通配(类似于正则表达式,但功能受到更多限制)可以将多个模式与一个复杂的模式进行匹配,但更具可读性的方法是使用PythonSensor [19]来实现这两个检查。

在提供了可调用的Python(函数/方法/等)来执行的意义上,PythonSensor与PythonOperator相似。然而,PythonSensor的可调用仅限于返回布尔值。 如果为True表示成功满足条件,则为False表示未成功满足条件。 我们来检查一下这两个条件,看看PythonSensor可调用项:

Listing 6.2

from pathlib import Path
 
from airflow.contrib.sensors.python_sensor import PythonSensor
 
def _wait_for_supermarket(supermarket_id):
   supermarket_path = Path("/data/" + supermarket_id)
   data_files = supermarket_path.glob("data-*.csv")
   success_file = supermarket_path / "_SUCCESS"
   return data_files and success_file.exists()
 
 
wait_for_supermarket_1 = PythonSensor(
   task_id="wait_for_supermarket_1",
   python_callable=_wait_for_supermarket,
   op_kwargs={"supermarket_id": "supermarket1"},
   dag=dag,
)

提供给PythonSensor的callable被执行,并期望返回布尔True或False。 上面显示的可调用对象现在检查两个条件:

img

除了PythonSensor任务的颜色不同之外,它们在UI中的外观也相同:

图6.6使用PythonSensors定制条件的超市促销DAG

img

6.1.2 Sensors outside the happy flow

既然我们已经看到Sensors运行成功,那么如果某天超市不再发送数据会发生什么呢? 默认情况下,Sensors将像操作员一样发生故障:

图6.7超出最大时间范围的传感器将发生故障

img

Sensors接受超时参数,该参数保持Sensors允许运行的最大秒数。如果在下一次测试开始时,运行秒数高于设置的超时数,Sensors将失效:

[2019-10-13 11:58:50,001] {python_sensor.py:78} INFO - Poking callable: <function wait_for_supermarket at 0x7fb2aa1937a0>
[2019-10-13 11:59:49,990] {python_sensor.py:78} INFO - Poking callable: <function wait_for_supermarket at 0x7fb2aa1937a0>
[2019-10-13 11:59:50,004] {taskinstance.py:1051} ERROR - Snap. Time is OUT.
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/airflow/models/taskinstance.py", line 926, in _run_raw_task
    result = task_copy.execute(context=context)
  File "/usr/local/lib/python3.7/site-packages/airflow/sensors/base_sensor_operator.py", line 116, in execute
    raise AirflowSensorTimeout('Snap. Time is OUT.')
airflow.exceptions.AirflowSensorTimeout: Snap. Time is OUT.
[2019-10-13 11:59:50,010] {taskinstance.py:1082} INFO - Marking task as FAILED.

默认情况下,sensor 超时设置为7天。 如果将DAG schedule_interval设置为每天一次,则将导致意外的滚雪球效应(令人惊讶的是,许多DAG都容易遇到这种效应!)。DAG每天运行一次,超级市场2、3和4将在7天后失效,如图6.7所示。环图每天运行一次,超市2、3和4在7天后就会失效,如图6.7所示。然而,每天都会增加新的 DAG 运行,这些日子的sensor 都会启动,因此越来越多的任务开始运行。问题是,Airflow可以处理并可以运行(在各个级别上)的任务数量是有限制的。

图6.8传感器死锁——正在运行的任务是所有等待条件为真的传感器,该条件永远不会发生,因此占用所有槽。

img

重要的是要了解Airflow中各个级别上正在运行的任务的最大数量是有限的。这一点很重要; 每个 DAG 的任务数量、全局层次上的任务数量、每个 DAG 的运行数量等等。在图6.8中,我们看到16个正在运行的任务(全部是sensors)。DAG类有一个并发参数,用来控制DAG中允许同时运行多少个任务:

img

在图6.8的例子中,我们用所有缺省值运行 DAG,即每个 DAG 有16个并发任务。接下来的雪球效应发生了:

  • 第1天:超市1成功,超市2、3和4轮询,占用3个任务

  • 第2天:超市1成功,超市2、3和4轮询,占用6个任务

  • 第3天:超市1成功,超市2、3和4投票,占用9个任务

  • 第4天:超市1成功,超市2、3和4投票,占用12个任务

  • 第5天:超市1成功,超市2、3和4投票,占用15个任务

  • 第6天:超市1成功完成,超市2、3和4进行了轮询,占用了16个任务,有2个新任务无法运行,并且其他任何试图运行的任务都被阻止

这种行为通常称为“sensors死锁”。在此示例中,达到了超市优惠券DAG中运行任务的最大数量,因此影响仅限于该DAG,而其他DAG仍然可以运行。但是,一旦达到最大任务的全局Airflow 限制,您的整个系统就会停顿,这显然是非常不可取的! 该问题可以通过多种方式解决。

Sensor 类采用一个参数模式,可以设置为“ poke”或“ redatation”(从 Airflow 1.10.2开始就可以使用)。默认情况下,它被设置为“ poke”,导致阻塞行为。这表示; 只要传感器任务处于运行状态,它就会占用一个任务槽。偶尔它会戳条件,然后什么也不做,但仍然占用一个任务插槽。sensor “reschedule”模式在戳完之后释放插槽,因此仅在实际工作时才占用插槽。 让我们来看看:

图6.9模式为“ rechedched”的传感器在戳后释放其插槽,从而允许其他任务运行

image-20210512145942776

并发任务的数量还可以通过全局Airflow级别上的几个配置选项来控制,这些选项在第12.6节中进行了介绍。 在下一部分中,让我们看一下如何将单个DAG拆分为多个较小的DAG,它们相互触发以分离关注点。

6.2 触发其他DAG

随着时间的推移,更多的超市会加入到我们的优惠券服务中来。越来越多的人希望对超市的促销活动有深入的了解,而在所有超市的所有数据都已交付并处理之后,最后每天只执行一次create_metrics步骤。在当前设置中,它取决于process_supermarket_ {1,2,3,4}任务的成功状态:

图6.10特定于超市的任务与create_metrics任务之间的执行逻辑不同,表明在单独的DAG中可能存在分裂

img

我们收到了分析师团队的疑问,这些指标是否也可以在处理后直接提供,而不必等待其他超市传递其数据并通过管道运行它们。 我们在这里有几种选择。 (取决于它执行的逻辑),我们可以在每个process_supermarket_ *任务之后将create_metrics任务设置为下游任务:

图6.11复制任务以避免等待所有进程完成

img

假设create_metrics任务演变为多个任务,从而使DAG结构更加复杂,并导致了更多“重复”任务:

图6.12更多的逻辑再次表明在单独的DAG中可能的分裂

img

避免(几乎)具有相同功能的重复任务的一种方法是将DAG拆分为多个较小的DAG,其中每个DAG负责全部工作流程的一部分。这样做的一个好处是,你可以从 DAG # 1多次调用 DAG # 2,而不需要从 DAG # 2单独调用一个 DAG 来执行多个(重复的)任务。 这是否可行取决于许多因素,例如工作流程的复杂性。例如,如果您希望能够创建指标,而不必等待工作流按照其时间表完成,而是随时手动触发它,那么将它分割为两个独立的DAGs是有意义的。

可以使用TriggerDagRunOperator实现此方案。 此运算符允许触发其他DAG,您可以将其应用于工作流的解耦部分:

img

提供给TriggerDagRunOperator的trigger_dag_id参数的字符串必须与DAG的dag_id匹配才能触发。 最终结果是,我们现在有两个DAG,一个DAG用于从超市获取数据,另一个DAG用于计算数据的指标:

图6.13 DAG分为两部分,其中DAG#1使用TriggerDagRunOperator触发DAG#2。 现在仅定义一次DAG#2中的逻辑,从而简化了图6.12所示的情况。

img

视觉上,在Airflow UI中,计划的DAG,手动触发的DAG或自动触发的DAG几乎没有区别。 树状视图中的两个小细节告诉您DAG是由计划触发还是启动。 首先,计划的DAG运行,任务实例显示黑色边框:

图6.14黑色边框表示计划的运行,没有黑色边框的择时触发

img

其次,每个DAG运行都包含一个字段“ run_id”。 run_id的值以以下任意一个开头:

  • scheduled__ 指示因时间表而启动的DAG

  • trig__ 指示由TriggerDagRunOperator启动的DAG

  • manual__ 指示DAG运行是通过手动操作开始的(即,按下“ Trigger Dag”按钮)

将鼠标悬停在DAG运行圆圈上会显示一个工具提示,其中显示run_id值,告诉我们DAG如何开始运行。

图6.15 run_id告诉我们DAG运行的来源

img

6.2.1 使用TriggerDagRunOperator回填

如果你改变了process_任务中的一些逻辑,并希望从那里重新运行dag,该怎么办?在单个DAG中,您可以清除process_的状态和相应的下游任务。但是,清除任务只清除同一DAG中的任务!在另一个DAG中,TriggerDagRunOperator下游的任务不会被清除,所以要清楚地意识到这种行为。

图6.16清除TriggerDagRunOperators不会清除触发的DAG中的任务,而是会创建新的DAG运行

image-20210512151601306

6.2.2 轮询其他DAG的状态

只要“triggering” DAG之间没有依赖关系,图6.13中的示例就可以工作。 换句话说,图6.13中的例子类似于一对一的关系,DAG 保存了 TriggerDagRunOperator 和 DAG 之间的触发关系,如图6.16左边所示。

可以将第一个DAG进一步拆分为多个DAG,并可以为每个相应的DAG做出相应的TriggerDagRunOperator任务,如图所示。同样,使用TriggerDagRunOperator可能会触发一个DAG触发多个下游DAG。

图6.17使用TriggerDagRunOperator可以实现各种dag间的依赖关系

img

但是,如果必须在另一个DAG开始运行之前完成多个触发DAG,该怎么办? 例如,如果DAG 1、2和3分别提取,转换和加载数据集,而您只想在所有三个DAG完成后才运行DAG 4,例如计算一组汇总指标,该怎么办? Airflow管理单个DAG中任务之间的依赖关系,但是它没有提供DAG间依赖关系的机制[20]。

图6.18举例说明了使用TriggerDagRunOperator无法解决的DAG间依赖关系

img

对于这种情况,我们可以应用ExternalTaskSensor,它是一个sensor ,可poking 其他DAG中的任务状态,如图6.19所示。 这样,wait_for_etl_dag {1,2,3}任务充当代理,以确保在最终执行报告任务之前,所有三个DAG的完成状态。

图6.19在某些情况下,例如确保所有三个DAG 1、2和3的完成状态,我们不能使用TriggerDagRunOperator来“pull”执行,而必须使用ExternalTaskSensor将其“pushing”向DAG 4。

img

ExternalTaskSensor 的工作方式是将其指向另一个 DAG 中的任务,以检查其状态。例如:

图6.20 ExternalTaskSensor 的示例使用

image-20210512152257280

由于没有从DAG1到DAG2的事件,因此DAG2轮询DAG1中的任务状态,尽管这样做有几个缺点。在Airflow的世界中,DAG没有其他DAG的概念。虽然从技术上可以查询基础元存储库(这是ExternalTaskSensor的工作)或从磁盘读取DAG脚本并推断其他工作流程的执行详细信息,但它们在Airflow中不会以任何方式耦合。如果使用ExternalTaskSensor,则需要在DAG之间进行一些对齐。默认行为是ExternalTaskSensor只需检查与自身执行日期完全相同的任务的成功状态。因此,如果 ExternalTaskSensor 的执行日期为2019-10-12T18:00:00,那么它将为给定的任务查询Airflow元存储,执行日期也是2019-10-12T18:00:00。现在,假设两个DAG的调度间隔都不同,那么它们将无法对齐,因此ExternalTaskSensor将永远找不到相应的任务!

img

6.3 使用 REST/CLI 启动工作流

除了从其他DAGs触发dag外,还可以通过REST API和CLI触发它们。如果你想从外部AIrflow开始工作流,这可能很有用。作为CI / CD管道的一部分。或者,可以通过将Lambda函数设置为调用REST API触发DAG来处理在AWS S3存储桶中随机到达的数据,而不必始终运行传感器进行轮询。

使用Airflow CLI,我们可以触发DAG,如下所示:

Listing 6.3

airflow trigger_dag dag1
 
[2019-10-06 14:48:54,297] {cli.py:238} INFO - Created <DagRun dag1 @ 2019-10-06 14:48:54+00:00: manual__2019-10-06T14:48:54+00:00, externally triggered: True>

这会触发执行日期设置为当前日期和时间的dag1。DAG运行ID的前缀为“ manual__”,以表明它是手动触发的,或者是从Airflow外部触发的。旁注:在Airflow 2.0中,CLI进行了重组,以提供用于使用Airflow组件的逻辑命令集。 因此,可以在Airflow 2.0中按以下方式触发DAG:

Listing 6.4

airflow dags trigger dag1

CLI接受对触发的DAG的其他配置:

Listing 6.5

airflow trigger_dag dag1 -c ‘{“supermarket_id”: 1}’
airflow trigger_dag dag1 --conf ‘{“supermarket_id”: 1}’

然后,可以通过任务上下文变量在触发的DAG运行的所有任务中使用此配置:

Listing 6.6

import airflow.utils.dates
from airflow import DAG
from airflow.operators.python_operator import PythonOperator
 
dag = DAG(dag_id="print_dag_run_conf", start_date=airflow.utils.dates.days_ago(3), schedule_interval=None)
 
 
def print_conf(**context):
   print(context["dag_run"].conf)
 
 
copy_to_raw = PythonOperator(task_id="copy_to_raw", python_callable=print_conf, provide_context=True, dag=dag)
process = PythonOperator(task_id="process", python_callable=print_conf, provide_context=True, dag=dag)
create_metrics = PythonOperator(task_id="create_metrics", python_callable=print_conf, provide_context=True, dag=dag)
copy_to_raw >> process >> create_metrics

这些任务打印提供给DAG运行的conf,可以将其作为变量应用到整个任务中:

[2019-10-15 20:03:05,885] {cli.py:516} INFO - Running <TaskInstance: print_dag_run_conf.process 2019-10-15T20:01:57+00:00 [running]> on host ebd4ad13bf98
[2019-10-15 20:03:05,909] {logging_mixin.py:95} INFO - {'supermarket': 1}
[2019-10-15 20:03:05,909] {python_operator.py:114} INFO - Done. Returned value was: None
[2019-10-15 20:03:09,150] {logging_mixin.py:95} INFO - [2019-10-15 20:03:09,149] {local_task_job.py:105} INFO - Task exited with return code 0

因此,如果你有一个 DAG,你在其中运行副本只是为了支持不同的变量,这使 DAG 运行配置变得更简洁,因为它允许你在管道中插入变量。但是请注意,清单6.6中的DAG没有调度间隔,即它仅在触发时运行。 如果DAG中的逻辑依赖于DAG运行配置,则将无法按计划运行,因为该时间表不提供任何DAG运行配置。

图6.21通过在运行时提供有效载荷来简化dag

img

同样,也可以将REST API用于相同的结果,例如 如果您无权访问CLI:

Listing 6.7

curl localhost:8080/api/experimental/dags/print_dag_run_conf/dag_runs -d {}
 
curl localhost:8080/api/experimental/dags/print_dag_run_conf/dag_runs -d '{"conf": {"supermarket": 1}}'

首先要注意,restapi 端点需要一段数据,因此-d {}是必需的,即使在没有额外配置的情况下触发 DAG 也是如此。其次,在Airflow 1. *中,API是实验性的,因此URL中的“实验性”。 在撰写本文时,社区计划重构用于Airflow 2.0的整个REST API,重写API并更改所有端点。 但是,在编写本文时,用于触发DAG的新端点尚不清楚。

6.4 摘要

  • Sensors 是一种特殊类型的运算符,可连续轮询给定条件为True。

  • Airflow提供了用于各种系统/用例的sensors 集合,也可以使用PythonSensor设置自定义条件。

  • TriggerDagRunOperator可以从另一个DAG触发DAG,而ExternalTaskSensor可以轮询另一个DAG中的状态。

  • 可以使用REST API和/或CLI从外部Airflow触发DAG。

  • CLI和REST API都可能在Airflow 2.0中进行更改。

posted @ 2021-05-12 16:54  yueqiudian  阅读(1024)  评论(0编辑  收藏  举报