Airflow 是 Airbnb 开发的用于工作流管理(管理和调度各种离线定时 Job ,可以替代 crontab)的开源平台,它以编程方式创作,自带 web UI 和调度。基于有向无环图(DAG),可以定义一组有依赖的任务,并按照依赖依次执行。提供了丰富的命令行工具用于系统管控,而其web管理界面同样也可以方便的管控调度任务,并且对任务运行状态进行实时监控,方便系统的运维和管理。
Airflow的调度,有定义图(DAG)、任务(Task)、依赖、监控之类的东西,基本上数仓是一个图对应一个调度,很相似的也可以放在一个图里做。
DAG 的定义使用 Python 完成的,其实就是一个 Python 文件,存放在 DAG 目录,Airflow 会动态的从这个目录构建 DAG object,每个 DAG object 代表了一个 workflow,每个 workflow 都可以包含任意个 task。
Gitlab持续发布自动部署方案
查看是否已安装 airflow 模块:
pip list
# 或
pip show airflow
安装
pip install apache-airflow
# pip3 install "apache-airflow~=2.2.5"
安装版本一般为 2.2.5
找相关负责人开通 airflow 项目git代码权限。
从git 复制 airflow 项目地址(eg: [email protected]:bigdata/airflow-2.0-sa.git),在本地IDE环境新建项目,并创建个人分支。
--- 用于代码测试 ---
(1)登录airflow 服务器:
hw-xx-bigdata-prod-airflow-01-11.22.33.44 (通过跳板机登录)
(2)跳转到airflow 账户:
sudo su - airflow
# 激活环境(. ~/venv/bin/activate)
(3)新建一个自己的目录(/home/airflow/zz),并把待运行脚本上传到这里
rz test_air_flow.py
(4)测试脚本中每一个任务task
airflow tasks test {dag_id} {task_id} {execution_date} -S {script path}
airflow tasks test app_daily_tj t2 '2023-08-28 10' -S app_daily_tj.py
注意:dag_id 尽量多加后缀,不能与其他项目重复!
(5)代码测试通过之后,上传到git;
--- 代码正式例行调度 ---
代码上传到git平台后,过几分钟(运维配置会自动同步到Airflow调度任务),在Airflow平台通过筛选DAG或用户名,可以看到自己的任务,点击开关按钮,其会进行调度(并自动执行过往日期任务)。
本地编写"app_daily_tj.sql"文件和“app_daily_tj.py”文件(两者同名)。“app_daily_tj.py”脚本实际上只是一个配置文件,将DAG的结构指定为代码。
# -*- coding: utf-8 -*-
import os
import requests
import logging
import MySQLdb
import time
from datetime import datetime, timedelta
from utils.validate_metrics_utils import *
from utils.connection_helper import get_hive_cursor, get_db_conn
import airflow
from airflow.contrib.hooks.redis_hook import RedisHook
from airflow.hooks.hive_hooks import HiveCliHook, HiveServer2Hook
from airflow.hooks.mysql_hook import MySqlHook
from airflow.operators.bash_operator import BashOperator
from airflow.operators.dagrun_operator import TriggerDagRunOperator
from airflow.operators.hive_operator import HiveOperator
from airflow.operators.hive_to_mysql import HiveToMySqlTransfer
from airflow.operators.impala_plugin import ImpalaOperator
from airflow.operators.python_operator import PythonOperator
from airflow.operators.mysql_operator import MySqlOperator
from airflow.sensors.external_task_sensor import ExternalTaskSensor
from airflow.sensors.hive_partition_sensor import HivePartitionSensor
from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor
from airflow.models import Variable
from plugins.OdsSchemaUpdate import OdsSchemaUpdate
from plugins.TaskTimeoutMonitor import TaskTimeoutMonitor
from plugins.TaskTouchzSuccess import TaskTouchzSuccess
主要修改owner、日期、邮箱地址,另外此处还可以添加一些程序中需要使用的变量,其会与环境变量一起,在后续程序中可以直接使用。
args = {
'owner': 'zz.song',
'start_date': datetime(2023, 6, 1), # 脚本调度起始时间
# 'start_date': datetime(2023, 6, 1, 23),
'depends_on_past': False,
'retries': 3,
'retry_delay': timedelta(minutes=3),
'email': ['[email protected]'],
'email_on_failure': True,
'email_on_retry': False,
# 'end_date': datetime(2023, 7, 1), # 与start_date配合,可补跑指定范围的任务
# 'queue': 'bash_queue',
# 'pool': 'backfill',
# 'priority_weight': 40,
# 'provide_context': True,
# 'on_failure_callback': send_email_msg # 设置回调函数
# 'end_date': datetime(2016, 1, 1)
}
dag = airflow.DAG(
'app_daily_tj', # dag_id,作为DAG的唯一标识符
schedule_interval = "30 05 * * *", # 每天5:30自动开始执行
# schedule_interval=timedelta(days=1),
default_args=args,
# catchup=False, # 为True会回填start_date至今的任务
# max_active_runs=2,
# concurrency=10,
)
注意:dag_id 尽量与脚本名称保持一致!保证唯一性。
任务Task 可分为“依赖检测任务”和“自定义执行任务”,具体参考“任务拆解”章节。
t2.set_upstream(t1)
t3.set_upstream(t1)
# 其他定义方式 (t2依赖t1)
t1.set_downstream(t2)
t1 >> t2
t2 << t1
# t3依赖t2, t2依赖t1,
t1 >> t2 >> t3
t1.set_downstream([t2, t3])
t1 >> [t2, t3]
[t2, t3] << t1
一个DAG图中,任务可以有多个。
主要是检测文件是否存在。
from airflow.sensors import OssSensor
table_1_task = OssSensor(
task_id = 'table_1_task', # task_id:与依赖的命名对齐
bucket_key = '{hdfs_path_str}/dt={pt}/_SUCCESS'.format(
hdfs_path_str='oss://bd-datalake/bd_dw/table_1', # 可通过元数据信息查看表文件路径
pt='{{ds}}' # 自带环境变量
# pt='{{macros.ds_add(next_ds, -1)}}'
),
poke_interval=60, # 依赖不满足时,一分钟检查一次依赖状态
dag=dag
)
小时级分区表,execution_date 为环境变量
from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor
t1 = S3KeySensor(
task_id='t1',
bucket_key='{hdfs_path_str}/dt={pt}/hour={h}/_SUCCESS'.format(
hdfs_path_str="obs://bd-datalake/table_1",
pt='{{execution_date.strftime("%Y-%m-%d")}}',
h='{{execution_date.strftime("%H")}}'
),
poke_interval=60, # 依赖不满足时,一分钟检查一次依赖状态
dag=dag
)
1. 元数据检测: hive_partition_sensor
通过连接hive元数据所在的mysql数据库,来检查hive表的分区是否存在,速度比较快。
from airflow.sensors.hive_partition_sensor.MetastorePartitionSensor
# 用于检查hive分区是否生成
check_hive_partition= MetastorePartitionSensor(
task_id='check_hive_artition_task',
mysql_conn_id='mysql-conn', # hive元数据库连接,可在web界面的Connection进行配置
schema='default', # 如果是default,那么table里要加上库名
table='db_name.table_name', # 需要检查的hive表名
partition="dt='{{ ds }}'", # 需要检测的分区
dag=dag
)
2. 分区检测:hive_partition_sensor
from airflow.sensors.hive_partition_sensor import HivePartitionSensor
check_hive_partition= HivePartitionSensor(
task_id='check_hive_artition_task',
metastore_conn_id='hive-conn', # hive_metastore连接,可在web界面的Connection进行配置
schema='default', # 如果是default,那么table里要加上库名
table='库名.表名', # 需要检查的hive表名
poke_interval=300, # 两次检查的间隔时间,单位秒。建议该值不小于60。
partition="dt='{{ ds }}'", # 需要检测的分区
# partition="dt='{{ ds }}' and hour='{{ execution_date.strftime(\"%H\") }}'",
dag=dag
)
注意:这类非默认op,需要指定conn_id。
在实例化操作对象时生成任务,从运算符实例化的对象称为构造函数。
t1 = BashOperator(
task_id='print_date', # 任务的唯一标识符
bash_command='date',
dag=dag)
t2 = BashOperator(
task_id='sleep',
bash_command='sleep 5',
retries=3,
dag=dag)
Airflow利用Jinja 模板的强大功能, 为管道作者提供一组内置参数和宏,还为管道作者提供了定义自己的参数,宏和模板的钩子。最常见的模板变量: {{ ds }} -- 今天的“日期戳”
templated_command = """
{% for i in range(5) %}
echo "{{ ds }}"
echo "{{ macros.ds_add(ds, 7)}}"
echo "{{ params.my_param }}"
{% endfor %}
"""
t3 = BashOperator(
task_id='templated',
bash_command=templated_command,
# bash_command='bash -x test.sh 110 {{ ds }}',
# bash_command='bash -x test.sh ', # 一定要留一个空格
# bash_command='sh /home/airflow/test.sh %s'%datetime.now().strftime("%Y-%m-%d"),
# bash_command='bash -x test.sh 110 {{ arg_params.yesterday }}', # 从arg字典取数
params={'my_param': 'Parameter I passed in'},
dag=dag)
Jinja模板常见内置变量说明:
{{ data_interval_start }} 数据间隔的开始(pendulum.DateTime)
{{ data_interval_end }} 数据间隔结束 ( pendulum.DateTime )
{{ ds }} DAG 运行的逻辑日期(YYYY-MM-DD).
{{ ts }} 2018-01-01T00:00:00+00:00
{{ tomorrow_ds }} 2023-08-31
{{ yesterday_ds }} 2023-08-29
{{ execution_date }} DateTime(2023, 8, 30, 10, 0, 0, tzinfo=Timezone('UTC')))
{{ v_execution_time }}
{{ dag }}
{{ task }} 任务对象
{{ task_instance }} task_instance 对象
{{ macros }} 对宏包的引用
{{ params }} 对用户定义的 params 字典的引用
{{ conf }} airflow.configuration.conf代表您的内容的完整配置对象 airflow.cfg。
t5 = PythonOperator(
task_id='t5',
python_callable=fun,
provide_context=True,
op_kwargs={
'v_execution_time': '{{execution_date.strftime("%Y-%m-%d %H:%M:%S")}}'
},
# on_retry_callback=send_email_msg,
dag=dag
)
from plugins.CountriesAppFrame import CountriesAppFrame
def fun(ds, dag, **kwargs):
v_execution_time = kwargs.get('v_execution_time')
hive_hook = HiveCliHook()
args = [
{
"dag": dag,
"is_countries_online": "true",
"db_name": db_name,
"table_name": table_name,
"data_oss_path": hdfs_path,
"is_country_partition": "true",
"is_result_force_exist": "false",
"execute_time": v_execution_time,
"is_hour_task": "false",
"frame_type": "local",
"business_key": "bd"
}
]
cf = CountriesAppFrame(args)
_sql = "\n" + cf.alter_partition() + "\n" + temp_sql
hive_hook.run_cli(_sql)
cf.touchz_success() # 生产success
以上与4.2.3 也是两种执行sql语句以及创建success文件的方式。
hive_table = "ods_xx_di"
# 任务1: 创建分区
app_partiton_task = HiveOperator(
task_id="add_partition_%s" % hive_table,
hql='''
alter table bd_ods.{table} add IF NOT EXISTS partition (dt='{pt}', hour='{dh}')
'''.format(table=hive_table,
pt='{{ds}}',
dh='{{execution_date.strftime("%H")}}',
# hql=open('xx.sql').read().format(pt='{{ ds }}', db='db_name', table='table_name') # 读取sql文件
# schema=hive_db,
dag=dag
)
# 任务2: 创建success文件
cmd = "hadoop fs -touchz obs://bd-datalake/ods_xx_di/"+"dt='{{ ds }}'"+"/_SUCCESS"
touch_success_task = BashOperator(
task_id='touch_success_%s' % str.lower(hive_table),
bash_command=cmd,
dag=dag
)
# 依赖
app_partiton_task >> touch_success_task
from pathlib import Path
from airflow.providers.apache.spark.operators.spark_sql import SparkSqlOperator
# 获取服务器sql代码文件地址
dag_var = Variable.get('var_global', deserialize_json=True) # 获取airflow web中Variable配置参数
sql_path = Path(dag_var['code_path']) / 'bd_script' / 'sql'
tt = SparkSqlOperator(
task_id='tt',
name = 'tt',
conn_id='spark_default',
master = 'yarn',
yarn_queue = 'root.bigdata',
num_executors = 8,
executor_cores = 2,
executor_memory = '4G',
sql=open('{}/xx_df.sql'.format(sql_path)).read().format(pt='{{ ds }}'),
# sql="select count(*) from table1 where dt='2023-03-01'"
dag=dag
)
SSHOperator、PostgresOperator
每天例行统计指定表中分区的数据量
# coding: utf-8
import logging
from datetime import datetime, timedelta
from utils.validate_metrics_utils import *
from impala.dbapi import connect
import airflow
from airflow.operators.python_operator import PythonOperator
from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor
from airflow.hooks.base_hook import BaseHook
# --- 1. 定义默认参数 ---
args = {
'owner': 'zz.song',
'start_date': datetime(2023, 8, 28),
'depends_on_past': False,
'retries': 1,
'retry_delay': timedelta(minutes=5),
}
# --- 2. 定义DAG对象 ---
dag = airflow.DAG(
'count_xx_table_daily',
schedule_interval="20 02 * * *",
default_args=args
)
# --- 3. 定义任务 ---
# 3.1 任务1
t1 = S3KeySensor(
task_id='t1',
bucket_key='{hdfs_path_str}/country_code=NG/dt={pt}/_SUCCESS'.format(
hdfs_path_str="obs://db_name/table_name",
pt='{{ds}}'
),
bucket_name='bd-datalake',
poke_interval=60, # 依赖不满足时,一分钟检查一次依赖状态
dag=dag
)
# 3.2 任务2
t2 = PythonOperator(
task_id='t2',
python_callable=fun,
provide_context=True,
dag=dag
)
# --- 4. 设置依赖关系 ---
t1 >> t2
具体执行的逻辑
def fun(ds, **kwargs): # ds 为调度传入当前时间
dt = datetime.strptime(ds + ' 00:00:00', '%Y-%m-%d %H:%M:%S')
# dt = kwargs.get('execution_date') # 环境变量
# dt = dt.strftime('%Y-%m-%d')
last_7_day = (dt + timedelta(days=-7)).strftime('%Y-%m-%d')
# 连接hive 客户端
conn = BaseHook.get_connection('hive_cli_default')
conn_hive = connect(host=conn.host,
port=conn.port,
timeout=3600,
auth_mechanism='PLAIN',
user=conn.login,
password=conn.password)
cursor = conn_hive.cursor()
cursor.execute('set mapreduce.job.queuename=root.bigdata')
cursor.execute('set hive.vectorized.execution.enabled=false')
sql = '''
select count(1)
from db_name.table_name
where dt='{dt}'
'''.format(dt=last_7_day)
logging.info('Executing: %s', sql)
cursor.execute(sql)
data = cursor.fetchall()
cursor.close()
logging.info(data)
连接其他客户端
# 连接mysql客户端
def get_mysql_conn(conf_name='mysql_default'):
conn = BaseHook.get_connection(conf_name)
conn_mysql = MySQLdb.connect(host=conn.host, port=conn.port, db=conn.schema,
user=conn.login, passwd=conn.password,
charset='utf8mb4', use_unicode=True,
autocommit=True)
return conn_mysql
# 连接redis
def get_redis_connection(conf_name='redis'):
conn = BaseHook.get_connection(conf_name)
return StrictRedis(host=conn.host, port=conn.port)
点击下图小方框,可以查看任务执行详情
查看日志
批量任务执行
from airflow.operators.dummy import DummyOperator
dag_start = DummyOperator(task_id='dag_start', dag=dag)
dag_end = DummyOperator(task_id='dag_end', dag=dag)
dag_start >> [t1,t2, t3] >> t4 >> t5
dag_start >> [a1, a2] >> a3 >> a4 >> dag_end
一般不作更改
def task_timeout_monitor(ds, dag, **kwargs):
msg = [{
"dag": dag,
"db": "db_name",
"table": "{dag_name}".format(dag_name=dag.dag_id),
"partition": "dt={pt}".format(pt=ds),
# "partition": "country_code=NG/dt={pt}".format(pt=ds)
"timeout": "600"
}]
TaskTimeoutMonitor().set_task_monitor(msg)
xx_task_timeout_monitor = PythonOperator(
task_id='task_timeout_monitor',
python_callable=task_timeout_monitor,
provide_context=True,
dag=dag
)
1. 在飞书中新建一个群(2人即可),加入机器人,并找到机器人的webhook地址;
找到此群 --> 设置 --> 群机器人 --> 添加机器人 --> 自定义机器人 --> 添加即可
点击机器人头像,复制其 webhook 地址
def send_feishu_markdown_warning(title, message):
_http_headers = {'content-type': 'application/json'}
_http_feishu_warning_url = 'https://open.feishu.cn/open-apis/bot/v2/hook/f8727e49-bd8e-40f1-8346-00d423d18288'
request_data = {
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": True},
"header": {
"title": {
"content": title,
"tag": "plain_text"
}
},
"elements": [
{
"tag": "markdown",
"content": message + "\n" + " ",
}
]
}
}
response = requests.post(_http_feishu_warning_url, json=request_data, headers=_http_headers)
if response.status_code != 200:
print('预警消息发送失败,http_code=%s,http_message=%s' % (response.status_code, response.reason))
else:
print('预警消息发送成功')
调用
def product_email(**kwargs):
dt = kwargs.get('execution_date') # 环境变量
t_day, t_hour = dt.strftime('%Y-%m-%d %H').split(" ")
msg_1 = 'hello'
msg_2 = '哈喽'
message = f"
日期:{str(t_day)+' '+str(t_hour)+':00:00'} \n 下一行" \
f"
{msg_1}" \
f"
{msg_2}"
send_feishu_markdown_warning('XX服务监控日报', message)
from plugins.QqMailAlert import send_feishu_markdown_warn
def send_lark_msg(context):
execution_date = context.get("execution_date").strftime("%Y-%m-%d")
dag_id = context.get("dag").dag_id
send_feishu_markdown_warn(session_title='数据校验',
warning_message=f"{execution_date} {dag_id} 已延迟产出,请尽快处理!",
token='c42e086d-0b38-4762-ab57-004c45e7d97a',
at_all=True)
from plugins.QqMailAlert import send_mail
def send_email_msg(context):
execution_date = context.get("execution_date").strftime("%Y-%m-%d")
dag_id = context.get("dag").dag_id
sender = '110'
password = '123321'
receiver = '[email protected]'
subject = f"失败警告!"
message = f"{execution_date} {dag_id} 已失败,请尽快处理!http://11.22.33.44:8080/tree?dag_id={dag_id}&root="
send_mail(sender, password, receiver, subject, message)
import requests
_http_headers = {'content-type': 'application/x-www-form-urlencoded'}
request_url = 'https://ops.bddev.com/api/alarm_channel/?aisle_type=TEL&username=devops&api_password=dL54CW&system=other&level=10&receive_obj=123,134&content=XX核心指标日报紧急报警,请及时关注!'
request_data = {"data": [
{"status": True, "code": "OK", "message": "OK", "request_id": "CF700C2E-4C34-5E76-BE2E-8462546F2CD5"}]}
response = requests.get(request_url, json=request_data, headers=_http_headers)
if response.status_code != 200:
print('告警电话拨打失败,http_code=%s,http_message=%s' % (response.status_code, response.reason))
else:
print('告警电话拨打成功')