本次部署的Airflow版本为1.10.4,依赖的Python版本为Python3.6
Python的安装就不多做赘述,网上教程太多了。如果是本地部署测试,可以新启一个python的测试环境virtualenv
#1.安装virtualenv
pip3 install virtualenv
#2.创建目录
mkdir Myproject
cd Myproject
#3.创建独立运行环境-命名,得到独立第三方包的环境,并且指定解释器是python3
virtualenv --no-site-packages --python=python3 venv
#4.进入虚拟环境,此时进入虚拟环境(venv)Myproject
source venv/bin/activate
#5.安装第三方包,我把需要用到的包都放到配置文件中了(可选),如果安装速度慢可以选择国内的源(可选)
pip install --upgrade -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
包配置文件
airflow
alembic==1.0.11
amqp==2.5.1
apache-airflow==1.10.4
apispec==2.0.2
asn1crypto==0.24.0
attrs==19.1.0
Babel==2.7.0
bcrypt==3.1.7
billiard==3.6.1.0
cached-property==1.5.1
celery==4.3.0
certifi==2019.6.16
cffi==1.12.3
chardet==3.0.4
Click==7.0
colorama==0.4.1
colorlog==4.0.2
configparser==3.5.3
croniter==0.3.30
cryptography==2.7
defusedxml==0.6.0
dill==0.2.9
docutils==0.15.2
dumb-init==1.2.2
Flask==1.1.1
Flask-Admin==1.5.3
Flask-AppBuilder==1.13.1
Flask-Babel==0.12.2
Flask-Bcrypt==0.7.1
Flask-Caching==1.3.3
Flask-JWT-Extended==3.21.0
Flask-Login==0.4.1
Flask-OpenID==1.2.5
Flask-SQLAlchemy==2.4.0
flask-swagger==0.2.13
Flask-WTF==0.14.2
flower==0.9.3
funcsigs==1.0.0
future==0.16.0
gunicorn==19.9.0
idna==2.8
importlib-metadata==0.19
iso8601==0.1.12
itsdangerous==1.1.0
Jinja2==2.10.1
json-merge-patch==0.2
jsonschema==3.0.2
kombu==4.6.4
lazy-object-proxy==1.4.2
lockfile==0.12.2
Mako==1.1.0
Markdown==2.6.11
MarkupSafe==1.1.1
marshmallow==2.19.5
marshmallow-enum==1.5.1
marshmallow-sqlalchemy==0.17.0
more-itertools==7.2.0
mysqlclient==1.3.14
numpy==1.17.0
ordereddict==1.1
pandas==0.25.1
pendulum==1.4.4
prison==0.1.0
protobuf==3.9.1
psutil==5.6.3
pycparser==2.19
Pygments==2.4.2
PyJWT==1.7.1
pyrsistent==0.15.4
python-daemon==2.1.2
python-dateutil==2.8.0
python-editor==1.0.4
python3-openid==3.1.0
pytz==2019.2
pytzdata==2019.2
PyYAML==5.1.2
requests==2.22.0
setproctitle==1.1.10
six==1.12.0
snakebite==2.11.0
SQLAlchemy==1.3.7
tabulate==0.8.3
tenacity==4.12.0
termcolor==1.1.0
text-unidecode==1.2
thrift==0.11.0
tornado==5.1.1
tzlocal==1.5.1
unicodecsv==0.14.1
urllib3==1.25.3
vine==1.3.0
Werkzeug==0.15.5
WTForms==2.2.1
zipp==0.6.0
zope.deprecation==4.4.0
安装完毕之后就可以启动Airflow了
Airflow要贴合生产使用正常是需要更改配置的,内置的SqLite3并不是特别好用,一般会换成别的数据库,推荐Mysql,原因是简单易用;还有一点就是Executor的修改,生产上一般都是使用CeleryExecutor,也就是分布式任务队列。
[core]
# The home folder for airflow, default is ~/airflow
# airflow的home目录指定
airflow_home = /project/workflow
# airflow的dags目录指定
dags_folder = /project/workflow/dags
# airflow的日志文件目录指定
base_log_folder = /project/workflow/logs
# 时区指定(时区指定,需要多处修改)
default_timezone = Asia/Shanghai
# 修改executor的模式为CeleryExecutor
executor = CeleryExecutor
# 存储数据的数据库修改为mysql
sql_alchemy_conn = mysql://root:@localhost/airflow
# 不加载默认的example dag
load_examples = False
# airflow插件位置修改
plugins_folder = /project/workflow/plugins
# 账户安全性配置,只通过账号密码控制(可选)
[webserver]
authenticate = True
auth_backend = airflow.contrib.auth.backends.password_auth
启用密码身份验证后,需要先创建初始用户,然后其他账户才能登陆,进入 python 命令行,执行以下命名, 或者通过运行python脚本,设置 airflow 的用户名和密码,若提示缺失包,直接通过 pip 安装即可,用户信息会存入 users 表中
import airflow
from airflow import models, settings
from airflow.contrib.auth.backends.password_auth import PasswordUser
user = PasswordUser(models.User())
user.username = 'new_user_name'
user.email = '[email protected]'
user.password = 'set_the_password'
session = settings.Session()
session.add(user)
session.commit()
session.close()
通过账号密码+角色权限控制来登陆
[webserver]
security = Flask AppBuilder
secure_mode = True
rbac=True
ps: 和第一种方式不共存,必须删除 authenticate 和 auth_backend 的配置
添加配置之后,需要重建数据库表:
airflow resetdb
这种情况下,创建用户必须使用命令行 airflow create_user
例如:
airflow create_user --lastname user --firstname admin --username admin --email [email protected] --role Admin --password admin123
airflow create_user --lastname user --firstname view --username view --email [email protected] --role Viewer --password view123
此时, admin 角色的用户 UI 界面会出现 Security 的 Tab, 就可以愉快的通过 UI 界面来添加/修改用户了
时区的修改,界面右上角的时间目前没有配置修改,只能改源码实现
需要在 dag 的 arg 中 start_date 指定时区,这样定时任务就会按照上海时间来执行
import pendulum
from datetime import datetime
from airflow.models import DAG
from airflow.operators.bash_operator import BashOperator
from airflow.operators.dummy_operator import DummyOperator
local_tz = pendulum.timezone("Asia/Shanghai")
args = {
'owner': 'Airflow',
'start_date': datetime(2019, 1, 1, tzinfo=local_tz),
}
dag = DAG(
dag_id='bash_test',
default_args=args,
schedule_interval='* * * * *',
)
启动Airflow
# 主服务启动,webserver 是一个守护进程,它接受 HTTP 请求,允许你通过 Python Flask Web 应用程序与 airflow 进行交互。
Airflow webserver
# 调度器启动,scheduler 是一个守护进程,它周期性地轮询任务的调度计划,以确定是否触发任务执行。
Airflow scheduler
# worker启动,worker 是一个守护进程,它启动 1 个或多个 Celery 的任务队列,负责执行具体 的 DAG 任务。
Airflow worker
然后皆可以登录web界面操作Airflow了。
# -*- coding: utf-8 -*-
#用于初始化DAG对象
from airflow import DAG
#众多operators的一种,用于执行bash命令
from airflow.operators.bash_operator import BashOperator
#日期时间相关的包
from datetime import timedelta, datetime
# DAG时区指定
local_tz = pendulum.timezone("Asia/Shanghai")
# ------------------------------------------------------------------------------
# 当创建DAG对象或者task时,可以明确地传递一系列的参数来描述它。可以定义一个字典来实现。default_args作用于该DAG下的所有task
default_args = {'owner': 'admin',
'depends_on_past': False,#此DAG开始执行的日期
'start_date': datetime(2019, 8, 25, tzinfo=local_tz),#此DAG开始执行的日期
'email': '[email protected]',#任务失败、重试时用于接受邮件
'email_on_failure': True,#任务失败是是否接收邮件
'email_on_retry': True,#任务重试时是否接收邮件
'retries': 1,#重试次数
'retry_delay': timedelta(minutes=1),#重试间隔
# 'priority_weight': 10,
# 'end_date': datetime(2016, 1, 1),
}
# -------------------------------------------------------------------------------
# 初始化DAG对象
dag = DAG(
'test_dag',#dag_id唯一标示
default_args=default_args,
description='my first DAG',
schedule_interval='30 2 * * *'#DAG schedule间隔,支持cron格式(0 7 * * *)
# -------------------------------------------------------------------------------
# first operator
t1 = BashOperator(
task_id='date_task',#task_id唯一标示
bash_command='date ',#具体的bash命令
dag=dag)
t2 = BashOperator(
task_id='sleep',
bash_command='sleep 5',
retries=3, #task里传递的参数值优先于dag(此task的重试次数是3而不是1)
dag=dag)
t1 >> t2 #等价于 t1.set_downstream(t2),t2 << t1,t2.set_upstream(t1)
#如果有多个task,task列表也可以设置依赖关系
#t1.set_downstream([t2, t3])
#t1 >> [t2, t3]
#[t2, t3] << t1
这基本是一个DAG对象的骨架,DAG()除了上述列出之外还有很多的参数可以设置,来提高task的运行效率。operators同理,除了最基本的BashOperator还有诸如PythonOperator,mysql_to_hiveOperator等等,每种operator都有相应的功能,可以根据业务场景任意挑选
python ~/airflow/dags/test.py (如果脚本不报错表示没有语法错误且你的airflow运行环境正常)
airflow list_dags (输出显示可用的dags)
airflow list_tasks tutorial (输出显示tutorial dag下所有的task)
airflow list_tasks tutorial --tree (以树形结构输出显示tutorial dag下所有的task)
airflow test tutorial print_date 20159-09-01 (测试该task是否可以正确运行)
airflow的scheduler监控所有的dag和task,根据其依赖关系触发执行。在后台, scheduler启动了一个linux子进程来监控DAG_FOLDER目录,收集dag的python文件(.py)和检测可用的task是否可以被触发执行。airflow scheduler被设计为持续性的后台服务来运行,可以用airflow scheduler命令来启动,其将读取airflow.cfg配置文件。
airflow的调度模式是在当前的调度周期触发上一个调度周期,举例说明:
对于天调度。一个dag的schedule_interval设置为0 7 * * ,现在是2019-01-02 08:00:00,那么scheduler执行的最后一个task实例是2019-01-01T07:00:00*,而不是2019-01-02T07:00:00
对于周调度。举例说明:start_date为2019-02-01,schedule_interval为0 7 * * 1,now是2019-01-11 07:00:00,那么scheduler执行的最后一个task实例是2019-01-04T07:00:00,因为从start_date到2019-01-11期间,只有2019-02-04到2019-02-10是一个完整的周期。如果start_date和now不变,schedule_interval改为0 7 * * 2,则没有实例可执行
airflow dag除了scheduler在后天自动执行外,还可以在命令行airflow trigger_dag
和webUI手动触发执行。命令行执行时需要传入run_id。
airflow关于并行度在airflow.cfg
里有4个配置项:
parallelism: 指整个Airflow在任何一刻能同时运行的Task Instance的数量,这个数量跟DAG无关
max_active_runs_per_dag: 指同一个Dag能被同时激活的Dag Run的数量
dag_concurrency: 指同一个Dag Run中能同时运行的Task Instance的个数
non_pooled_task_slot_count: 指默认的Pool能同时运行的Task Instance的数量,如果你的Task没有指定Pool选项,那么这个Task就是属于这个默认的Pool的
这次主要介绍的是Airflow新版本的钉钉机器人插件改企业微信机器人,Airflow刚开始的告警只有内置的邮件,现在很多公司办公都选择企业微信或者是钉钉,能用他们进行告警显得尤为重要,1.10版本之前的Airflow要实现钉钉或者企微告警只能通过改造源码,模仿邮件告警写新的告警系统。
官网链接:https://airflow.apache.org/howto/operator/dingding.html
使用起来很简单,官方文档很详细,有个坑说一下。
这边文档说吧dingding_default的连接改一下,并不是改下边的配置,而是要去改web界面的connection,不然运行的时候会报数据库找不到配置信息。
直接贴代码
WechatHook.py文件
import json
import requests
from airflow import AirflowException
from airflow.hooks.http_hook import HttpHook
class WechatHook(HttpHook):
def __init__(self,
wechat_conn_id='wechat_default',
message_type='text',
message=None,
at_mobiles=None,
at_all=False,
*args,
**kwargs
):
super(WechatHook, self).__init__(http_conn_id=wechat_conn_id, *args, **kwargs)
self.message_type = message_type
self.message = message
self.at_mobiles = at_mobiles
self.at_all = at_all
def _get_endpoint(self):
"""
Get WeChat endpoint for sending message.
"""
conn = self.get_connection(self.http_conn_id)
token = conn.password
if not token:
raise AirflowException('WeChat token is requests but get nothing, '
'check you conn_id configuration.')
return 'cgi-bin/webhook/send?key={}'.format(token)
def _build_message(self):
"""
Build different type of WeChat message
As most commonly used type, text message just need post message content
rather than a dict like ``{'content': 'message'}``
"""
if self.message_type in ['text', 'markdown']:
data = {
'msgtype': self.message_type,
self.message_type: {
'content': self.message
} if self.message_type == 'text' else self.message,
'at': {
'atMobiles': self.at_mobiles,
'isAtAll': self.at_all
}
}
else:
data = {
'msgtype': self.message_type,
self.message_type: self.message
}
return json.dumps(data)
def get_conn(self, headers=None):
"""
Overwrite HttpHook get_conn because just need base_url and headers and
not don't need generic params
:param headers: additional headers to be passed through as a dictionary
:type headers: dict
"""
conn = self.get_connection(self.http_conn_id)
self.base_url = conn.host if conn.host else 'https://qyapi.weixin.qq.com'
session = requests.Session()
if headers:
session.headers.update(headers)
return session
def send(self):
"""
Send WeChat message
"""
support_type = ['text', 'link', 'markdown', 'actionCard', 'feedCard']
if self.message_type not in support_type:
raise ValueError('WeChatWebhookHook only support {} '
'so far, but receive {}'.format(support_type, self.message_type))
data = self._build_message()
self.log.info('Sending WeChat type %s message %s', self.message_type, data)
resp = self.run(endpoint=self._get_endpoint(),
data=data,
headers={'Content-Type': 'application/json'})
# WeChat success send message will with errcode equal to 0
if int(resp.json().get('errcode')) != 0:
raise AirflowException('Send WeChat message failed, receive error '
'message %s', resp.text)
self.log.info('Success Send WeChat message')
WechatOperator.py文件
from airflow.operators.bash_operator import BaseOperator
from airflow.utils.decorators import apply_defaults
from wechat_hook import WechatHook
class WechatOperator(BaseOperator):
template_fields = ('message',)
ui_color = '#4ea4d4' # Wechat icon color
@apply_defaults
def __init__(self,
wechat_conn_id='wechat_default',
message_type='text',
message=None,
at_mobiles=None,
at_all=False,
*args,
**kwargs):
super(WechatOperator, self).__init__(*args, **kwargs)
self.wechat_conn_id = wechat_conn_id
self.message_type = message_type
self.message = message
self.at_mobiles = at_mobiles
self.at_all = at_all
def execute(self, context):
self.log.info('Sending WeChat message.')
hook = WechatHook(
self.wechat_conn_id,
self.message_type,
self.message,
self.at_mobiles,
self.at_all
)
hook.send()
两个文件直接放到plugins目录下即可
operator可以通过编写函数通过回调来进行操作,然后将该函数传递给on_success_callback、on_failure_callback或on_retry_callback。简单来说就是一个task执行之后,可以通过不同的状态执行回调函数发送消息到企业微信,回调的配置可以配置在arg中,也可以配置在task中。
# -*- coding: utf-8 -*-
from airflow import DAG
from airflow.operators.bash_operator import BashOperator
from datetime import timedelta, datetime
from wechat_operator import WechatOperator
def failure_callback(context):
message = 'AIRFLOW TASK FAILURE TIPS:\n' \
'DAG: {}\n' \
'TASKS: {}\n' \
'Reason: {}\n' \
.format(context['task_instance'].dag_id,
context['task_instance'].task_id,
context['exception'])
return WechatOperator(
task_id='failure_callback',
dingding_conn_id='wechat_default',
message_type='text',
message=message,
at_all=True,
).execute(context)
def success_callback(context):
message = 'AIRFLOW TASK SUCCESS TIPS:\n' \
'DAG: {}\n' \
'TASKS: {}\n' \
.format(context['task_instance'].dag_id,
context['task_instance'].task_id)
return WechatOperator(
task_id='success_callback',
dingding_conn_id='wechat_default',
message_type='text',
message=message,
at_all=True,
).execute(context)
def retry_callback(context):
message = 'AIRFLOW TASK RETRY TIPS:\n' \
'DAG: {}\n' \
'TASKS: {}\n' \
'Reason: {}\n' \
.format(context['task_instance'].dag_id,
context['task_instance'].task_id,
context['exception'])
return WechatOperator(
task_id='retry_callback',
dingding_conn_id='wechat_default',
message_type='text',
message=message,
at_all=True,
).execute(context)
# -------------------------------------------------------------------------------
# these args will get passed on to each operator
# you can override them on a per-task basis during operator initialization
default_args = {'owner': 'admin',
'depends_on_past': False,
'start_date': datetime(2019, 8, 25),
'email': '[email protected]',
'email_on_failure': False,
'email_on_retry': False,
'retries': 1,
'retry_delay': timedelta(minutes=1),
'on_failure_callback': failure_callback,
'on_success_callback': success_callback,
'on_retry_callback': retry_callback
}
# -------------------------------------------------------------------------------
# dag
dag = DAG(
'callback_test_dag',
default_args=default_args,
description='my first DAG',
schedule_interval=timedelta(days=1))
# -------------------------------------------------------------------------------
# first operator
date_operator = BashOperator(
task_id='date_task',
bash_command='abc ',
dag=dag)
date_operator