本章包括以下内容:
- 使用模板化在运行时渲染变量
- PythonOperator与其他操作符的变量模板化对比
- 为调试目的渲染模板化的变量
- 在外部系统上执行操作
在前面的章节中,我们简单介绍了DAG和操作符如何配合工作以及如何在Airflow中调度工作流。在本章中,我们深入探讨了操作符的含义、功能、执行时机以及执行方式。我们还演示了如何使用操作符通过hooks与远程系统进行通信,从而可以执行一些任务,比如将数据加载到数据库中、在远程环境中运行命令以及在Airflow之外执行工作负载。
使用Airflow检查待处理数据
在本章中,我们将通过一个(虚构的)股市预测工具来逐步完善操作符的几个组件,这个工具应用情感分析,我们将称之为StockSense。维基百科是互联网上最大的公共信息资源之一。除了维基页面外,其他一些项目,比如页面访问计数,也是公开可用的。对于这个例子,我们将应用这样的公理:公司页面访问量的增加表示积极的情感,该公司的股价可能会上涨。另一方面,页面访问量的减少告诉我们兴趣的减少,该公司的股价可能会下降。
确定如何加载增量数据
维基媒体基金会(维基百科背后的组织)提供自2015年以来的所有页面访问量的机器可读格式。这些页面访问量可以以gzip格式下载,并且每小时每个页面的访问量被聚合。每个小时的数据约为50 MB,经过gzip压缩的文本文件,解压后的大小在200到250 MB之间。
无论处理任何类型的数据,这些都是基本细节。任何大小的数据都可能是复杂的,重要的是在构建数据处理流程之前要有一个技术方案。解决方案始终取决于您或其他用户对数据的需求,因此,请自问和询问其他人一些问题,例如“我们将来是否希望再次处理数据?”、“我如何接收数据(例如频率、大小、格式、来源类型)?”以及“我们将用数据构建什么?”在了解了这些问题的答案后,我们可以解决技术细节。
让我们下载一个小时的数据,并手动检查数据。为了开发数据处理流程,我们必须了解如何以增量方式加载数据,并如何处理这些数据(图4.1)。
我们可以看到URL遵循一个固定的模式,我们可以在批量下载数据时使用这个模式(在第3章中简要提及)。作为一个思想实验并验证数据,让我们看看在7月7日10:00至11:00之间最常用的域名代码是什么(图4.2)。
从顶部的结果中可以看出,1061202次访问的是"en"和995600次访问的是"en.m",这告诉我们在7月7日10:00至11:00之间,最常访问的域名是"en"和"en.m"(.en的移动版本),这是合理的,因为英语是世界上使用最广泛的语言。同时,结果按照我们期望的显示,这确认了没有意外字符或列的错位,这意味着我们不需要进行额外的处理来清理数据。通常,将数据清理和转换为一致的状态是工作的主要部分。
任务上下文和Jinja模板化
现在让我们将所有这些放在一起,创建第一个DAG,获取维基百科页面访问量。我们先从简单的开始,下载、提取和读取数据。我们选择了五家公司(亚马逊、苹果、Facebook、谷歌和微软)来最初跟踪和验证假设(图4.3)。
第一步是下载每个间隔的.zip文件。URL由各种日期和时间组件构成:
https://dumps.wikimedia.org/other/pageviews/
{year}/{year}-{month}/pageviews-{year}{month}{day}-{hour}0000.gz
对于每个间隔,我们将需要将该特定间隔的日期和时间插入到URL中。在第三章中,我们简要介绍了调度以及如何在代码中使用执行日期来执行一个特定的间隔。让我们深入探讨一下它是如何工作的。有很多方法可以下载页面访问量,但是让我们重点关注BashOperator和PythonOperator。在这些操作符中运行时插入变量的方法可以推广到所有其他操作符类型。
模板化操作符参数
首先,让我们使用BashOperator下载维基百科页面访问量。BashOperator接受一个参数bash_command,我们在其中提供要执行的Bash命令。在这个命令中,需要在运行时插入变量的URL组件都以双大括号开头和结尾。
import airflow.utils.dates
from airflow import DAG
from airflow.operators.bash import BashOperator
dag = DAG(
dag_id="chapter4_stocksense_bashoperator",
start_date=airflow.utils.dates.days_ago(3),
schedule_interval="@hourly",
)
get_data = BashOperator(
task_id="get_data",
bash_command=(
"curl -o /tmp/wikipageviews.gz "
"https://dumps.wikimedia.org/other/pageviews/"
" {{ execution_date.year }} /" ❶
"{{ execution_date.year }}-"
"{{ '{:02}'.format(execution_date.month) }}/"
"pageviews-{{ execution_date.year }}"
"{{ '{:02}'.format(execution_date.month) }}"
"{{ '{:02}'.format(execution_date.day) }}-"
"{{ '{:02}'.format(execution_date.hour) }}0000.gz" ❷
),
dag=dag,
)
❶ 双大括号表示在运行时插入的变量。
❷ 可以提供任何Python变量或表达式。
在第3章中简要提到,execution_date是任务运行时“神奇”可用的变量之一。双花括号表示一个Jinja模板字符串。Jinja是一个模板引擎,它在运行时替换模板字符串中的变量和/或表达式。当你作为程序员在编写代码时不知道某个值,但在运行时知道该值时,就会使用模板化。一个例子是当你有一个表单,可以在其中插入你的名字,然后代码会打印出插入的名字(见图4.4)。
在编程时不知道名字的值,因为用户会在运行时在表单中输入他们的名字。我们知道的是插入的值被赋给一个名为name的变量,然后我们可以提供一个模板化的字符串,"Hello {{ name }}!",以在运行时渲染和插入name的值。
在Airflow中,您可以在任务上下文中使用许多变量。其中一个变量是execution_date。Airflow使用Pendulum库(pendulum.eustace.io)处理日期和时间,而execution_date是Pendulum日期时间对象。它是原生Python datetime的替代品,因此所有可以应用于Python的方法也可以应用于Pendulum。就像你可以使用datetime.now().year一样,使用pendulum.now().year可以得到相同的结果。
>>> from datetime import datetime
>>> import pendulum
>>> datetime.now().year
2020
>>> pendulum.now().year
2020
维基百科页面浏览量的URL需要在月份、日期和小时上补零(例如,小时7表示为“07”)。因此,在Jinja模板字符串中,我们使用字符串格式化进行补零操作:
{{ '{:02}'.format(execution_date.hour) }}
什么可以用于模板化?
现在我们了解了哪些运算符的参数可以进行模板化,那么我们可以使用哪些变量进行模板化呢?我们之前在多个示例中已经看到了execution_date的使用,但实际上还有更多可用的变量。借助PythonOperator,我们可以打印出完整的任务上下文并对其进行检查。
import airflow.utils.dates
from airflow import DAG
from airflow.operators.python import PythonOperator
dag = DAG(
dag_id= "chapter4_print_context" ,
start_date=airflow.utils.dates.days_ago(3),
schedule_interval= "@daily" ,
)
def _print_context(**kwargs):
print(kwargs)
print_context = PythonOperator(
task_id= "print_context" ,
python_callable=_print_context,
dag=dag,
)
运行这个任务将打印出任务上下文中所有可用变量的字典。
{
'dag': <DAG: print_context>,
'ds': '2019-07-04',
'next_ds': '2019-07-04',
'next_ds_nodash': '20190704',
'prev_ds': '2019-07-03',
'prev_ds_nodash': '20190703',
...
}
所有变量都被捕获在 **kwargs 中,并传递给 print() 函数。所有这些变量在运行时都是可用的。表4.1提供了所有可用任务上下文变量的描述。
为PythonOperator创建模板
PythonOperator是在第4.2.1节中所展示的模板化的一个例外情况。对于BashOperator(以及Airflow中的所有其他Operator),你需要提供一个字符串给bash_command参数(或其他Operator中所使用的参数名称),该字符串会在运行时自动进行模板化。然而,PythonOperator是一个例外情况,因为它不使用可以在运行时上下文中进行模板化的参数,而是使用python_callable参数来应用运行时上下文。
我们来看一下在代码清单4.1中使用BashOperator下载维基百科页面视图的示例,但现在使用PythonOperator来实现。从功能上讲,这两种方法得到的结果是相同的。
from urllib import request
import airflow
from airflow import DAG
from airflow.operators.python import PythonOperator
dag = DAG(
dag_id="stocksense",
start_date=airflow.utils.dates.days_ago(1),
schedule_interval="@hourly",
)
def _get_data(execution_date): ❶
year, month, day, hour, *_ = execution_date.timetuple()
url = (
"https://dumps.wikimedia.org/other/pageviews/"
f"{year}/{year}-{month:0>2}/"
f”pageviews-{year}{month:0>2}{day:0>2}-{hour:0>2}0000.gz"
)
output_path = "/tmp/wikipageviews.gz"
request.urlretrieve(url, output_path)
get_data = PythonOperator(
task_id="get_data",
python_callable=_get_data, ❶
dag=dag,
)
❶ PythonOperator接受一个Python函数作为参数,而BashOperator则接受一个要执行的Bash命令字符串作为参数
在Python中,函数是一等公民(first-class citizens),我们可以将一个可调用对象(callable)2(函数是一种可调用对象)提供给PythonOperator的python_callable参数。在执行时,PythonOperator会执行所提供的可调用对象,这可以是任何函数。由于它是一个函数,而不是像其他Operator一样是一个字符串,因此函数内部的代码无法自动进行模板化。相反,任务上下文变量可以被提供并在给定的函数中使用,如图4.5所示。
Python允许在函数中捕获关键字参数。这在各种情况下都有用途,主要是为了在不预先知道关键字参数的情况下使用,以及避免明确地写出所有预期的关键字参数名称。
def _print_context(**kwargs): ❶
print(kwargs)
❶ 关键字参数可以通过双星号(**)进行捕获。一个常用的约定是将捕获的参数命名为kwargs。
为了向将来的自己和其他阅读Airflow代码的人表明你意图在关键字参数中捕获Airflow任务上下文变量,一个良好的实践是适当地命名这个参数(例如,"context")。
def _print_context( **context): ❶
print(context)
print_context = PythonOperator(
task_id="print_context",
python_callable=_print_context,
dag=dag,
)
❶ 将这个参数命名为context表明我们期望使用Airflow任务上下文。
context变量是一个包含所有上下文变量的字典,它允许我们为任务在运行的时间间隔内提供不同的行为,例如打印当前时间间隔的开始和结束日期时间:
def _print_context(**context):
start = context["execution_date"] ❶
end = context["next_execution_date"]
print(f"Start: {start}, end: {end}")
print_context = PythonOperator(
task_id="print_context", python_callable=_print_context, dag=dag
)
# Prints e.g.:
# Start: 2019-07-13T14:00:00+00:00, end: 2019-07-13T15:00:00+00:00
❶ 从上下文中提取execution_date。
现在我们已经看过了一些基本的例子,让我们来解析一下PythonOperator下载每小时维基百科页面视图的示例,就像在清单4.5(图4.6)中所示。
被PythonOperator调用的_get_data函数接受一个参数:context。如前所述,我们可以通过一个名为kwargs的单个参数来接受所有关键字参数(双星号表示接受所有关键字参数,而kwargs是实际变量名)。为了表示我们期望任务上下文变量,我们可以将其重命名为**context。然而,在Python中还有另一种接受关键字参数的方式。
def _get_data(execution_date, **context): ❶
year, month, day, hour, *_ = execution_date.timetuple()
# ...
❶ 这告诉Python我们期望接收一个名为execution_date的参数。它不会被捕获在context参数中。
在底层实现中,_get_data函数被调用时,所有上下文变量都作为关键字参数传递:
_get_data(conf=..., dag=..., dag_run=..., execution_date=..., ...)
然后,Python会检查函数签名中是否有任何预期的参数(图4.7)。
首先,参数conf被检查并且在_get_data函数的签名(预期参数)中找不到,因此被添加到**context中。对于参数dag和dag_run也是同样的情况,因为这两个参数都不在函数的预期参数中。接下来是execution_date,我们期望接收这个参数,因此它的值被传递给_get_data()函数中的execution_date参数(图4.8)。
在这个例子中的最终结果是,名为execution_date的关键字参数被传递给execution_date参数,而所有其他变量都被传递给**context,因为它们在函数签名中没有明确地被期望(图4.9)。
现在,我们可以直接使用execution_date变量,而不需要从**context中提取它,比如context["execution_date"]。此外,你的代码将更加清晰明了,并且像代码检查工具(linters)和类型提示等工具都可以受益于显式的参数定义。
向PythonOperator提供变量
现在我们已经了解了操作符中任务上下文的工作原理以及Python如何处理关键字参数,想象一下我们想要从多个数据源下载数据。可以复制_get_data()函数并稍作修改以支持第二个数据源。然而,PythonOperator还支持向可调用函数提供额外的参数。例如,假设我们首先使output_path可配置,这样根据任务的不同,我们可以配置输出路径,而不必复制整个函数只为了更改输出路径(图4.10)。
可以通过两种方式提供output_path的值。第一种方式是通过一个参数:op_args。
get_data = PythonOperator(
task_id="get_data",
python_callable=_get_data,
op_args=["/tmp/wikipageviews.gz"], ❶
dag=dag,
)
❶ 使用op_args向可调用函数提供额外的变量。
在操作符执行时,op_args参数提供的列表中的每个值都会传递给可调用函数(即与直接调用函数_get_data("/tmp/wikipageviews.gz")相同的效果)。
由于图4.10中output_path在_get_data函数中是第一个参数,当运行时它的值将被设置为/tmp/wikipageviews.gz(我们称这些为非关键字参数)。第二种方法是使用op_kwargs参数,如下面的示例所示。
get_data = PythonOperator(
task_id="get_data",
python_callable=_get_data,
op_kwargs={"output_path": "/tmp/wikipageviews.gz"}, ❶
dag=dag,
)
❶ 给op_kwargs传递的字典将作为关键字参数传递给可调用函数。
与op_args类似,op_kwargs中的所有值都会传递给可调用函数,但这次是作为关键字参数。等效于调用_get_data的方式为:
_get_data(output_path="/tmp/wikipageviews.gz")
请注意,这些值可以包含字符串,因此可以进行模板化。这意味着我们可以避免在可调用函数内部提取日期时间组件,而是将模板化的字符串传递给可调用函数。
def _get_data(year, month, day, hour, output_path, **_):
url = (
"https://dumps.wikimedia.org/other/pageviews/"
f"{year}/{year}-{month:0>2}/”
f"pageviews-{year}{month:0>2}{day:0>2}-{hour:0>2}0000.gz"
)
request.urlretrieve(url, output_path)
get_data = PythonOperator(
task_id="get_data",
python_callable=_get_data,
op_kwargs={
"year": " {{ execution_date.year }} ", ❶
"month": "{{ execution_date.month }}",
"day": "{{ execution_date.day }}",
"hour": "{{ execution_date.hour }}",
"output_path": "/tmp/wikipageviews.gz",
},
dag=dag,
)
❶ 用户定义的关键字参数在传递给可调用函数之前进行模板化处理。
检查模板化参数
一个有用的工具用于调试模板化参数的问题是Airflow的用户界面(UI)。在运行任务后,您可以通过在图形视图或树状视图中选择任务,然后点击"Rendered Template"按钮来查看模板化参数的值(图4.11)。
渲染后的模板视图显示了给定Operator中所有可渲染的属性及其值。这个视图是每个任务实例可见的。因此,任务必须由Airflow调度后才能够查看给定任务实例的渲染属性(也就是说,您必须等待Airflow调度下一个任务实例)。在开发过程中,这可能不太实用。Airflow命令行界面(CLI)允许我们渲染任何给定日期时间的模板化值。
# airflow tasks render stocksense get_data 2019-07-19T00:00:00
# ----------------------------------------------------------
# property: templates_dict
# ----------------------------------------------------------
None
# ----------------------------------------------------------
# property: op_args
# ----------------------------------------------------------
[]
# ----------------------------------------------------------
# property: op_kwargs
# ----------------------------------------------------------
{'year': '2019', 'month': '7', 'day': '19', 'hour': '0', 'output_path': '/tmp/wikipageviews.gz'}
CLI为我们提供了与Airflow UI中显示的完全相同的信息,而无需运行任务,这使得检查结果更加方便。使用CLI渲染模板的命令是:
airflow tasks render [dag id] [task id] [desired execution date]
您可以输入任何日期时间,Airflow CLI将渲染所有模板化属性,就像任务将在所需的日期时间运行一样。使用CLI不会在元存储中注册任何内容,因此是一种更轻量级和灵活的操作。
连接其他系统
现在我们已经弄清楚了模板化的工作原理,让我们继续使用案例,处理每小时的维基百科页面视图。接下来的两个操作符将提取存档并通过扫描文件并选择给定页面名称的页面视图计数来处理提取的文件。然后,结果将被打印在日志中。
extract_gz = BashOperator(
task_id="extract_gz",
bash_command="gunzip --force /tmp/wikipageviews.gz",
dag=dag,
)
def _fetch_pageviews(pagenames):
result = dict.fromkeys(pagenames, 0)
with open(f"/tmp/wikipageviews", "r") as f: ❶
for line in f:
domain_code, page_title, view_counts, _ = line.split(" ") ❷
if domain_code == "en" and page_title in pagenames: ❸ ❹
result[page_title] = view_counts
print(result)
# Prints e.g. "{'Facebook': '778', 'Apple': '20', 'Google': '451', 'Amazon': '9', 'Microsoft': '119'}"
fetch_pageviews = PythonOperator(
task_id="fetch_pageviews",
python_callable=_fetch_pageviews,
op_kwargs={
"pagenames": {
"Google",
"Amazon",
"Apple",
"Microsoft",
"Facebook",
}
},
dag=dag,
)
❶ 打开之前任务中写入的文件。
❷ 提取一行上的元素。
❸ 仅保留域名为“en”的元素。
❹ 检查页面标题是否在给定的页面名称中。
例如,这将输出{'Apple': '31','Microsoft': '87','Amazon': '7','Facebook': '228','Google': '275'}。作为第一个改进,我们希望将这些计数写入我们自己的数据库,以便可以使用SQL查询并询问诸如“Google Wikipedia页面的平均每小时页面浏览次数是多少?”(图4.12)这样的问题。
我们有一个Postgres数据库用于存储每小时的页面浏览量。用于保存数据的表包含三列,如4.16节中所示。
CREATE TABLE pageview_counts (
pagename VARCHAR(50) NOT NULL,
pageviewcount INT NOT NULL,
datetime TIMESTAMP NOT NULL
);
pagename列和pageviewcount列分别保存维基百科页面的名称和给定小时内该页面的页面浏览量。datetime列将保存计数的日期和时间,这与Airflow的执行日期(execution_date)相等,用于表示一个时间间隔。一个示例的INSERT查询如下所示。
INSERT INTO pageview_counts VALUES ('Google', 333, '2019-07-17T00:00:00');
目前,此代码仅打印找到的页面浏览次数,现在我们希望通过将这些结果写入Postgres表来连接这些结果。PythonOperator目前仅打印结果,而不会将结果写入数据库,因此我们需要第二个任务来写入结果。在Airflow中,有两种在任务之间传递数据的方式:
- 使用Airflow元数据存储在任务之间写入和读取结果。这称为XCom,并在第5章中介绍。
- 在任务之间将结果写入和读取到持久位置(例如磁盘或数据库)。
Airflow任务是相互独立运行的,根据您的设置,可能在不同的物理机器上运行,因此无法共享内存中的对象。因此,任务之间的数据必须在其他地方持久化,任务完成后数据会保留在那里,可以被另一个任务读取。
Airflow提供了一个称为XCom的机制,允许存储和稍后读取Airflow元数据中的任何可pickle化对象。Pickle是Python的序列化协议,序列化是将内存中的对象转换为可以存储在磁盘上以供稍后读取的格式,可能由另一个进程读取。默认情况下,所有基本Python类型构建的对象(例如字符串,整数,字典,列表)都可以pickle化。不可pickle化对象的例子包括数据库连接和文件处理程序。使用XCom存储pickled对象仅适用于较小的对象。由于Airflow的元数据存储(通常是MySQL或Postgres数据库)的大小是有限的,并且pickled对象存储在元数据存储中的blob中,因此通常建议仅将XCom应用于传输小型数据片段,例如一些字符串(例如,名称列表)。
在任务之间传递数据的另一种选择是将数据保留在Airflow之外。存储数据的方式有无限多种,但通常会创建一个磁盘上的文件。在此用例中,我们获取了一些字符串和整数,这些本身并不占用大量空间。考虑到可能会添加更多的页面,因此数据大小可能会在未来增长,我们将提前考虑并将结果持久化到磁盘上,而不是使用XCom。
为了决定如何存储中间数据,我们必须了解数据将在何处以及如何再次使用。由于目标数据库是Postgres,我们将使用PostgresOperator来插入数据。首先,我们必须安装一个附加包来在项目中导入PostgresOperator类:(以下未提供具体的包名,请根据具体情况安装)
pip install apache-airflow-providers-postgres
PostgresOperator将运行您提供的任何查询。由于PostgresOperator不支持从CSV数据进行插入操作,因此我们将首先将SQL查询作为我们的中间数据来写入。
def _fetch_pageviews(pagenames, execution_date, **_):
result = dict.fromkeys(pagenames, 0) ❶
with open("/tmp/wikipageviews", "r") as f:
for line in f:
domain_code, page_title, view_counts, _ = line.split(" ")
if domain_code == "en" and page_title in pagenames:
result[page_title] = view_counts ❷
with open("/tmp/postgres_query.sql", "w") as f:
for pagename, pageviewcount in result.items(): ❸
f.write(
"INSERT INTO pageview_counts VALUES ("
f"'{pagename}', {pageviewcount}, '{execution_date}'"
");\n"
)
fetch_pageviews = PythonOperator(
task_id="fetch_pageviews",
python_callable=_fetch_pageviews,
op_kwargs={"pagenames": {"Google", "Amazon", "Apple", "Microsoft", "Facebook"}},
dag=dag,
)
❶ 对所有页面浏览量初始化为零。
❷ 存储页面浏览量计数。
❸ 对于每个结果,编写SQL查询。
运行此任务将为给定的时间间隔生成一个文件(/tmp/postgres_query.sql),其中包含由PostgresOperator运行的所有SQL查询。请参考以下示例。
INSERT INTO pageview_counts VALUES ('Facebook', 275, '2019-07-18T02:00:00+00:00');
INSERT INTO pageview_counts VALUES ('Apple', 35, '2019-07-18T02:00:00+00:00');
INSERT INTO pageview_counts VALUES ('Microsoft', 136, '2019-07-18T02:00:00+00:00');
INSERT INTO pageview_counts VALUES ('Amazon', 17, '2019-07-18T02:00:00+00:00');
INSERT INTO pageview_counts VALUES ('Google', 399, '2019-07-18T02:00:00+00:00');
现在我们已经生成了查询,是时候连接最后一块拼图了。
from airflow.providers.postgres.operators.postgres import PostgresOperator
dag = DAG(..., template_searchpath="/tmp") ❶
write_to_postgres = PostgresOperator(
task_id="write_to_postgres",
postgres_conn_id="my_postgres", ❷
sql="postgres_query.sql", ❸
dag=dag,
)
❶ 搜索SQL文件的路径
❷ 用于连接的凭据标识符
❸ SQL查询或包含SQL查询的文件路径
相应的图形视图将类似于图4.13所示。
PostgresOperator只需填写两个参数即可针对Postgres数据库运行查询。复杂的操作,例如建立与数据库的连接并在完成后关闭它,都在幕后进行处理。postgres_conn_id参数指向一个保存Postgres数据库凭据的标识符。Airflow可以管理这些凭据(以加密形式存储在元数据存储中),并且操作器在需要时可以获取其中的一个凭据。在不详细介绍的情况下,我们可以借助CLI在Airflow中添加my_postgres连接。
airflow connections add \
--conn-type postgres \
--conn-host localhost \
--conn-login postgres \
--conn-password mysecretpassword \
my_postgres ❶
❶ 连接标识符
连接随后在用户界面中可见(也可以从此处创建)。前往管理 > 连接,以查看Airflow中存储的所有连接(图4.14所示)。
一旦完成了多个DAG运行,Postgres数据库将保存一些计数:
"Amazon",12,"2019-07-17 00:00:00"
"Amazon",11,"2019-07-17 01:00:00"
"Amazon",19,"2019-07-17 02:00:00"
"Amazon",13,"2019-07-17 03:00:00"
"Amazon",12,"2019-07-17 04:00:00"
"Amazon",12,"2019-07-17 05:00:00"
"Amazon",11,"2019-07-17 06:00:00"
"Amazon",14,"2019-07-17 07:00:00"
"Amazon",15,"2019-07-17 08:00:00"
"Amazon",17,"2019-07-17 09:00:00"
在这最后一步中,有几点需要指出。DAG有一个额外的参数:template_searchpath。除了一个字符串INSERT INTO ...,文件的内容也可以是模板化的。每个操作器都可以通过向操作器提供文件路径来读取和模板化特定扩展名的文件。对于PostgresOperator来说,参数SQL可以是模板化的,因此也可以提供一个包含SQL查询的文件路径。任何以.sql结尾的文件路径将被读取,文件中的模板将被渲染,并且PostgresOperator将执行文件中的查询。再次强调,参考操作器的文档并检查字段template_ext,该字段包含操作器可以模板化的文件扩展名。
注意:Jinja要求您提供可模板化的文件搜索路径。默认情况下,仅搜索DAG文件的路径,但由于我们已将其存储在/tmp中,Jinja将无法找到它。要为Jinja添加搜索路径,请在DAG上设置参数template_searchpath,Jinja将遍历默认路径以及其他提供的路径进行搜索。
Postgres是一个外部系统,Airflow通过其生态系统中的许多操作器支持连接到各种外部系统。这确实有一个含义:连接到外部系统通常需要安装特定的依赖项,这些依赖项允许与外部系统进行连接和通信。对于Postgres也是如此;我们必须安装apache-airflow-providers-postgres包,以在我们的Airflow安装中安装额外的Postgres依赖项。许多依赖项是任何编排系统的特点之一-为了与许多外部系统通信,不可避免地需要安装许多依赖项。
执行PostgresOperator时,会发生一系列操作(图4.15)。PostgresOperator将实例化一个所谓的hook来与Postgres进行通信。该hook负责创建连接,将查询发送到Postgres,并在完成后关闭连接。在这种情况下,操作器仅仅是将用户的请求传递给hook。
注意:操作器确定要执行的操作,而hook确定如何执行操作。
在构建这样的流水线时,您只需要处理操作器,对于任何hook,您不需要了解,因为hook在操作器内部使用。
经过多个DAG运行后,Postgres数据库将包含从维基百科页面浏览量中提取出的一些记录。现在,Airflow每小时自动下载新的小时页面浏览量数据集,解压缩它,提取所需的计数,并将其写入Postgres数据库。现在我们可以提出问题,比如“每小时哪个页面最受欢迎?”
SELECT x.pagename, x.hr AS "hour", x.average AS "average pageviews"
FROM (
SELECT
pagename,
date_part('hour', datetime) AS hr,
AVG(pageviewcount) AS average,
ROW_NUMBER() OVER (PARTITION BY pagename ORDER BY AVG(pageviewcount) DESC)
FROM pageview_counts
GROUP BY pagename, hr
) AS x
WHERE row_number=1;
这个列表告诉我们查看特定页面最受欢迎的时间是在16:00到21:00之间,如表格4.2所示。
通过这个查询,我们现在已经完成了设想的维基百科工作流程,该流程执行完整的循环:下载每小时的页面浏览量数据,处理数据,并将结果写入Postgres数据库以供未来分析。Airflow负责编排任务的正确时间和顺序。借助任务运行时上下文和模板化,代码将根据给定的时间间隔执行,使用该时间间隔附带的日期时间值。如果一切都设置正确,工作流程现在可以无限运行。
总结
- 一些操作器的参数可以进行模板化。
- 模板化是在运行时发生的。
- 对PythonOperator进行模板化与其他操作器不同;变量将传递给提供的可调用对象。
- 可以使用airflow tasks render来检查模板化参数的结果。
- 操作器可以通过hooks与其他系统通信。
- 操作器描述要做什么;hooks确定如何执行工作。