元数据库:这个数据库存储有关任务状态的信息。
调度器:Scheduler 是一种使用 DAG 定义结合元数据中的任务状态来决定哪些任务需要被执行以及任务执行优先级的过程。调度器通常作为服务运行。
执行器:Executor 是一个消息队列进程,它被绑定到调度器中,用于确定实际执行每个任务计划的工作进程。有不同类型的执行器,每个执行器都使用一个指定工作进程的类来执行任务。例如,LocalExecutor 使用与调度器进程在同一台机器上运行的并行进程执行任务。其他像 CeleryExecutor 的执行器使用存在于独立的工作机器集群中的工作进程执行任务。
Workers:这些是实际执行任务逻辑的进程,由正在使用的执行器确定。
时间依赖:任务需要等待某一个时间点触发。
外部系统依赖:任务依赖外部系统需要调用接口去访问。
任务间依赖:任务 A 需要在任务 B 完成后启动,两个任务互相间会产生影响。
资源环境依赖:任务消耗资源非常多, 或者只能在特定的机器上执行。
pip install apache-airflow
# 初始化数据库
airflow initdb
# 上面的命令默认在家目录下创建 Airflow 文件夹和相关配置文件
# 也可以使用以下命令来指定目录
export AIRFLOW_HOME={yourpath}/airflow
# 配置数据库
# vim airflow/airflow.cfg
# 修改 sql_alchemy_conn
# 守护进程运行 webserver,默认端口为8080,也可以通过`-p`来指定端口
airflow webserver -D
# 守护进程运行调度器
airflow scheduler -D
定义第一个 DAG在 AIRFLOW_HOME 目录下新建 DAGs 文件夹,后面的所有 DAG 文件都要存储在这个目录。新建 demo.py,语句含义见注释。
from datetime import datetime, timedelta
from airflow import DAG
from airflow.utils.dates import days_ago
from airflow.operators.bash_operator import BashOperator
from airflow.operators.python_operator import PythonOperator
from airflow.operators.dummy_operator import DummyOperator
def default_options():
default_args = {
'owner': 'airflow', # 拥有者名称
'start_date': days_ago(1), # 第一次开始执行的时间,为 UTC 时间(注意不要设置为当前时间)
'retries': 1,# 失败重试次数
'retry_delay': timedelta(seconds=5) # 失败重试间隔
}
return default_args
# 定义 DAG
def test1(dag):
t = "echo 'hallo world'"
# operator 支持多种类型, 这里使用 BashOperator
task = BashOperator(
task_id='test1', # task_id
bash_command=t, # 指定要执行的命令
dag=dag # 指定归属的 DAG
)
return task
def hello_world_1():
current_time = str(datetime.today())
print('hello world at {}'.format(current_time))
def test2(dag):
# PythonOperator
task = PythonOperator(
task_id='test2',
python_callable=hello_world_1, # 指定要执行的函数
dag=dag)
return task
def test3(dag):
# DummyOperator
task = DummyOperator(
task_id='test3',
dag=dag)
return task
with DAG(
'test_task', # dag_id
default_args=default_options(), # 指定默认参数
schedule_interval="@once" # 执行周期
) as d:
task1 = test1(d)
task2 = test2(d)
task3 = test3(d)
task1 >> task2 >> task3 # 指定执行顺序
写完后执行 python $AIRFLOW_HOME/dags/demo.py 检查是否有错误,如果命令行没有报错,就表示没问题。
Web UI打开 localhost:8080。主视图:
# 测试任务,格式:airflow test dag_id task_id execution_time
airflow test test_task test1 2019-09-10
# 查看生效的 DAGs
airflow list_dags -sd $AIRFLOW_HOME/dags
# 开始运行任务(同 web 界面点 trigger 按钮)
airflow trigger_dag test_task
# 暂停任务
airflow pause dag_id
# 取消暂停,等同于在 web 管理界面打开 off 按钮
airflow unpause dag_id
# 查看 task 列表
airflow list_tasks dag_id 查看task列表
# 清空任务状态
airflow clear dag_id
# 运行task
airflow run dag_id task_id execution_date
Airflow 核心原理分析
JOB:最上层的工作。分为 SchedulerJob、BackfillJob 和 LocalTaskJob。SchedulerJob 由 Scheduler 创建,BackfillJob 由 Backfill 创建,LocalTaskJob 由前面两种 Job 创建。
DAG:有向无环图,用来表示工作流。
DAG Run:工作流实例,表示某个工作流的一次运行(状态)。
Task:任务,工作流的基本组成部分。
TaskInstance:任务实例,表示某个任务的一次运行(状态)。
刷新 DAGs
收集新的 DagRuns
执行 DagRuns(包括更新 DagRuns 的状态为成功或失败)
唤醒 executor/心跳检查
遍历 DAGs 路径下的所有 DAG 文件,启动一定数量的进程(进程池),并且给每个进程指派一个 DAG 文件。每个 DagFileProcessor 解析分配给它的 DAG 文件,并根据解析结果在DB中创建 DagRuns 和 TaskInstance。
在 scheduler_loop 中,检查与活动 DagRun 关联的 TaskInstance 的状态,解析 TaskInstance 之间的任何依赖,标识需要被执行的 TaskInstance,然后将它们添加至 executor 队列,将新排列的 TaskInstance 状态更新为 QUEUED状态。
每个可用的 executor 从队列中取一个 TaskInstance,然后开始执行它,将此 TaskInstance 的数据库记录更新为 SCHEDULED。
当一个 TaskInstance 完成运行,关联的 executor 就会报告到队列并更新数据库中的 TaskInstance 的状态(例如“完成”、“失败”等)。
一旦所有的 DAG 处理完毕后,就会进行下一轮循环处理。这里还有一个细节就是上一轮的某个 DAG 的处理时间可能很长,导致到下一轮处理的时候这个 DAG 还没有处理完成。Airflow 的处理逻辑是在这一轮不为这个 DAG 创建进程,这样就不会阻塞进程去处理其余 DAG。
Enumerate the all the files in the DAG directory.
Start a configurable number of processes and for each one, assign a DAG file to process.
In each child process, parse the DAG file, create the necessary DagRuns given the state of the DAG's task instances, and for all the task instances that should run, create a TaskInstance (with the SCHEDULED state) in the ORM.
Back in the main scheduler process, query the ORM for task instances in the SCHEDULED state. If any are found, send them to the executor and set the task instance state to QUEUED.
If any of the child processes have finished, create another process to work on the next file in the series, provided that the number of running processes is less than the configured limit.
Once a process has been launched for all of the files in the DAG directory, the cycle is repeated. If the process to parse a particular DAG file is still running when the file's turn comes up in the next cycle, a new process is not launched and a process for the next file in the series is launched instead. This way, a DAG file that takes a long time to parse does not necessarily block the processing of other DAGs.
following_schedule() 计算当前 DAG 的下一次调度时间
previous_schedule() 计算当前 DAG 的上一次调度时间
get_dagrun() 返回给定执行日期的 dagrun(如果存在)
create_dagrun() 创建一个包括与此 DAG 相关任务的 dagrun
ckear() 清除指定日期范围内与当前 DAG 相关的一组任务实例
run() 实例化为 BackfillJob 同时调用 job.run()
ID_PREFIX = 'scheduled__'
ID_FORMAT_PREFIX = ID_PREFIX + '{0}'
id = Column(Integer, primary_key=True)
dag_id = Column(String(ID_LEN))
execution_date = Column(UtcDateTime, default=timezone.utcnow)
start_date = Column(UtcDateTime, default=timezone.utcnow)
end_date = Column(UtcDateTime)
_state = Column('state', String(50), default=State.RUNNING)
run_id = Column(String(ID_LEN))
external_trigger = Column(Boolean, default=True)
conf = Column(PickleType)
method:
get_dag() 返回与当前 DagRun 相关的 Dag
gettaskinstances() 返回与当前 DagRun 的所有 TaskInstances
update_state() 根据 TaskInstances 的状态确定 DagRun 的总体状态
getlatestruns() 返回每个 Dag 的最新一次 DagRun
__tablename__ = "task_instance"
task_id = Column(String(ID_LEN), primary_key=True)
dag_id = Column(String(ID_LEN), primary_key=True)
execution_date = Column(UtcDateTime, primary_key=True)
start_date = Column(UtcDateTime)
end_date = Column(UtcDateTime)
duration = Column(Float)
state = Column(String(20))
_try_number = Column('try_number', Integer, default=0)
max_tries = Column(Integer)
hostname = Column(String(1000))
unixname = Column(String(1000))
job_id = Column(Integer)
pool = Column(String(50), nullable=False)
queue = Column(String(256))
priority_weight = Column(Integer)
operator = Column(String(1000))
queued_dttm = Column(UtcDateTime)
pid = Column(Integer)
executor_config = Column(PickleType(pickler=dill))
method:
get_dagrun() 返回当前 TaskInstance 的 DagRun
run() TaskInstance run
gettemplatecontext() 通过 Jinja2 模板获取上下文
xcom_push() 创建一个 XCom 可用于 task 发送参数
xcom_pull() 创建一个 XCom 可用于 task 接收参数
def _execute(self):
"""
The actual scheduler loop. The main steps in the loop are:
#. Harvest DAG parsing results through DagFileProcessorAgent
#. Find and queue executable tasks
#. Change task instance state in DB
#. Queue tasks in executor
#. Heartbeat executor
#. Execute queued tasks in executor ake_aware(execution_date,
self.task.dag.timezone)
"""
self.processor_agent = DagFileProcessorAgent() # 通过检查当前 processor 数量来控制进程个数
self.executor.start()
# Start after resetting orphaned tasks to avoid stressing out DB.
self.processor_agent.start() # 在解析 DAG 文件时,只会对最近修改过的文件进行解析
execute_start_time = timezone.utcnow()
# For the execute duration, parse and schedule DAGs
while (timezone.utcnow() - execute_start_time).total_seconds() < \
self.run_duration or self.run_duration < 0:
# Starting Loop...
self.processor_agent.heartbeat() # 控制 DagFileProcessor 解析 DAG 文件的速度
# Harvesting DAG parsing results
simple_dags = self.processor_agent.harvest_simple_dags()
if len(simple_dags) > 0:
self._execute_task_instances()
...
# Call heartbeats
self.executor.heartbeat()
# heartbeat() 中根据 parallelism 得出当前可用的 slots 数量,
# 决定 execute_async 多少个 task
# Process events from the executor
self._process_executor_events(simple_dag_bag)
# Ran scheduling loop for all tasks done
...
# Stop any processors
self.processor_agent.terminate()
# Verify that all files were processed, and if so, deactivate DAGs that
# haven't been touched by the scheduler as they likely have been
# deleted.
...
self.executor.end()
method:
createdagrun() 根据调度周期检查是否需要为 DAG 创建新的 DagRun。如果已调度,则返回 DagRun,否则返回 None
process_file() 解析 DAG 定义文件
executetask_instances() 尝试执行调度器调度过的 TaskInstances
There are three steps:
-
Pick TaskInstances by priority with the constraint that they are in the expected states and that we do exceed maxactiveruns or pool limits.
Change the state for the TaskInstances above atomically.
Enqueue the TaskInstances in the executor.
reduceinchunks() 用来进行小的分批处理