Apache Airflow: преимущества и недостатки

Оркестратор задач — инструмент или система, предназначенные для управления и контроля выполнением задач в сложных вычислительных и информационных средах. Он облегчают процесс развертывания, автоматизации и управления выполнением задач, что позволяет повысить эффективность работы и оптимизировать ресурсы.

Одним из популярных оркестратором задач является Apache Airflow. Он, как и все инструменты, имеет свои преимущества и недостатки, о которых пойдет речь в данной статье

Apache Airflow

Apache Airflow — это платформа для программирования, планирования и мониторинга рабочих процессов (Workflows). Он позволяет создавать и управлять сложными зависимостями между задачами, автоматизировать их выполнение и мониторить процесс обработки данных.

Airflow предоставляет графический интерфейс для создания, выполнения и мониторинга рабочих процессов, также он позволяет создавать рабочие процессы в форме направленных ацикличных графов (DAG), где каждый узел представляет собой отдельную задачу. Это обеспечивает удобство и гибкость в управлении задачами и зависимостями между ними.

Airflow широко используется для автоматизации распределенных вычислений, обработки данных, управления ETL (Extract, Transform, Load) процессами, планирования задач и других сценариев работы с данными.

По умолчанию Apache Airflow использует SQLite, как базу данных.

Один из способов использования Apache Airflow — docker файл.

docker-compose

version: '3.8'
x-airflow-common:
  &airflow-common
  image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:2.7.3}
  environment:
    &airflow-common-env
    AIRFLOW__CORE__EXECUTOR: LocalExecutor
    AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
    AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
    AIRFLOW__CORE__FERNET_KEY: ''
    AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true'
    AIRFLOW__CORE__LOAD_EXAMPLES: 'false'
    AIRFLOW__API__AUTH_BACKENDS: 'airflow.api.auth.backend.basic_auth,airflow.api.auth.backend.session'
    AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: 'true'
    _PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:-}
  volumes:
    - ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags
    - ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs
    - ${AIRFLOW_PROJ_DIR:-.}/config:/opt/airflow/config
    - ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins
  user: "${AIRFLOW_UID:-50000}:0"
  depends_on:
    &airflow-common-depends-on
    postgres:
      condition: service_healthy

services:
  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: airflow
      POSTGRES_PASSWORD: airflow
      POSTGRES_DB: airflow
    volumes:
      - postgres-db-volume:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "airflow"]
      interval: 10s
      retries: 5
      start_period: 5s
    restart: always

  airflow-webserver:
    <<: *airflow-common
    command: webserver
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always
    depends_on:
      <<: *airflow-common-depends-on
      airflow-init:
        condition: service_completed_successfully

  airflow-scheduler:
    <<: *airflow-common
    command: scheduler
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8974/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always
    depends_on:
      <<: *airflow-common-depends-on
      airflow-init:
        condition: service_completed_successfully

  airflow-init:
    <<: *airflow-common
    entrypoint: /bin/bash
    # yamllint disable rule:line-length
    command:
      - -c
      - |
        function ver() {
          printf "%04d%04d%04d%04d" $${1//./ }
        }
        airflow_version=$$(AIRFLOW__LOGGING__LOGGING_LEVEL=INFO && gosu airflow airflow version)
        airflow_version_comparable=$$(ver $${airflow_version})
        min_airflow_version=2.2.0
        min_airflow_version_comparable=$$(ver $${min_airflow_version})
        if (( airflow_version_comparable < min_airflow_version_comparable )); then
          echo
          echo -e "\033[1;31mERROR!!!: Too old Airflow version $${airflow_version}!\e[0m"
          echo "The minimum Airflow version supported: $${min_airflow_version}. Only use this or higher!"
          echo
          exit 1
        fi
        if [[ -z "${AIRFLOW_UID}" ]]; then
          echo
          echo -e "\033[1;33mWARNING!!!: AIRFLOW_UID not set!\e[0m"
          echo "If you are on Linux, you SHOULD follow the instructions below to set "
          echo "AIRFLOW_UID environment variable, otherwise files will be owned by root."
          echo "For other operating systems you can get rid of the warning with manually created .env file:"
          echo "    See: https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#setting-the-right-airflow-user"
          echo
        fi
        one_meg=1048576
        mem_available=$$(($$(getconf _PHYS_PAGES) * $$(getconf PAGE_SIZE) / one_meg))
        cpus_available=$$(grep -cE 'cpu[0-9]+' /proc/stat)
        disk_available=$$(df / | tail -1 | awk '{print $$4}')
        warning_resources="false"
        if (( mem_available < 4000 )) ; then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough memory available for Docker.\e[0m"
          echo "At least 4GB of memory required. You have $$(numfmt --to iec $$((mem_available * one_meg)))"
          echo
          warning_resources="true"
        fi
        if (( cpus_available < 2 )); then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough CPUS available for Docker.\e[0m"
          echo "At least 2 CPUs recommended. You have $${cpus_available}"
          echo
          warning_resources="true"
        fi
        if (( disk_available < one_meg * 10 )); then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough Disk space available for Docker.\e[0m"
          echo "At least 10 GBs recommended. You have $$(numfmt --to iec $$((disk_available * 1024 )))"
          echo
          warning_resources="true"
        fi
        if [[ $${warning_resources} == "true" ]]; then
          echo
          echo -e "\033[1;33mWARNING!!!: You have not enough resources to run Airflow (see above)!\e[0m"
          echo "Please follow the instructions to increase amount of resources available:"
          echo "   https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#before-you-begin"
          echo
        fi
        mkdir -p /sources/logs /sources/dags /sources/plugins
        chown -R "${AIRFLOW_UID}:0" /sources/{logs,dags,plugins}
        exec /entrypoint airflow version
    environment:
      <<: *airflow-common-env
      _AIRFLOW_DB_MIGRATE: 'true'
      _AIRFLOW_WWW_USER_CREATE: 'true'
      _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME:-airflow}
      _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD:-qwerty12345!}
      _PIP_ADDITIONAL_REQUIREMENTS: ''
    user: "0:0"
    volumes:
      - ${AIRFLOW_PROJ_DIR:-.}:/sources

  airflow-cli:
    <<: *airflow-common
    profiles:
      - debug
    environment:
      <<: *airflow-common-env
      CONNECTION_CHECK_MAX_COUNT: "0"
    command:
      - bash
      - -c
      - airflow

volumes:
  postgres-db-volume:

Несколько популярных операторов в Apache Airflow:

EmptyOperator

Этот оператор представляет собой очень простой оператор в Apache Airflow, который не выполняет никаких реальных действий.

  • Ожидание или задержка: можно использовать EmptyOperator для создания задержки между выполнением задач в вашем DAG.

  • Создание структуры DAG: может быть использован для создания структуры вашего DAG, когда у вас еще нет конкретных операций для некоторых частей рабочего процесса.

  • Использование в качестве заполнителя: может понадобиться создать «место действия» для определенных зависимостей

  • Тестирование: при разработке и тестировании DAG, вы можете использовать EmptyOperator, чтобы вставить временную заглушку вместо реальных операций.

Пример кода с EmptyOperator:

from airflow import DAG
from airflow.utils.dates import days_ago
from airflow.operators.empty import EmptyOperator

default_my_args = {
    'owner': 'PB_Academy',
    'retries': 5
}

with DAG (
    dag_id = 'dag_empty',
    description='ДАГ с использованием bash оператора',
    schedule_interval='@once',
    default_args= default_my_args, 
    start_date=days_ago(1)
) as dag:
    start_task = EmptyOperator(
        task_id = 'start_task'
    )
    task_1_1 = EmptyOperator(
        task_id = 'task_1_1'
    )
    task_1_2 = EmptyOperator(
        task_id = 'task_1_2'
    )
    union_task = EmptyOperator(
        task_id = 'union'
    )
    task_2_1 = EmptyOperator(
        task_id = 'task_2_1'
    )
    task_2_2 = EmptyOperator(
        task_id = 'task_2_2'
    )
    finish_task = EmptyOperator(
        task_id = 'finish_task'
    )
    start_task >> [task_1_1, task_1_2] >> union_task >>[task_2_1, task_2_2] >> finish_task

Отображение dag с помощью графа

Отображение dag с помощью графа

Начиная с версии 2.3.0 Apache Airflow появился данный оператор. До этого существовал DummyOperator. В версии 2.3.4 разработчики призывают переходить на EmptyOperator.

BashOperator

Этот оператор используется для выполнения произвольных bash-команд или скриптов. Он является универсальным инструментом для выполнения различных командных операций в рамках ваших рабочих процессов.

Пример кода с BashOperator:

from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime, timedelta

default_my_args = {
    'owner': 'PB_Academy',
    'retries': 5,
    'retry_delay': timedelta(minutes=1)
}
with DAG(
   dag_id='dag_bush',
   default_args= default_my_args, 
   description='ДАГ с использованием bash оператора',
   start_date= datetime(2024, 5, 7, 16, 50, 0),
   schedule_interval='@daily'
) as dag: 
    task1 = BashOperator(
        task_id = 'first_task',
        bash_command= 'mkdir PB_Academy'
    )
    task2 = BashOperator(
        task_id = 'second_task',
        bash_command= 'touch tmp.txt'
    )
    task3 = BashOperator(
        task_id = 'thrird_task',
        bash_command= 'echo Hello World'
    )
  task1 >> task2 >> task3

PostgresOperator

Данный оператор позволяет выполнять SQL-запросы в базе данных PostgreSQL. Он предоставляет удобный способ выполнения различных операций с базой данных PostgreSQL в рамках ваших DAG.

Для того, чтобы установить PostgresOperator рекомендуем выполнить следующую команду:

pip install apache-airflow-providers-postgres

Пример кода с PostgresOperator:

from airflow import DAG
from datetime import datetime, timedelta
from airflow.providers.postgres.operators.postgres import PostgresOperator

default_my_args = {
    'owner': 'PB_Academy',
    'retries': 5
}

with DAG(
    dag_id = 'dag_postgres',
    schedule_interval='@once',
    default_args= default_my_args, 
    start_date= datetime(2023, 12, 8)
) as dag:
    task_pg_create = PostgresOperator(
        task_id='task_pg_create',
        postgres_conn_id='postgres_default',
        sql='CREATE TABLE test_1(id SERIAL, date TIMESTAMP);'
    ) 
    task_pg_insert = PostgresOperator(
        task_id='task_pg_insert',
        postgres_conn_id='postgres_default',
        sql='INSERT INTO test_1 (date) VALUES (localtimestamp);'
    )
    task_pg_create >> task_pg_insert

PythonOperator

Этот оператор позволяет запускать произвольные функции Python в вашем DAG. Это может быть полезно, если вам нужно выполнить какой-то кастомный код или манипуляции с данными в Python.

Для общения между задачами в рамках одного dag существует механизм — XCom. Это словарь, из которого можно брать данные. Как конфиги, так и результаты других задач. Данные в XCom могут быть как public, так и private. Push — отправить данные в словарь, pull — получить. Стоит добавить, что XCom не подчищает за собой данные, так что нужно побеспокоиться, чтобы после выполнения сессии dag удалял историю.

from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta

default_my_args = {
    'owner': 'PB_Academy',
    'retries': 5,
    'retry_delay': timedelta(minutes=5)
}

def print_param(ti):
    sql = ti.xcom_pull(task_ids = 'get_param', key = 'SQL')
    nosql = ti.xcom_pull(task_ids = 'get_param', key = 'NoSQL')
    print(f"SQL database: {sql} \n NoSQL database: {nosql}")

def get_param(ti):
    ti.xcom_push(key = 'SQL', value = ['MS SQL','PostgreSQL','Oracle'])
    ti.xcom_push(key = 'NoSQL', value = ['MongoDB','Redis','Elasticsearch'])

with DAG(
   dag_id='dag_python',
   default_args= default_my_args, 
   description='-',
   start_date= datetime(2023, 12, 8, 8,47),
   schedule_interval='@once'
) as dag:
    task1 = PythonOperator(
        task_id='task_print_param',
        python_callable = print_param,
    )
    task2 = PythonOperator(
        task_id='task_get_param',
        python_callable = get_param
    )
    task2 >> task1

Task Flow API

Task Flow API — это часть Apache Airflow, которая предоставляет возможность программно создавать и управлять задачами и их зависимостями в DAG. Он позволяет создавать и изменять DAG и его структуру изнутри самого Python-кода, а не только из конфигурационных файлов.

Пример кода с иcпользованием Task Flow API:

from datetime import datetime
from airflow.decorators import dag, task

default_my_args = {
    'owner': 'PB_Academy',
    'retries': 5
}

@dag(
    dag_id = 'taskflow_api_dag',
    default_args = default_my_args,
    start_date = datetime(2023,12,10),
    schedule_interval='@daily'
)
def calculate_etl():

    @task(multiple_outputs = True)
    def get_numbers():
        return {
            'number_1' : 21,
            'number_2' : 99
        }

    @task
    def calculate(number_1, number_2):
        print(number_1 + number_2)
    
    dicts = get_numbers()
    calculate(dicts['number_1'], dicts['number_2'])
    
my_dag = calculate_etl()

Дополнительно

catchup — параметр в Apache Airflow, который определяет, должны ли старые даты выполнения задач быть выполнены при первом запуске DAG. Когда catchup установлен в True, Apache Airflow автоматически запускает пропущенные даты выполнения задач после того, как DAG был включен в работу.

Пример кода с catch up:

from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime

default_my_args = {
    'owner': 'PB_Academy',
    'retries': 5
}

with DAG(
   dag_id='dag_catchup',
   default_args= default_my_args, 
   start_date= datetime(2023, 12, 7, 16, 50, 0),
   schedule_interval='@daily',
   catchup=True
) as dag: 
    task1 = BashOperator(
        task_id = 'first_task',
        bash_command= 'echo Hello World'
    )

 В Apache Airflow можно написать задачу, которая получает статус других задач. Данная задача будет полезна для:

  1. Отчетность и мониторинг: Задача получения статуса задач может использоваться для создания мониторинговых или отчетных процедур, которые сообщают о статусе выполнения других задач. Например, вы можете хотеть получить уведомление или отправить отчет, когда определенная задача завершается успешно или неудачно.

  2. Автоматизация бизнес-процессов: Получение статуса задач также может быть использовано для автоматизации бизнес-процессов. Например, если определенная задача завершилась успешно, вы можете автоматически запускать следующие действия или процессы.

  3. Управление зависимостями: Получение статуса задач может помочь в управлении зависимостями и выполнением последующих действий в зависимости от статуса предыдущих задач. Например, если одна задача завершилась неудачно, вы можете хотеть принять решение о перезапуске другой задачи или выполнении альтернативных действий.

Пример кода с получением статуса задачи:

from airflow.models.dagrun import DagRun
from airflow.decorators import dag, task
from datetime import datetime

default_my_args = {
    'owner': 'PB_Academy',
    'retries': 5
}

@dag(
    dag_id = 'dag_with_status',
    default_args = default_my_args,
    start_date = datetime(2023,12,10),
    schedule_interval='@daily'
)
def calculate_etl():

    @task()
    def get_status_dag():
        dag_id = 'dag_with_status'
        dag_runs = DagRun.find(dag_id=dag_id)
        print(f'Length: {len(dag_runs)}')
        for dag_run in dag_runs:
            if dag_run.external_trigger == False :
                print(f'Dag Name {dag_run}\n Dag Status {dag_run.state}')
    
    @task()
    def success_task():
        4 / 2
    
    @task()
    def fail_task():
        4 / 0
        
    success_task()
    fail_task()
    get_status_dag()

my_dag = calculate_etl()

Параметр schedule_interval используется для определения расписания выполнения задач в рамках DAG. Этот параметр указывает, с какой периодичностью DAG и его задачи должны выполняться.schedule_interval может быть определен различными способами, включая крон-выражения, объекты datetime.timedelta или ключевые слова, такие как @daily, @hourly и т. д.

Вывод

Таким образом, Apache Airflow часто выбирают для сложных рабочих процессов с большим количеством задач. Подводя итог, рассмотрим основные плюсы и минусы данной системы:

Apache Airflow

Плюсы

Минусы

Гибкость

Airflow написан на Python, это делает его гибким и легко интегрируемым с другими инструментами.

Сложность настройки

Наличие неявных зависимостей может усложнить начальную настройку и конфигурацию.

Мощный пользовательский интерфейс

Графический веб-интерфейс позволяет легко создавать и мониторить рабочие процессы.

Требования к ресурсам

Для управления большими рабочими процессами требуются значительные вычислительные ресурсы.

Расширяемость

Поддерживает множество плагинов и интеграций с различными сервисами и базами данных.

Масштабируемость

Модульная архитектура и очереди сообщений позволяют обрабатывать большое количество задач.

Для изучения данной темы рекомендуем прочитать книгу Баса Харенслака и Джулиана де Руйтера »Apache Airflow и конвейеры обработки данных»

© Habrahabr.ru