Data Pipelines with Apache Airflow机翻-2.Airflow 有向无环图剖析

2.Airflow 有向无环图剖析

本章涵盖

  • 在自己的机器上运行Airflow
  • 编写并运行您的第一个工作流程
  • 在Airflow界面上检查第一个视图
  • 处理Airflow中失败的任务

在上一章中,我们了解了为什么处理数据以及数据环境中的许多工具不是一件容易的事。 在本章中,我们将开始使用Airflow,并查看一个示例工作流程,该示例工作流程使用了许多工作流程中的基本构建基块。

由于Airflow是在Python代码中定义的,因此在使用Airflow时具有一些Python经验会有所帮助。 学习Airflow基础知识的差距并不大。 通常,启动并运行Airflow工作流程的基本结构是一项容易的任务。 让我们深入研究一个火箭爱好者的用例,看看Airflow会如何帮助他。

2.1 从众多来源收集数据

火箭是人类的工程奇迹之一,每次火箭发射都会吸引全世界的目光。 在本章中,我们介绍了一个名为John的火箭爱好者的生活,他追踪并跟踪每一次火箭的发射。 在约翰一直跟踪的许多新闻来源中都可以找到有关火箭发射的新闻,理想情况下,约翰希望将他所有的火箭新闻汇总到一个位置。 约翰最近开始编程,希望以某种自动化的方式来收集所有火箭发射的信息,并最终以某种个人见解来了解最新的火箭新闻。 为了从小做起,约翰决定首先收集火箭的图像。

2.1.1 探索数据

对于数据,我们利用了“发射库” [5],这是一个在线数据库,用于存储来自各种来源的历史和未来火箭发射的数据。 它是一个免费开放的API,适用于地球上的任何人。

约翰目前仅对即将到来的火箭发射感兴趣。幸运的是,Launch Library 提供了他正在寻找的 URL 数据: https://launchlibrary.net/1.4/Launch?next=5&mode=verbose。它提供了关于未来五次火箭发射的数据,以及在哪里可以找到各自火箭的图片的 url。下面是这个 URL 返回的数据片段:

清单2.1对 Launch Library API 的 curl 请求和响应示例

$ curl "https://launchlibrary.net/1.4/launch?next=5&mode=verbose"
 
{
   "launches": [
       {
           "id": 1343,
           "name": "Ariane 5 ECA | Eutelsat 7C & AT&T T-16",
           "windowstart": "June 20, 2019 21:43:00 UTC",
           "windowend": "June 20, 2019 23:30:00 UTC",
           ...
           "rocket": {
               "id": 27,
               "name": "Ariane 5 ECA",
               ...
               "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/Ariane+5+ECA_1920.jpg"
           },
           ...
       },
       {
           "id": 1112,
           "name": "Proton-M/Blok DM-03 | Spektr-RG",
           "windowstart": "June 21, 2019 12:17:14 UTC",
           "windowend": "June 21, 2019 12:17:14 UTC",
           ...
           "rocket": {
               "id": 62,
               "name": "Proton-M/Blok DM-03",
               ...
               "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png"
           },
           ...
       },
       ...
   ],
   "total": 202,
   "offset": 0,
   "count": 5
}

正如您可以看到的,数据是 JSON 格式的,并提供火箭发射信息,每次发射都有一个字段“ rocket”,其中包含有关特定火箭的信息,如 id、名称和 imageURL。这正是 John 所需要的,他最初制定了以下计划来收集即将到来的火箭发射的图像(例如,将他的屏幕保护程序指向保存这些图像的目录) :

图2.1约翰下载火箭图片的心理模型

img

根据图2.1中的示例,我们可以看到,最终,约翰的目标是建立一个目录,其中包含火箭图像,比如这张阿丽亚娜5号 ECA 火箭的图像。

图2.2阿丽亚娜5号ECA火箭的示例图片

img

2.2 编写第一个Airflow DAG

John 的用例的范围很好,所以让我们来看看如何编写他的计划。这只是几个步骤,理论上,通过一些 Bash-fu,您可以用一句俏皮话解决这个问题。那么为什么我们需要一个像Airflow 这样的系统来完成这项工作呢?

关于Airflow的好处是,我们可以将包含一个或多个步骤的大型工作拆分为单独的“任务”,并一起形成有向无环图(DAG)。 可以并行运行多个任务,并且任务可以运行不同的技术。 例如,我们可以先运行一个Bash脚本,然后运行一个Python脚本。 我们将John关于工作流程的思维模型分解为Airflow中的三个逻辑任务:

图2.3 John的心理模型映射到Airflow中的任务

img

为什么您可能会问这三个任务? 为什么不在您可能想知道的单个任务中下载启动程序和相应的图片? 还是为什么不将其分为五个任务? 毕竟,约翰的计划中有五个箭头? 这些都是在开发工作流程时会问自己的有效问题,但事实是,没有正确或错误的答案。 尽管有几点需要考虑,并且在本书中,我们制定了许多这些用例,以了解对与错。 此工作流程的代码如下:

清单2.2用于下载和处理火箭发射数据的DAG

import json
import pathlib
 
import airflow
import requests
import requests.exceptions as requests_exceptions
from airflow import DAG
from airflow.operators.bash_operator import BashOperator
from airflow.operators.python_operator import PythonOperator
 
dag = DAG(
   dag_id="download_rocket_launches",
   start_date=airflow.utils.dates.days_ago(14),
   schedule_interval=None,
)
 
download_launches = BashOperator(
   task_id="download_launches",
   bash_command="curl -o /tmp/launches.json 'https://launchlibrary.net/1.4/launch?next=5&mode=verbose'",
   dag=dag,
)
 
 
def _get_pictures():
   # Ensure directory exists
   pathlib.Path("/tmp/images").mkdir(parents=True, exist_ok=True)
 
   # Download all pictures in launches.json
   with open("/tmp/launches.json") as f:
       launches = json.load(f)
       image_urls = [launch["rocket"]["imageURL"] for launch in launches["launches"]]
       for image_url in image_urls:
           try:
               response = requests.get(image_url)
               image_filename = image_url.split("/")[-1]
               target_file = f"/tmp/images/{image_filename}"
               with open(target_file, "wb") as f:
                   f.write(response.content)
               print(f"Downloaded {image_url} to {target_file}")
           except requests_exceptions.MissingSchema:
               print(f"{image_url} appears to be an invalid URL.")
           except requests_exceptions.ConnectionError:
               print(f"Could not connect to {image_url}.")
 
 
get_pictures = PythonOperator(
   task_id="get_pictures",
   python_callable=_get_pictures,
   dag=dag,
)
 
notify = BashOperator(
   task_id="notify",
   bash_command='echo "There are now $(ls /tmp/images/ | wc -l) images."',
   dag=dag,
)
 
download_launches >> get_pictures >> notify

让我们分解一下工作流程。 DAG是任何工作流程的起点。 工作流中的所有任务都引用此DAG对象,以便Airflow知道哪些任务属于哪个DAG:

dag = DAG(
   dag_id="download_rocket_launches",
   start_date=airflow.utils.dates.days_ago(14),
   schedule_interval=None,
)

请注意,(小写)dag是分配给(大写)DAG类的实例的名称。 实例名称可以有任何名称; 您可以将其命名为 rocket_dag或者你喜欢的任何名称。 我们将在所有运算符中引用变量(小写字母dag),该变量告诉Airflow运算符所属的DAG。

接下来,Airflow工作流程脚本由一个或多个执行实际工作的操作员组成。 在清单2.4中,我们应用BashOperator来运行一个Bash命令:

download_launches = BashOperator(
   task_id="download_launches",
   bash_command="curl -o /tmp/launches.json 'https://launchlibrary.net/1.4/launch?next=5&mode=verbose'",
   dag=dag,
)

每个操作员执行一个工作单元,而多个操作员一起组成工作流或Airflow中的DAG。 尽管您可以定义执行顺序,但操作员彼此独立运行,我们在Airflow中将其称为“依赖关系”。 毕竟,如果您在没有图片位置信息的情况下首次尝试下载图片,John的工作流程将无济于事。 为了确保任务以正确的顺序运行,我们可以如下设置任务之间的依赖关系:

download_launches >> get_pictures >> notify

在Airflow中,我们可以使用“二进制右移运算符”或“ rshift”(>>)来定义任务之间的依赖关系。 (注意:在Python中,rshift运算符(>>)用于移位,这是加密库中常见的操作。在Airflow中,没有用于移位的用例,并且rshift运算符被重写以提供可读性。 这样可以确保get_pictures任务仅在download_launches成功完成后才运行,而notify任务仅在get_pictures成功完成后才运行。

2.2.1 任务 vs 操作员

您可能想知道tasks和operators之间的区别是什么?毕竟,它们都会执行一些代码。在Airflow中,operators 只有一项职责: 他们的存在是为了完成一项工作。有些operators执行通用的工作,如 BashOperator (用于运行 Bash 脚本)和 PythonOperator (用于运行 Python 函数) ,其他operators有更具体的用例,如 EmailOperator (用于发送电子邮件)或 HTTPOperator (用于调用 HTTP 端点)。不管怎样,他们都只执行一项工作。

DAG的作用是协调一组operators的执行。这包括operators的启动和停止,operators完成后启动连续的任务,确保满足运算符之间的依赖关系,等等。

在这种情况下,以及在整个Airflow文档中,我们可以将术语“operator”和“task”互换使用。 从用户的角度来看,他们指的是同一件事,并且在讨论中两者经常互为替代。 operator提供一项工作的实施。 Airflow有一个称为BaseOperator的类,还有许多从BaseOperator继承的子类,例如PythonOperator,EmailOperator和OracleOperator。

不过还是有区别的。Airflow中的Tasks管理Operator的执行;可以将它们视为围绕操作员的小型“wrapper”或“manager”,以确保operators正确执行。 用户可以使用operators来专注于要完成的工作,而Airflow通过tasks来确保正确执行工作:

图2.4 Airflow用户使用DAG和Operators 。 Tasks 是管理operator 状态并向用户显示状态更改(例如开始/完成)的内部组件。

img

2.2.2 运行任意的Python代码

在 Bash 中,获取接下来五次火箭发射的数据是一个单独的 curl 命令,使用 BashOperator 可以轻松地执行这个命令。但是,解析JSON结果,从中选择图像URL并下载相应图像需要更多的工作。 尽管在Bash单一代码中仍然可以实现所有这些功能,但是使用几行 Python 语言或您选择的任何其他语言通常会更简单、更易读。由于Airflow代码是用Python定义的,因此将工作流和执行逻辑都保留在同一脚本中非常方便。 为了下载火箭图片,我们实现了以下内容:


def _get_pictures():
   # Ensure directory exists
   pathlib.Path("/tmp/images").mkdir(parents=True, exist_ok=True)
 
   # Download all pictures in launches.json
   with open("/tmp/launches.json") as f:
       launches = json.load(f)
       image_urls = [launch["rocket"]["imageURL"] for launch in launches["launches"]]
       for image_url in image_urls:
           try:
               response = requests.get(image_url)
               image_filename = image_url.split("/")[-1]
               target_file = f"/tmp/images/{image_filename}"
               with open(target_file, "wb") as f:
                   f.write(response.content)
               print(f"Downloaded {image_url} to {target_file}")
           except requests_exceptions.MissingSchema:
               print(f"{image_url} appears to be an invalid URL.")
           except requests_exceptions.ConnectionError:
               print(f"Could not connect to {image_url}.")
 
 
get_pictures = PythonOperator(
   task_id="get_pictures",
   python_callable=_get_pictures,
   dag=dag,
)

Airflow中的PythonOperator负责运行任何Python代码。 就像之前使用的BashOperator一样,此operators 和所有其他operators 也需要task_id。 运行任务时引用task_id并显示在UI中。 PythonOperator的使用总是双重的:

  1. 定义operator本身(get_pictures)和
  2. python_callable参数指向可调用对象,通常是一个函数(_get_pictures)

在运行操作符时,调用 Python 函数并执行该函数。我们来分析一下。PythonOperator 的基本用法总是如下所示:

img

尽管不是必需的,但为方便起见,我们将变量名“ get_pictures”保持为与task_id相等。

清单2.7确保输出目录存在-如果不存在则创建

# Ensure directory exists
pathlib.Path("/tmp/images").mkdir(parents=True, exist_ok=True)

可调用的第一步是确保存储图像的目录存在,如清单2.7所示。接下来,我们打开从 Launch Library API 下载的结果,提取每次发布的图片 url:

清单2.8提取每次火箭发射的图像 url

with open("/tmp/launches.json") as f:
   launches = json.load(f)
   image_urls = [launch["rocket"]["imageURL"] for launch in launches["launches"]]

调用每个图像URL来下载图像并将其保存在/ tmp / images中:

清单2.9从检索到的图像URL下载所有图像

for image_url in image_urls:
   try:
       response = requests.get(image_url)
       image_filename = image_url.split("/")[-1]
       target_file = f"/tmp/images/{image_filename}"
       with open(target_file, "wb") as f:
           f.write(response.content)
       print(f"Downloaded {image_url} to {target_file}")
   except requests_exceptions.MissingSchema:
       print(f"{image_url} appears to be an invalid URL.")
   except requests_exceptions.ConnectionError:
       print(f"Could not connect to {image_url}.")

2.3 在Airflow中运行DAG

现在我们有了基本的火箭发射 DAG,让我们启动并运行它,并在Airflow界面中查看它。最低限度的Airflow由两个核心组件组成:(1)调度程序和(2)Web服务器。 为了启动和运行Airflow,您可以在Python环境中安装Airflow或运行Docker容器。 Docker方式是单线的:

docker run -p 8080:8080 airflowbook/airflow

这需要在您的机器上安装Docker引擎。 它将下载并运行Airflow Docker容器。 运行后,您可以在http:// localhost:8080上查看Airflow。 第二个选项是从PyPi安装并以Python软件包的形式运行Airflow:

pip install apache-airflow

确保您安装的是apache-airflow,而不仅仅是airflow。 随着2016年加入Apache基金会,PyPi airflow存储库被重命名为apache-airflow。 由于许多人仍在安装airflow,而不是删除旧的存储库,因此将其保留为虚拟对象,以向所有人提供指向正确存储库的消息。

现在,您已经安装了Airflow,首先初始化元存储(存储所有Airflow状态的数据库),然后将火箭发射DAG复制到DAGs目录中,并启动调度程序和网络服务器:

  1. airflow initdb
  2. cp download_rocket_launches.py ~/airflow/dags/
  3. airflow scheduler
  4. airflow webserver

请注意,调度程序和Web服务器都是连续的过程,它们使您的终端保持打开状态,因此请在后台使用airflow调度程序运行&或打开第二个终端窗口以分别运行调度程序和Web服务器。 设置完成后,浏览至http:// localhost:8080以查看Airflow。

图2.5Airflow主屏幕

img

这是您将首先看到的Airflow。 当前,唯一的DAG是download_rocket_launches,可在DAGs目录中的Airflow上使用。主视图上有很多信息,但让我们先检查一下download_rocket_launches DAG。 单击DAG名称以将其打开并检查所谓的“图形视图”:

图2.6Airflow图视图

img

该视图向我们展示了提供给Airflow的DAG脚本的结构。 一旦放置在DAGs目录中,Airflow将读取脚本并拉出组成DAG的点点滴滴,以便可以在UI中将其可视化。 图形视图向我们展示了DAG的结构,如何以及以什么顺序连接和运行DAG中的所有任务。 这是您在开发工作流程时最常使用的视图之一。

状态图例显示了您在运行时可能会看到的所有颜色,因此让我们看看会发生什么并运行DAG。 首先,要运行DAG,必须将其设置为“开”,切换“ Off”按钮即可。 接下来,单击“触发DAG”以运行它。

图2.7显示正在运行的 DAG 的图形视图

img

触发DAG后,它将开始运行,您将看到由颜色表示的工作流的当前状态。 由于我们在任务之间设置了依赖关系,因此连续的任务只有在先前的任务完成后才开始运行。 让我们检查一下“通知”任务的结果。在实际用例中,您可能想要发送电子邮件或Slack 通知。为了简单起见,它现在打印下载的图像数量。 让我们检查一下日志。

所有任务日志都收集在Airflow中,因此我们可以在UI中搜索输出或发生故障时的潜在问题。 单击完成的“通知”任务,您将看到一个弹出窗口,其中包含许多选项:

图2.8任务弹出选项

img

单击右上方的“查看日志”按钮以查看日志:

默认情况下,日志非常详细,但会在输出日志中显示已下载图像的数量。 最后,我们可以打开/ tmp / images目录并查看它们:

图2.10生成的火箭图片

img

2.4 定期运行

火箭爱好者约翰很高兴,他已经在Airflow中启动并运行了一个工作流程,他可以不时触发该流程来收集最新的火箭图片。 他可以在Airflow UI中看到其工作流程的状态,与之前运行的命令行脚本相比,已经有了改善。 但是他仍然需要时不时地手动触发工作流程,而这应该是自动化的。 毕竟,没有人喜欢执行计算机擅长的重复性任务。

在Airflow中,我们可以安排DAG按一定的时间间隔运行-每小时,每天或每月一次。 这是通过在DAG上通过设置schedule_interval参数来控制的:

清单2.10

dag = DAG(
   dag_id="download_rocket_launches",
   start_date=airflow.utils.dates.days_ago(14),
   schedule_interval="@daily",
)

将schedule_interval设置为“ @daily”会通知Airflow每天运行一次此工作流程,因此John不必每天手动触发一次。 最好在树形视图中查看其行为:

图2.11Airflow树视图

img

树视图类似于图视图,但是随着时间的推移会显示图结构。 可以在此处查看单个工作流程的所有运行状态的概述。

图2.12图视图和树视图之间的关系

img

显示DAG的结构以适合“行和列”布局,特别是特定DAG的所有运行的状态,其中每列表示某个时间点的一次运行。

当我们将schedule_interval设置为“ @daily”时,Airflow知道它必须每天运行一次此DAG。 给定提供给DAG的14天前的起始日期,这意味着从14天之前到现在的时间可以分为14个相等的间隔,即1天。 由于这14个间隔的开始和结束日期时间都是过去的时间,因此一旦我们为Airflow提供了schedule_interval,它们就会开始运行。 调度间隔的语义以及各种配置间隔的方法将在第3章中更详细地介绍。

2.5 处理失败的任务

到目前为止,我们在Airflow UI中仅看到绿色。 但是,如果失败了怎么办? 任务失败的情况并不罕见,这可能是由多种原因造成的(例如,外部服务关闭,网络连接问题或磁盘损坏)。

举例来说,在获取约翰的火箭照片时,我们有时会遇到网络打中断的情况。 结果,Airflow任务失败,并且我们在Airflow UI中看到了失败的任务。 它看起来如下:

图2.13在“图形视图”和“树形视图”中显示的故障

img

由于无法从Internet获取图像,因此特定的失败任务将在图形视图和树视图中均以红色显示。 连续的“通知”任务根本不会运行,因为它取决于“ get_pictures”任务的成功状态。 此类任务实例以橙色显示。 默认情况下,所有先前的任务必须成功运行,并且失败任务的任何后续任务都将不运行。

让我们通过再次检查日志来找出问题所在。 打开“ get_pictures”任务的日志:

![image-20210511102748609](2.Airflow 有向无环图剖析.assets/image-20210511102748609.png)

在堆栈跟踪中,我们发现了问题的潜在原因:

urllib3.exceptions.NewConnectionError: <urllib3.connection.VerifiedHTTPSConnection object at 0x7f8a99d5d320>: Failed to establish a new connection: [Errno -2] Name or service not known

这表明urllib3(即Python的HTTP客户端)正在尝试建立连接,但无法建立连接,这可能暗示防火墙规则阻止了该连接或没有Internet连接。 假设我们已解决了该问题(例如,插入了互联网电缆),那么让我们重新开始该任务。 注意:不必重新启动整个工作流程。 Airflow的一个不错的功能是您可以从故障点开始重新启动,而不必重新启动以前成功完成的任务。

图2.15单击一个失败的任务以获得清除它的选项

img

单击失败的任务,然后单击弹出窗口中的“清除”按钮。 它将向您显示您将要清除的任务; 这意味着您将“重置”这些任务的状态,然后Airflow将重新运行它们:

图2.16清除get_pictures和后续任务的状态

img

点击“确定!” 失败的任务及其后续任务将被清除:

图2.17在“图形视图”中显示的已清除任务

img

假设连接问题已解决,那么任务现在将成功运行并使整个树形视图变为绿色:

图2.18清除失败的任务后成功完成的任务

img

在任何软件中,都有许多失败原因。 在Airflow工作流程中,有时接受失败,有时不接受,有时仅在某些情况下。 可以在工作流的任何级别上配置发生故障时的处理标准,并且在第4章中将更详细地介绍。

2.6 摘要

  • Airflow中的工作流在DAG中表示

  • Operators代表一个工作单元

  • Airflow包含一系列用于常规和特定类型工作的Operators

  • Airflow UI提供了一个用于查看DAG结构的图形视图和一个用于查看DAG运行时间的树形视图

  • 失败的任务可以在DAG中的任何位置重新启动

posted @ 2021-05-11 10:37  yueqiudian  阅读(447)  评论(0编辑  收藏  举报