airflow

1,039 阅读5分钟

airflow 常用概念

operator

operator 是 airflow 定义任务的一个模板,常用的 operator 如

  • BashOperator - 执行 bash 命令
  • PythonOperator - 调用 python 函数
  • EmailOperator - 发送邮件

airflow 也提供了很多第三方的 operator,比如 GCP 上大多的云服务都有其对应的 operator,这在分产品展示表调度程序中是很有用的,这个程序中用到了执行 bq job、创建 dataproc 集群,提交 pyspark 任务等操作,使用对应的 BigQueryInsertJobOperator

DataprocCreateClusterOperator DataprocSubmitJobOperator 可以替换原先通过调用对应服务 python 客户端的方式

通过使用BigQueryInsertJobOperator 也优化了该调度程序执行的方式,由原先每个任务调用不同python 函数,再去提交bq job 的方式升级为直接读取 sql 文件(参数通过 jinja 模板渲染)提交 bq job,这样的方式很好的提升了程序的可扩展性和和维护性

sensor

sensor 是一种特殊的 operator,它会等待指定的事件发生然后执行某些操作

GCP 有对应的监测 bq 表、storage 文件的 sensor

variabiles

variabile 用于设置一些常用的变量,可以通过在 airflow ui 上 Admin → Variables 中设置,并在程序中使用使用以下代码获取

from airflow.models import Variable

app_id = Variable.get("fs_app_id")

Task Instances

task instances 中存放了任务执行的详细信息,在 airflow ui 中通过 browse → task instances查看

可以看到任务执行时传入的参数、实际执行的命令、日志、XComs 等信息

task instances 中可以通过条件筛选出某些任务,如果 dag 执行中有些任务失败,修复后需要重新运行,可以筛选出 state 为 failed 以及 up_stream_failed 的 task,clear 掉状态即可重新运行

XComs

airflow 中用于在不同任务之间传递数据的方式

Task Instances 类提供了 xcom_push 和 xcom_pull 方法用于显式地存入和获取 xcoms 中的值,默认情况下函数的返回值会被设置为 xcoms 中的 return_value

动态 DAG

有些 dag 的执行的任务相同,但是部分参数不同,可以创建动态的 DAG,实现方法如下

from airflow.operators.bash_operator import BashOperator
from datetime import datetime
from airflow import DAG
from airflow.operators.python_operator import PythonOperator
 
 
def create_dag(dag_id,
               schedule,
               dag_number,
               default_args):
    def hello_world_py(*args):
        print('Hello World')
        print('This is DAG: {}'.format(str(dag_number)))
 
    dag = DAG(dag_id,
              schedule_interval=schedule,
              default_args=default_args)
 
    with dag:
        t1 = PythonOperator(
            task_id='hello_world',
            python_callable=hello_world_py,
            dag_number=dag_number)
        t2 = BashOperator(
            task_id='current_date',
            bash_command='date'
        )
        t1 >> t2
 
    return dag
 
 
def get_api_data():
    data = ['test01', 'test02', 'test03']
    return data
 
 
def create_dags(data=None):
    for n in range(len(data)):
        dag_id = 'dynamic_day_{}'.format(data[n])
        default_args = {'owner': 'airflow',
                        'start_date': datetime(2019, 6, 1)}
        schedule = None
        dag_number = n
        globals()[dag_id] = create_dag(dag_id, schedule, dag_number, default_args)
 
 
data = get_api_data()
create_dags(data)

Airflow 调优

问题

最近 airflow 在执行时总会报一些奇怪的错误,类似

image.png

并且在 airflow 网页中看不到对应任务的日志

尝试

刚开始觉得可能是这个 DAG 的任务量过多,导致大多任务排队太久失败,于是增加了工作器的并发任务量celery.worker_concurrency 和单 DAG 的并发任务上限core.dag_concurrency ,修改后不但没有改善,报错的任务还变多了

解决方案

后经查阅 composer 文档后发现可能是由于工作器 pod 逐出,导致分配到该工作器的任务无法执行,可以在 composer 的监控页面中观察 worker pod evictions 图表的情况

image.png

如果有 pod 被逐出的情况,则该 pod 上运行的所有任务实例都将中断,之后被 Airflow 标记为失败。(这里有个问题是,线上环境中此图表并没有检测到有 pod 被逐出,不过在 logging 中可以看到有 celery 节点重启、报错的日志,因此怀疑是工作器 pod 出了问题)

工作器 pod 逐出的大多数问题都是因为工作器存在内存不足的情况,文档提供了以下解决方案

  1. 增加工作器可用内存
  2. 减少工作器并发请求数

第二点就是我之前改动造成更多任务失败的原因,由于并发任务量增加,会导致单个工作器同时处理更多的任务,可能会导致内存不足。因此在更改工作器的并发任务数量时,还需要增加工作器数量的上限,例如,如果将工作器并发请求数从 12 减少到 6,则可能需要将工作器数量上限加倍。

最后通过以上两个方案以及增加工作器数量解决了这个问题

3.28-4.2

airflow 飞书通知插件

airflow 自带的报错通知为邮件形式,通过设置 dagdefault_args 中 email 参数,并配置以下参数至 airflow.cfg 中来实现

image.png 考虑到邮件的及时性不如飞书消息,因此开发了个 airflow 的飞书通知插件

airflow 的可扩展性很强,有很多第三方提供的插件,诸如 gcp 的各种服务、钉钉通知等,不过飞书还没有官方的 airflow 插件,这里的实现参考了钉钉通知插件的实现

airflow 的插件基本上分为三个目录

  • operator

    存放定义 operator 的代码,继承 BaseOperator,增加自定义参数,execute 方法调用 hook 类中的 send 方法

    from airflow.models import BaseOperator
    
    from airflow_feishu.hooks.feishu import FeishuHook
    
    class FeiShuOperator(BaseOperator):
        def __init__(
                self,
                *,
                feishu_conn_id: str = 'feishu',
                message_type: str = 'text',
                message: str = '',
                secret: bool = False,
                **kwargs,
        ) -> None:
            super().__init__(**kwargs)
            self.feishu_conn_id = feishu_conn_id
            self.message_type = message_type
            self.message = message
    
        def execute(self, context) -> None:
            self.log.info('Sending feishu message.')
            hook = FeishuHook(
                self.feishu_conn_id, self.message_type, self.message, self.secret
            )
            hook.send()
    
  • hooks

    存放具体插件功能如何实现的代码

    飞书通知是通过发送 http 请求实现的,airflow 中这类的功能可以通过继承 HttpHook 的类实现

    ***_conn_id(这里是 feishu_conn_id)用于获取 airflow connections 中设置的连接信息

image.png

这里将 host设置为飞书发送消息 api 的 base url airflow connections 可以用于存放一些常用到的连接信息,如数据库连接、email 配置信息等,airflow 会识别密码等敏感信息,显示为***

```
def send(self) -> None:
        support_type = ["text", "post"]
        if self.message_type not in support_type:
            raise ValueError(f"FeishuWebhookHook only support {support_type} so far, but receive {self.message_type}")

        data = self._build_message()
        tenant_access_token = self._get_tenant_access_token()
        headers = {"Content-Type": "application/json; charset=utf-8", "Authorization": f"Bearer {tenant_access_token}"}
        self.log.info("Sending Feishu type %s message %s", self.message_type, data)
        resp = self.run(endpoint="", data=data, headers=headers, params={"receive_id_type": "chat_id"})
        pprint(resp.json())
        code = resp.json().get("code")
        if code is None or int(code) != 0:
            raise AirflowException("Send Feishu message failed")
        self.log.info("Success Send Feishu message")
```

`_build_message` 将消息文本构建为飞书 api 指定的格式

`_get_tenant_access_token` 获取请求 token

`run` 会调用父类 HttpHook 中的 run 函数,根据`feishu_conn_id`获取到 connection 中对应的 host 赋值给 endpoint,并发送 http 请求(调用了 requests 库)
  • example

    存放 operator 调用示例代码

集成至 dag 中

在 dag 定义时指定on_failure_callback、on_success_callback 为 FeishuOperator

def failure_callback(context):
    """
    The function that will be executed on failure.

    :param context: The context of the executed task.
    """
    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 FeiShuOperator(
        feishu_conn_id="feishu",
        task_id="feishu_fail_callback",
        message_type="text",
        message=message,
    ).execute(context)

将任务上下文中的报错信息等指定为发送的 message