💡
本文是关于Airflow的基础操作和使用的介绍博客,包括基础的元素组件的使用和示例。
使用的环境为Python3.10,Airflow版本为2.10.3
安装步骤B站有比较多的视频,比如https://www.bilibili.com/video/BV19f4y1V7UG/?spm_id_from=333.999.0.0,里面讲的比较清楚,所以就简单描述一下。
1. 安装
主要参考官网教程
Quick Start — Airflow Documentation
逐行复制命令就可以了。
export AIRFLOW_HOME=~/airflow #设置根目录
AIRFLOW_VERSION=2.10.2#设置Airflow版本
PYTHON_VERSION="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" #python版本匹配系统版本
CONSTRAINT_URL="https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt" #根据以上两个信息得到下载链接
pip install "apache-airflow==${AIRFLOW_VERSION}" --constraint "${CONSTRAINT_URL}" #使用pip安装对应版本的Airflow
然后即可开始启动Airflow
airflow users create \
--username admin \
--firstname Peter \
--lastname Parker \
--role Admin \
--email spiderman@superhero.org #创建Airflow中的用户,邮箱,用户名,角色是必选的选项,命令成功会要求输入两次密码,输入之后即可成功创建
airflow webserver --port 8080 #启动Airflow 服务器,启动后即可在网页中进行访问 ,加入-D选项即可后台运行
airflow scheduler #启动调度器,加入-D选项即可后台运行
至此,安装完成,但是自带的sqlite数据库有很多操作上的限制,可以换成其他后端数据库。
#配置mysql容器
#新建数据库
CREATE DATABASE airflow_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'airflow_user' IDENTIFIED BY 'airflow_pass';
GRANT ALL PRIVILEGES ON airflow_db.* TO 'airflow_user';
#更改airflow.cfg
executor = LocalExecutor
mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
#安装mysqlconnector
pip install mysql-connector-python
pip install pymysql
#迁移数据库
airflow db migrate
#重启即可
2.基础组件
2.1. DAG
2.1.1DAG概述
Airflow中最为关键的概念就是DAG,中文名称是有向无环图(Directed Acyclic Graph)
一个DAG代表着一个较为完整的流程,其中的组成部分是执行不同步骤的Task。
如果说DAG是一条珍珠项链,那么TASK就是其中的珍珠。珍珠项链可以有不同的形态、数量和组合方式,DAG和TASK也是同理。
2.1.2 DAG参数描述
-
参数描述
with DAG( "tutorial",#DAG名,也可使用dag_display_name,指定dag的别名 default_args={ "depends_on_past": False, "email": ["airflow@example.com"], "email_on_failure": False, "email_on_retry": False, "retries": 1, #失败后重试的次数 "retry_delay": timedelta(minutes=5), #失败后多久再进行尝试 'queue': 'bash_queue',#指定对的队列,相当于标签 'pool': 'backfill',#限制tasks的并发,需要在UI建立池,设置slot槽的个数,运行时只进行 'priority_weight': 10, 'end_date': datetime(2016, 1, 1), #结束日期 'wait_for_downstream': False, 'sla': timedelta(hours=2), # Service Level Agreement sla的意思就是,某个任务设置了截止时间,如果某个它到了截止时间仍然没有启动、或者运行成功,那么就会记录到这张表中,作为告警使用❓ 'execution_timeout': timedelta(seconds=300), 'on_failure_callback': some_function, #任务失败时的操作 'on_success_callback': some_other_function, #任务成功时的操作 'on_retry_callback': another_function,#重试任务时执行的操作 'sla_miss_callback': yet_another_function, #超过sla时限没有执行的操作 'on_skipped_callback': another_function, #跳过时执行的操作 'trigger_rule': 'all_success'#启动下一个任务的条件 }, #适用于 DAG中所有的operator对应的选项初始化时的默认选项,如果再定义时,该参数会由显式定义的选项决定 description="A simple tutorial DAG",#对于任务的进一步描述,显示在UI端 schedule=timedelta(days=1), #任务执行的间隔 start_date=datetime(2021, 1, 1),#任务开始日期 catchup=False,#是否回溯 tags=["example"],#在UI端中显示的任务标签,用于筛选同类的任务 ) as dag:
2.1.3 DAG图示
这里假设了三个任务,所以作为一个串联形态的DAG
2.1.4DAG创建方式
https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html#taskgroups
#方式一:with关键字
with DAG(
dag_id="my_dag_name",
start_date=datetime.datetime(2021, 1, 1),
schedule="@daily",
):
#方式二:DAG类实例化
my_dag = DAG(
dag_id="my_dag_name",
start_date=datetime.datetime(2021, 1, 1),
schedule="@daily",
)
EmptyOperator(task_id="task", dag=my_dag)
#方式三:dag装饰器
@dag(start_date=datetime.datetime(2021, 1, 1), schedule="@daily")
def generate_dag():
EmptyOperator(task_id="task")
2.1.5 Dynamic DAGs
- 可以理解为这个DAG中会按照一定的规律来进行DAG的创建,并进行执行。
#每一个循环中都会执行类似但不同的三个task
with DAG("loop_example", ...):
first = EmptyOperator(task_id="first")
last = EmptyOperator(task_id="last")
options = ["branch_a", "branch_b", "branch_c", "branch_d"]
for option in options:
t = EmptyOperator(task_id=option)
first >> t >> last
2.2 Task
Task是DAG的重要部件,主要通过Operator进行实现。实际使用过程中需要定义一个Task,再确定其中调用的Operator是哪一个。Task之间会有对应的上下游依赖关系来确定执行的顺序。
2.2.1 Task上下游关系的三种指定方式
# 方式1:使用特殊运算符表示
first_task >> second_task >> [third_task, fourth_task]
# 方法2:使用set系函数
first_task.set_downstream(second_task)
third_task.set_upstream(second_task)
#方法三 使用chain函数
from airflow.models.baseoperator import chain
chain(first_task,second_task ,third_task, fourth_task)
2.2.2 task的定义方式
#方式一:类的实例化
test_task = PythonOperator( #以PythonOperator为例,也可以定义使用其他的Operator
task_id = 'test_task'#task在UI端显示的名字
python_callable = to_do_function #指定task所要执行的操作对应的函数名称
op_kwargs = {'args1':'value1'} #向task中的operator传递参数,传递的是一个字典
)
#方式二:装饰器
from airflow.decorators import task
@task(args_set)
def task_1(value: int) -> str:
"""Empty Task1"""
return f"[ Task1 {value} ]"
2.3 Operator
Operator也就是定义task中的执行的操作的函数,其中定义了一组行为
Airflow中提供的Operator主要包括PythonOperator、BashOperator等(其他的Operator,主要都与各数据库相关,具体可以查看airflow.operator中定义的具体内容)
2.3.1 PythonOperator如何接收参数
Operator的定义方式与一般的函数相同,但是其有一些其他的参数接收方式。
#方式一:使用python语法中的 **kwargs,不定量参数
def pyOp(**kwargs):
ti = kwargs['ti']
file_list = ti.xcom_pull(task_ids="upstream_task")#使用XCom进行通信
'''
left part of this funciton
'''
2.3.2 Sensor传感器的使用
-
Sensor概述:Sensor是一种特殊的Operator,主要的功能是作为一个等待指定条件达到后才会进行对应操作的函数。
-
Sensor定义:
def __init__( task_id = 'test_sensor', #任务的名称 python_callable: Callable, #对应执行的函数名称 mode = 'poke', #传感器的探测模式,默认的poke表示指定间隔一段指定的时间之后再进行探测,同时一直占用一个worker slot和一个pool slot。建议poke的时间间隔在一分钟以上,避免调度器负载过大。 #另一种模式是reschedule,会释放work slot poke_interval = 30, #探测间隔 单位默认为秒 op_args: list | None = None, #重要的参数 op_kwargs: Mapping[str, Any] | None = None, #可能会传入的参数 templates_dict: dict | None = None, #❓一个字典,其中的值是模板,这些模板将在“__init__”和“execute”之间的某个时间被Airflow引擎模板化,并在模板被应用后在可调用对象的context中可用。模板指的是 **kwargs, ):
2.4 TaskGroups
-
taskgroup概述:
- 可以简单理解为数个相关的任务被定为一组任务,拥有共同的默认参数
- 与SubDAGs不同,任务组纯粹是一个UI分组概念。TaskGroup中的Task位于相同的DAG上,并遵循所有DAG设置和Pool配置
- TaskGroup可用于将任务组织到图形视图中的分层组中。它对于创建重复模式和减少视觉混乱非常有用。也就是说,适用于任务多需要分类的情况。
- 在定义上下游任务的时候,taskgroup与task的方式相同
- taskgroup内部定义组内的任务执行的顺序
-
taskgroup定义:
#方式一:@taskgroup装饰器定义 @task_group(default_args={"retries": 3}) def group1(): """This docstring will become the tooltip for the TaskGroup.""" task1 = EmptyOperator(task_id="task1") task2 = BashOperator(task_id="task2", bash_command="echo Hello World!", retries=2) print(task1.retries) # 3 print(task2.retries) # 2 #方式二:with关键字定义 with TaskGroup(taskgroup_id,dag=dag) as task_group:
-
个人理解:taskgroup仅为一种逻辑上让任务的步骤更将清晰明确的组件,能够在有需要的时候更关注关键的整体情况,而不会被一些细节的信息和小的任务情况所干扰。
-
特别备注:XCom的原理是将参数存储至后端数据库当中,再从后端数据库进行调用。因此传递本身的体量会受到,后端数据库的一些限制。基于以上背景,需要传输大体量数据时,建议存储至本地文件,再从本地文件读取的方式进行数据的传输。
2.5.1 Edge labels
可以给上下游任务之间表明其间的关系
from airflow.utils.edgemodifier import Label
check >> Label("Errors found") >> describe >> error >> report #在运算符之间使用label函数进行设定
2.6XCom
(short for “cross-communications”)主要负责任务之间的沟通。任务之间本身是作为独立的任务来看待,所以需要xcom机制负责传递任务之间需要通信的信息。
官方文档建议:
传输短的信息(small messages)的时候使用XCom,传输较大的文件(large data)时使用远程存储,如S3/HDFS。
#需要发送消息则使用xcom_pushed函数
task_instance.xcom_push(key="identifier as a string", value=any_serializable_value)
#获取信息使用 xcom_pull函数
task_instance.xcom_pull(key="identifier as string", task_ids="task-1")
2.7 Variables
可以管理和使用Airflow的全局变量,这些变量可以在UI端中进行设定,并通过Variable类进行获取
# Returns the value of default_var (None) if the variable is not set
baz = Variable.get("baz", default_var=None)
2.8 Params
2.8.1 DAG-level Params
使用Params可以向DAG进行传参。
使得DAG可以当作一个复杂的函数使用,将其中需要进行变化的部分替换为变量,既可以实现DAG的复用。
#定义
from airflow.models.param import Param
with DAG(
"the_dag",
params={
"x": Param(5, type="integer", minimum=3),
"my_int_param": 6
},
) as dag:
@task.python
def example_task(params: dict): #Params中包含的参数可以在任务中进行调用
# This will print the default value, 6:
dag.log.info(dag.params['my_int_param'])
# This will print the manually-provided value, 42:
dag.log.info(params['my_int_param'])
# This will print the default value, 5, since it wasn't provided manually:
dag.log.info(params['x'])
example_task()
#使用
if __name__ == "__main__":
dag.test(
run_conf={"my_int_param": 42}
)
2.8.2 Task-level Params
def print_my_int_param(params):
print(params.my_int_param)
PythonOperator(
task_id="print_my_int_param",
params={"my_int_param": 10},
python_callable=print_my_int_param,
)
3.Best Practice
💡
官方文档给出的三个建立DAG的主要步骤:
- 使用Python语言建立DAG示例
- 测试函数的功能是否符合预期
- 调试环境依赖,运行DAG
参考翻译:https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html#dynamic-dag-generation
3.1 写一个DAG
- 建议一个自定义Operator/Hook
- 建立Task:(应当将Task等价于数据库中的事务)Task支持失败后重新执行,因此其应该在每次重新运行时产生相同的结果。
- 删除Task:不建议删除Task,如果要更改task,建议新建一个DAG
- 信息交互:
- 传输大文件使用远程存储,传输小文件使用Xcom
- task代码中不应该存储任何授权信息,比如密码或者是token。应该使用Connections将授权信息存储在后端,使用connection id来获取。
3.1.2 Top level Python 代码
Top level code指的是不再任何类或者函数内部定义的代码
- 避免使用 top level代码,会对于Airflow的调度和可扩展性造成影响。
- 特别是不应该运行任何数据库访问、繁重的计算和网络操作。
- top level import可能会花费大量时间,并且会产生大量开销,这可以通过将它们转换为Python可调用对象中的本地导入来轻松避免。
3.1.3 如何判断是否为Top Level Code
首先,需要对于Python如何切割Python文件有所理解。一般来说,当Python解析Python文件时,它会执行它看到的代码,除了(通常)不执行的方法的内部代码。
判断方法:只需要解析代码并查看这段代码是否被执行。可以在要检查的代码中添加一些print语句,然后运行python <my_dag_file>.py。如果有返回则证明会被视作Top Level Code
3.1.4Dynamic DAG Generation
参考文件:https://airflow.apache.org/docs/apache-airflow/stable/howto/dynamic-dag-generation.html
使用场景
当有很多相似的DAG但仅有参数不同或者需要一组DAG加载表格但是不想每次都手动更新参数的时候等类似的情况。
在动态DAG生成时,避免顶层代码更为重要
配置方式
- 通过环境变量
- 通过外部提供,生成Python 代码,包含DAG的元数据
- 通过外部提供,在DAG文件夹中生成配置的元数据
改善方法
启用缓存(enabling caching):https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#config-secrets-use-cache
在顶层代码中,使用jinja模板的变量在任务运行之前不会生成请求,而如果未启用缓存,则每次调度器解析dag文件时,Variable.get()都会生成一个请求。在不启用缓存的情况下使用Variable.get()将导致dag文件处理中的性能次优。在某些情况下,这可能导致dag文件在完全解析之前超时。
3.1.5 更改DAG之后触发
注意
避免在更改DAG或在DAG文件夹中更改的任何其他附带文件后立即触发DAG。我们需要给系统足够的反应时间来处理修改后的文件。
处理步骤
- 文件必须被分配到调度器,同冲通过分布式文件系统或者Git-Sync
- 调度器分割Python文件并将其存储至数据库(取决于用户配置、系统速度、文件数量、DAG数量、修改的数量、文件大小、调度器数量、CPU速度,可能耗时几秒至1分钟,极端情况下会消耗好几分钟,应该等到你的DAG出现在UI端之后再进行触发)
调试方法
如果处理更改的时间过长,则应该考虑微调以下设置
- scheduler_idle_sleep_time
- min_file_process_interval
- dag_dir_list_interval
- parsing_processes
- file_parsing_sort_mode
3.2 降低DAG复杂度
3.2.1 概述
虽然Airflow擅长处理很多的包含很多Task的DAG 以及其间的关系,但是如果有很多的复杂DAG,将有可能对于调度产生负面影响。让任务保持高效运行的方法就包括降低DAG的复杂度
3.2.2 优化方式
复杂度没有衡量标准,但是可以尽可能的进行优化
- 让DAG更快载入:不使用Top Level Code
- 让DAG的结构更加接近线性:每一个任务依赖都对于调度和执行增加额外的操作,线性DAG(如A -> B -> C)比复杂的树状DAG执行的更快。没有了每次执行之后的可能执行的选项,会有更加良好的调度性能
- 让每个文件中的DAG尽量的少:每个文件只能被一个文件处理器分割,让每个文件有更少的可扩展性能够有助于执行速度。
- 写高效的Python脚本
3.3 测试DAG
3.3.1 DAG加载器测试
此测试应确保DAG不包含在加载时引发错误的代码段。用户不需要编写额外的代码来运行此测试。确保DAG能够正确加载
#测试有无问题
python your-dag-file.py
#测试加载时间
time python airflow/example_dags/example_python_operator.py
3.3.2 单元测试
确保DAG中没有错误代码,可以写单个任务/DAG的单元测试
#加载DAG的单元测试
import pytest
from airflow.models import DagBag
@pytest.fixture()
def dagbag():
return DagBag()
def test_dag_loaded(dagbag):
dag = dagbag.get_dag(dag_id="hello_world")
assert dagbag.import_errors == {}
assert dag is not None
assert len(dag.tasks) == 1
#DAG结构的单元测试
def assert_dag_dict_equal(source, dag):
assert dag.task_dict.keys() == source.keys()
for task_id, downstream_list in source.items():
assert dag.has_task(task_id)
task = dag.get_task(task_id)
assert task.downstream_task_ids == set(downstream_list)
def test_dag():
assert_dag_dict_equal(
{
"DummyInstruction_0": ["DummyInstruction_1"],
"DummyInstruction_1": ["DummyInstruction_2"],
"DummyInstruction_2": ["DummyInstruction_3"],
"DummyInstruction_3": [],
},
dag,
)
#自定义Operator的单元测试
import datetime
import pendulum
import pytest
from airflow import DAG
from airflow.utils.state import DagRunState, TaskInstanceState
from airflow.utils.types import DagRunType
DATA_INTERVAL_START = pendulum.datetime(2021, 9, 13, tz="UTC")
DATA_INTERVAL_END = DATA_INTERVAL_START + datetime.timedelta(days=1)
TEST_DAG_ID = "my_custom_operator_dag"
TEST_TASK_ID = "my_custom_operator_task"
@pytest.fixture()
def dag():
with DAG(
dag_id=TEST_DAG_ID,
schedule="@daily",
start_date=DATA_INTERVAL_START,
) as dag:
MyCustomOperator(
task_id=TEST_TASK_ID,
prefix="s3://bucket/some/prefix",
)
return dag
def test_my_custom_operator_execute_no_trigger(dag):
dagrun = dag.create_dagrun(
state=DagRunState.RUNNING,
execution_date=DATA_INTERVAL_START,
data_interval=(DATA_INTERVAL_START, DATA_INTERVAL_END),
start_date=DATA_INTERVAL_END,
run_type=DagRunType.MANUAL,
)
ti = dagrun.get_task_instance(task_id=TEST_TASK_ID)
ti.task = dag.get_task(task_id=TEST_TASK_ID)
ti.run(ignore_ti_state=True)
assert ti.state == TaskInstanceState.SUCCESS
# Assert something related to tasks results.
3.3.4 自检
你可以使用DAG中的检查,确保处理的结果和预期结果一致。
3.3.5 环境
如果可能的话,在将DAG部署到生产环境之前,保留一个临时环境来测试完整的DAG运行。确保您的DAG参数化以更改变量,例如,S3操作的输出路径或用于读取配置的数据库。不要在DAG内硬编码值,然后根据环境手动更改它们。
您可以使用环境变量来参数化DAG。
import os
dest = os.environ.get("MY_DAG_DEST_PATH", "s3://default-target/path/")
3.4 模拟变量和连接
当不方便测试Varibale和conneciton的时候,可以使用unittest.mock.patch.dict()模拟os.environ
with mock.patch.dict("os.environ", AIRFLOW_VAR_KEY="env-value"):
assert "env-value" == Variable.get("key")
总结
-
airflow 基础pipeline形式
#定义整个DAG with DAG( default_args={}, dag_id='test', start_date=datetime(year,month,day), schedule='@daily', tags=['test'], max_active_run=1, catchup=False ) as dag: #定义Operator/task要执行的函数 def operator1(): print('1') def operator2(): print('2') #定义task first_task = PythonOperator( task_id = 'test_task1', python_callable=operator1, ) second_task = PythonOperator( task_id = 'test_task2', python_callable=operator2, ) #定义task的执行顺序 chain(first_task,second_task)
后记
Best Practice中还有很多有效的内容,请移步官方文档进行阅读https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html#metadata-db-maintenance
参考资料
参考DAG中参数的含义
中文版的Ariflow文档,但是感觉像是机翻的内容