• 系统研究Airbnb开源项目airflow


    开源项目airflow的一点研究

    调研了一些几个调度系统, airflow 更满意一些. 花了些时间写了这个博文, 这应该是国内技术圈中最早系统性研究airflow的文章了.  转载请注明出处 http://www.cnblogs.com/harrychinese/ .

    ========================
    airflow概况
    ========================
    文档:
    http://airflow.readthedocs.org/en/latest/
    几个调度系统的比较, 可参考:
    http://www.rigongyizu.com/about-workflow-schedule-system/

    Principles:
    动态性: Airflow pipeline是以python code形式做配置, 灵活性没得说了.
    扩展性: 可以自定义operator(运算子), 有几个executor(执行器)可供选择.
    优雅:pipeline的定义很简单明了, 基于jinja模板引擎很容易做到脚本命令参数化.
    扩展性:模块化的结构, 加上使用了message queue来编排多个worker(启用CeleryExcutor), airflow可以做到无限扩展.

    竞品:
    airflow并不是data streaming方案, 所以不是Spark Streaming/Storm的竞品. 和airflow类似的有: Apache Oozie, Linkedin Azkaban.

    比较优势:
    linkedin的Azkaban很不错, UI尤其很赞, 使用java properties文件维护上下游关系, 任务资源文件需要打包成zip, 部署不是很方便.
    Apache Oozie, 使用XML配置, Oozie任务的资源文件都必须存放在HDFS上. 配置不方便同时也只能用于Hadoop.
    Spotify的 Luigi, UI 太烂.
    airflow 总体都不错, 有实用的UI, 丰富的cli工具, Task上下游使用python编码, 能保证灵活性和适应性.


    ========================
    概念:
    ========================
    不用多说概念自然非常重要, 这是理解airflow的基础.

    ---------------
    Operators:
    ---------------  
    基本可以理解为一个抽象化的task, Operator加上必要的运行时上下文就是一个task. 有三类Operator:
      1. Sensor(传感监控器), 监控一个事件的发生.
      2. Trigger(或者叫做Remote Excution), 执行某个远端动作, (我在代码中没有找到这个类别)
      3. Data transfer(数据转换器), 完成数据转换
     
       
    ---------------
    Hooks:
    ---------------    
    Hook是airflow与外部平台/数据库交互的方式, 一个Hook类就像是一个JDBC driver一样. airflow已经实现了jdbc/ftp/http/webhdfs很多hook. 要访问RDBMS数据库 有两类Hook可供选择, 基于原生Python DBAPI的Hook和基于JDBC的Hook, 以Oracle为例,
          OracleHook, 是通过cx_Oracle 访问Oracle数据, 即原生Python binding, 有些原生的Hook支持Bulk load.
          JdbcHook, 是通过jaydebeapi+Oracle JDBC访问Oracle数据         
    Tasks: task代表DAG中的一个节点, 它其实是一个BaseOperator子类.
    Task instances, 即task的运行态实例, 它包含了task的status(成功/失败/重试中/已启动)
    Job: Airflow中Job很少提及, 但在数据库中有个job表, 需要说明的是Job和task并不是一回事, Job可以简单理解为Airflow的批次, 更准确的说法是同一批被调用task或dag的统一代号. 有三类Job, 分别SchedulerJob/BackfillJob/LocalTaskJob, 对于SchedulerJob和BackfillJob, job指的是指定dag这次被调用的运行时代号, LocalTaskJob是指定task的运行时代号.

       
    ---------------
    Connections:
    ---------------  
    我们的Task需要通过Hook访问其他资源, Hook仅仅是一种访问方式, 就像是JDBC driver一样, 要连接DB, 我们还需要DB的IP/Port/User/Pwd等信息. 这些信息不太适合hard code在每个task中, 可以把它们定义成Connection, airflow将这些connection信息存放在后台的connection表中. 我们可以在WebUI的Admin->Connections管理这些连接.
     
       
    ---------------
    Variables:
    ---------------   
    Variable 没有task_id/dag_id属性, 往往用来定义一些系统级的常量或变量,  我们可以在WebUI或代码中新建/更新/删除Variable. 也可以在WebUI上维护变量.
    Variable 的另一个重要的用途是, 我们为Prod/Dev环境做不同的设置, 详见后面的开发小节.

       
    ---------------
    XComs:
    ---------------  
    XCom和Variable类似, 用于Task之间共享一些信息. XCom 包含task_id/dag_id属性, 适合于Task之间传递数据, XCom使用方法比Variables复杂些. 比如有一个dag, 两个task组成(T1->T2), 可以在T1中使用xcom_push()来推送一个kv, 在T2中使用xcom_pull()来获取这个kv.

       
    ---------------
    Trigger Rules:
    ---------------  
    可以为dag中的每个task都指定它的触发条件, 这里的触发条件有两个维度, 以T1&T2->T3 这样的dag为例:
      一个维度是: 要根据dag上次运行T3的状态确定本次T3是否被调用, 由
    DAG的default_args.depends_on_past参数控制, 为True时, 只有上次T3运行成功, 这次T3才会被触发
      另一个维度是: 要根据前置T1和T2的状态确定本次T3是否被调用, 由T3.trigger_rule参数控制, 有下面6种情形, 缺省是all_success.  
        all_success: (default) all parents have succeeded
        all_failed: all parents are in a failed or upstream_failed state
        all_done: all parents are done with their execution
        one_failed: fires as soon as at least one parent has failed, it does not wait for all parents to be done
        one_success: fires as soon as at least one parent succeeds, it does not wait for all parents to be done
        dummy: dependencies are just for show, trigger at will

       
    ---------------
    分支的支持:
    ---------------      
    airflow有两个基于PythonOperator的Operator来支持dag分支功能.
    ShortCircuitOperator, 用来实现流程的判断. Task需要基于ShortCircuitOperator, 如果本Task返回为False的话, 其下游Task将被skip; 如果为True的话, 其下游Task将会被正常执行.  尤其适合用在其下游都是单线节点的场景.  
    BranchPythonOperator, 用来实现Case分支. Task需要基于BranchPythonOperator, airflow会根据本task的返回值(返回值是某个下游task的id),来确定哪个下游Task将被执行, 其他下游Task将被skip.

       
    ---------------
    airflow系统表:
    ---------------
    connection 表:
       我们的Task往往需要通过jdbc/ftp/http/webhdfs方式访问其他资源, 一般地访问资源时候都需要一些签证, airflow允许我们将这些connection以及鉴证存放在connection表中.  可以现在WebUI的Admin->Connections管理这些连接, 在代码中使用这些连接.
    需要说明的是, connection表有2个id栏位, 一个是id, 一个是conn_id, id栏位是该表的PK, conn_id栏位是connection的名义id, 也就是说我们可以定义多个同名的conn_id, 当使用使用时airflow将会从同名的conn_id的列表中随机选一个, 有点基本的load balance的意思.
    user 表 :
       包含user的username和email信息.  我们可以在WebUI的Admin->Users管理.
    variable 表 :
       包含定义variable
    xcom 表:
       包含xcom的数据
    dag 表:
       包含dag的定义信息, dag_id是PK(字符型)
    dag_run 表:
       包含dag的运行历史记录, 该表也有两个id栏位, 一个是id, 一个是run_id, id栏位是该表的PK, run_id栏位是这次运行的一个名字(字符型), 同一个dag, 它的run_id 不能重复.
       物理PK: 即id栏位
       逻辑PK: dag_id + execution_date 组合
       execution_date 栏位, 表示触发dag的准确时间
    注意: 没有 task 表:
       airflow的task定义在python源码中, 不在DB中存放注册信息.
    task_instance 表:
       物理PK: 该表没有物理PK
       逻辑PK: dag_id + task_id + execution_date 组合.
       execution_date 栏位, 表示触发dag的准确时间,是datetime类型字段
       start_date/end_date 栏位,表示执行task的起始/终止时间, 也是datetime类型字段
    job 表:
       包含job(这里可以理解为批次)的运行状态信息

     
    ========================
    操作
    ========================
    ---------------
    安装和配置
    ---------------
    1. 操作系统:
       Airflow不能在Windows上部署, 原因是使用了 gunicorn 作为其web server(目前gunicorn还不支持Windows), 另外我也在代码中看到一些hard code了一些bash命令.
    2. shell 解释器:
       操作系统应该安装bash shell, 运行airflow服务的账号, 最好默认使用bash shell.
    3. Python版本:
       目前Airflow只是实验性地支持Python3, 推荐使用Python2.7
    4. Backend 数据库:
       SqlAlchemy 支持的数据库都可以作为Backend, 甚至Sqlite(非常适合做Demo或临时体验一下), 官方推荐采用 MySQL 或 Postgres. 我试了Oracle, 但最终还是以失败告终. MySQL 应该使用 mysqlclient 包, 我简单试了mysql-connector-python 有报错.
    5. Executor的选择:
       有三个 Executor 可供选择, 分别是: SequentialExecutor 和 LocalExecutor 和 CeleryExecutor, SequentialExecutor仅仅适合做Demo(搭配Sqlite backend), LocalExecutor 和 CeleryExecutor 都可用于生产环境, CeleryExecutor 将使用 Celery 作为Task执行的引擎, 扩展性很好, 当然配置也更复杂, 需要先setup Celery的backend(包括RabbitMQ, Redis)等. 其实真正要求扩展性的场景并不多, 所以LocalExecutor 是一个很不错的选择了.

       
    ---------------
    初始化配置:
    ---------------
    1. 配置OS环境变量 AIRFLOW_HOME, AIRFLOW_HOME缺省为 ~/airflow
    2. 运行下面命令初始化一个Sqlite backend DB, 并生成airflow.cfg文件
       your_python ${AIRFLOW_HOME}inairflow initdb
    3. 如果需要修改backend DB类型, 修改$AIRFLOW_HOME/airflow.cfg文件 sql_alchemy_conn后, 然后重新运行 airflow initdb .
       官方推荐使用MySQL/PostgreSQL做DB Server.
       MySQL 应该使用 mysqlclient 驱动, 我试验了mysql-connector-python 驱动, 结果airflow 网页端报错
       我试着用Oracle 做DB, 解决了很多问题, 但终究还是不能完全运行起来.
    4. 修改$AIRFLOW_HOME/airflow.cfg文件
       重新设置Backend/Executor, 以及webserver端口, 设置dags_folder目录和base_log_folder目录. 有下面3个参数用于控制Task的并发度,
       parallelism, 一个Executor同时运行task实例的个数
       dag_concurrency, 一个dag中某个task同时运行的实例个数
       max_active_runs_per_dag: 一个dag同时启动的实例个数
       
       

    ---------------
    了解几种作业运行模式
    ---------------
    test 作业运行模式:
     该task是在本地运行, 不会发送到远端celery worker, 也不检查依赖状态, 也不将结果记录到airflow DB中, log也仅仅会在屏幕输出, 不记录到log文件.
     使用场景: 多用于测试单个作业的code的逻辑.  可以通过test 命令进入test 模式.

    mark_success 作业运行模式:
      仅仅将作业在DB中Mark为success, 但并不真正执行作业
      使用场景: 多用于测试整个dag流程控制, 或者为某个task在DB中补一些状态. 可以在backfill命令和 run命令中启用.
     
    dry_run 作业运行模式:
      airflow不检查作业的上下游依赖, 也不会将运行结果记录不到airflow DB中. 具体作业的运行内容分情况:
      如果你的Operator没有重载 dry_run()方法的话,  运行作业也仅打印一点作业执行log
      如果重载BaseOperator的dry_run()方法的话,  运行作业即是执行你的dry_run()
      使用场景: 个人觉得 dry_run 模式意义并不大, 可以在backfill命令和 test 命令中启用   
     
     
    ---------------
    命令行工具
    ---------------
    airflow 包安装后, 会往我们your_pythonin目录复制一个名为 airflow 的文件, 可以直接运行.
     
    下面是该命令行工具支持的命令
    1. 初始化airflow meta db  
    airflow initdb [-h]

    2. 升级airflow meta db
    airflow upgradedb [-h]

    3. 开启web server
    airflow webserver  --debug=False
    开启airflow webserver, 但不进入flask的debug模式

    4. 显示task清单
    airflow list_tasks --tree=True -sd=/home/docs/airflow/dags
    以Tree形式, 显示/home/docs/airflow/dags下的task 清单

    5. 检查Task状态
    airflow task_state  -sd=/home/docs/airflow/dags dag_id task_id execution_date
    这里的 execution_date 是触发dag的准确时间, 是DB的datetime类型, 而不是Date类型

    6. 开启一个dag调度器
    airflow scheduler [-d DAG_ID] -sd=/home/docs/airflow/dags  [-n NUM_RUNS]
    启动dag调度器, 注意启动调度器, 并不意味着dag会被马上触发, dag触发需要符合它自己的schedule规则.
    参数NUM_RUNS, 如果指定的话, dag将在运行NUM_RUNS次后退出. 没有指定时, scheduler将一直运行.
    参数DAG_ID可以设定, 也可以缺省, 含义分别是:
    如果设定了DAG_ID, 则为该DAG_ID专门启动一个scheduler;
    如果缺省DAG_ID, airflow会为每个dag(subdag除外)都启动一个scheduler.


    7. 立即触发一个dag, 可以为dag指定一个run id, 即dag的运行实例id.
    airflow trigger_dag [-h] [-r RUN_ID] dag_id
    立即触发运行一个dag, 如果该dag的scheduler没有运行的话, 将在scheduler启动后立即执行dag

    8. 批量回溯触发一个dag
    airflow backfill [-s START_DATE] [-e END_DATE]  [-sd SUBDIR]  --mark_success=False --dry_run=False dag_id
    有时候我们需要**立即**批量补跑一批dag, 比如为demo准备点执行历史, 比如补跑错过的运行机会. DB中dag execute_date记录不是当下时间, 而是按照 START_DATE 和 scheduler_interval 推算出的时间.  
    如果缺省了END_DATE参数, END_DATE等同于START_DATE.

    9. 手工调用一个Task
    airflow run [-sd SUBDIR] [-s TASK_START_DATE] --mark_success=False  dag_id task_id execution_date
    该命令参数很多, 如果仅仅是测试运行, 建议使用test命令代替.

    10. 测试一个Task
    airflow test -sd=/home/docs/airflow/dags --dry_run=False dag_id task_id execution_date
    airflow test -sd=/home/docs/airflow/dags --dry_run=False dag_id task_id 2015-12-31
    以 test 或 dry_run 模式 运行作业.


    11. 清空dag下的Task运行实例
    airflow clear [-s START_DATE] [-e END_DATE]  [-sd SUBDIR]  dag_id

    12. 显示airflow的版本号
    airflow version  


    ========================
    Airflow 开发
    ========================
    ---------------
    dag脚本开发
    ---------------
    dag脚本可参考example_dags目录中的sample, 然后将脚本存放到airflow.cfg指定的dags_folder下.
    airflow 已经包含实现很多常用的 operator, 包括 BashOperator/EmailOperator/JdbcOperator/PythonOperator/ShortCircuitOperator/BranchPythonOperator/TriggerDagRunOperator等, 基本上够用了, 如果要实现自己的Operator, 继承BaseOperator, 一般只需要实现execute()方法即可. execute()约定没有返回值, 如果execute()中抛出了异常, airflow则会认为该task失败.
    pre_execute()/post_execute()用处不大, 不用特别关注, 另外post_execute()是在on_failure_callback/on_success_callback回调函数之前执行的, 所以, 也不适合回写作业状态.


    作业流程串接的几个小贴士:
      使用 DummyOperator 来汇聚分支
      使用 ShortCircuitOperator/BranchPythonOperator 做分支
      使用 SubDagOperator 嵌入一个子dag
      使用 TriggerDagRunOperator  直接trigger 另一个dag
      T_B.set_upstream(T_A), T_A->T_B, 通过task对象设置它的上游
      T_1.set_downstream(T_2), T_1->T_2 , 通过task对象设置它的下游
      airflow.utils.chain(T_1, T_2, T_3), 通过task对象设置依赖关系, 这个方法就能一次设置长的执行流程, T_1->T_2->T_3
      dag.set_dependency('T_1_id', 'T_2_id'), 通过id设置依赖关系


    ---------------  
    扩展airflow界面
    ---------------
    扩展airflow,  比如WebUI上增加一个菜单项, 可以按照plugin形式实现.
     

    ---------------
    自己的表如何关联airflow的表
    ---------------
    很多时候airflow DB的各个表不够用, 我们需要增加自己的表. 比如增加一个my_batch_instance表, 一个my_task_instance表, my_batch_instance需要关联airflow dag_run表, my_task_instance需要关联airflow task_instance表.
    my_batch_instance表中, 增加airflow_dag_id和airflow_execute_date, 来对应airflow dag_run表的逻辑PK; my_task_instance表增加airflow_dag_id和airflow_task_id和airflow_execute_date, 对应airflow task_instance表的逻辑PK.  

    接下来的问题是, 如何在task的python代码中, 获取这些逻辑PK值? 其实也很简答, 我们的task都继承于BaseOperation类, BaseOperation.execute(self, context)方法, 有一个context参数, 它包含很丰富的信息, 有:
      dag定义对象, dag.dag_id 即是 dag_id 值
      task定义对象,task.task_id 即是 task_id 值
      execution_date相关的几个属性(包括datetime类型的execution_date, 字符类型的ds, 更短字符型的ds_nodash)  
      context是一个dict,完整的内容是
        {
            'dag': task.dag,
            'ds': ds,
            'yesterday_ds': yesterday_ds,
            'tomorrow_ds': tomorrow_ds,
            'END_DATE': ds,
            'ds_nodash': ds_nodash,
            'end_date': ds,
            'dag_run': dag_run,
            'run_id': run_id,
            'execution_date': task_instance.execution_date,
            'latest_date': ds,
            'macros': macros,
            'params': params,
            'tables': tables,
            'task': task,
            'task_instance': task_instance,
            'ti': task_instance,
            'task_instance_key_str': task_instance_key_str,
            'conf': configuration,
        }
    execution_date相关的几个属性具体取值是:
    ds=execution_date.isoformat()[:10]       
    ds_nodash = ds.replace('-', '')        
    ti_key_str_fmt = "{task.dag_id}__{task.task_id}__{ds_nodash}"
    task_instance_key_str = ti_key_str_fmt.format(task,ds_nodash)
    task_instance_key_str 值可以看做是Task instance表的单一的逻辑PK, 很可惜的是Task instance没有这个字段.


    ---------------
    如何及时拿到airflow task的状态
    ---------------
    举例说明, 比如我的task是执行一个bash shell, 为了能将task的信息及时更新到自己的表中, 需要基于BashOperator的实现一个子类MyBashOperator, 在execute(context)方法中, 将running状态记录到自己的表中.

    另外, 在创建MyBashOperator的实例时候, 为on_failure_callback和on_success_callback参数设置两个回调函数, 我们在回调函数中, 将success或failed状态记录到自己的表中.
    on_failure_callback/on_success_callback回调函数签名同execute(), 都有一个context参数.
     
       
    ---------------
    为生产环境和测试环境提供不同的设置
    ---------------
    系统级的设置, 见airflow.cfg文档

    DAG级别的设置, 我们可为Prod/Dev环境准备不同的default_args,  
    default_args = {
        'owner': 'airflow',
        'depends_on_past': False,
        'start_date': datetime(2015, 6, 1),
        'email': ['airflow@airflow.com'],
        'email_on_failure': False,
        'email_on_retry': False,
        'retries': 1,
        'retry_delay': timedelta(minutes=5),
        # 'queue': 'bash_queue',
        # 'pool': 'backfill',
        # 'priority_weight': 10,
        # 'end_date': datetime(2016, 1, 1),
    }

    dag = DAG('tutorial', default_args=default_args)

    通过Variable, 加载不同环境的配置. 详细思路如下:
    比如我们有一个My_Cfg参数, 在Prod和Dev取值有可能不同.
    首先设置一个 Environment_Flag variable, 其取值是Prod或Dev.
    然后, 定义为My_Cfg参数设定两个变量, My_Cfg_For_Prod 和 My_Cfg_For_Dev, 并赋值, 分别对应Prod/Dev环境下My_Cfg的取值.  
    在代码中, 我们就可以通过Environment_Flag的取值, 就知道是该访问 My_Cfg_For_Prod 变量还是 My_Cfg_For_Dev 变量, 进而得到My_Cfg的取值.


    ---------------
    Regular的External trigger触发dag的推荐用法
    ---------------
    外部触发需要trigger_dag命令行, 命令行最好要加上run_id参数;
    同时DAG的schedule_interval参数最好设置成None, 表明这个DAG始终是由外部触发
     
     
    ---------------
    测试运行步骤:
    ---------------
    1. 先测试pytnon代码正确性
       python ~/airflow/dags/tutorial.py
    2. 通过命令行验证DAG/task设置
       # print the list of active DAGs
       airflow list_dags

       # prints the list of tasks the "tutorial" dag_id
       airflow list_tasks tutorial

       # prints the hierarchy of tasks in the tutorial DAG
       airflow list_tasks tutorial --tree
    3. 通过test命令行试跑一下, 测试一下code逻辑
       airflow test tutorial my_task_id 2015-06-01   
    4. 通过 backfill --mark_success=True   
       airflow backfill tutorial -s 2015-06-01 -e 2015-06-07
            
              
    ---------------
    我的开发
    ---------------   
    1. 贡献一个ssh hook.
    2. 实现几个命令行, airflow_smic 命令行, 实现 terminate, pause, continue, failover 操作.  
       failover 操作, 其实就是skip已经完成的作业, 重新跑running的作业.
    3. 提供admin界面, 管理依赖关系, 并提供可视化预览
       通过 airflow list_tasks --tree=True, 实现可视化     

  • 相关阅读:
    css表格单元格间距设置
    JavaScript(js)设置输入焦点(focus)
    让div居中的方法
    Window.open()的使用
    getElementsByTagName的用法
    offsetTop获取top值
    js中indexof的使用
    jquery解析json数据
    iframe的使用
    WCF学习笔记Ⅲ
  • 原文地址:https://www.cnblogs.com/harrychinese/p/airflow.html
Copyright © 2020-2023  润新知