<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Apache Airflow 3 - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<atom:link href="https://datatalks.ru/tag/apache-airflow-3/feed/" rel="self" type="application/rss+xml" />
	<link>https://datatalks.ru/tag/apache-airflow-3/</link>
	<description>RoadMap для инженера данных. Дорожная карта по инструментам Data Engineer</description>
	<lastBuildDate>Thu, 12 Feb 2026 20:32:20 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://datatalks.ru/wp-content/uploads/2024/12/cropped-logo_datatalks-32x32.png</url>
	<title>Apache Airflow 3 - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<link>https://datatalks.ru/tag/apache-airflow-3/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Best Practices &#8212; Airflow 3 Документация</title>
		<link>https://datatalks.ru/best-practices-airflow-3-documentation/</link>
					<comments>https://datatalks.ru/best-practices-airflow-3-documentation/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sat, 17 Jan 2026 15:48:03 +0000</pubDate>
				<category><![CDATA[Apache Airflow Best Practices]]></category>
		<category><![CDATA[Apache Airflow]]></category>
		<category><![CDATA[Apache Airflow 3]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2751</guid>

					<description><![CDATA[<p>Перевод документации Apache Airflow 3 &#8212; Best Practices Лучшие практики по работе с Apache Airflow 3 Создание нового Dag — это процесс из трёх шагов: написание Python-кода для создания объекта Dag, проверка того, что код соответствует вашим ожиданиям, настройка зависимостей окружения для запуска вашего Dag В этом руководстве представлены лучшие практики для этих трёх шагов. [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/best-practices-airflow-3-documentation/">Best Practices &#8212; Airflow 3 Документация</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Перевод <a href="https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html" target="_blank" rel="noopener">документации Apache Airflow 3 &#8212; Best Practices</a></p>
<h1>Лучшие практики по работе с Apache Airflow 3</h1>
<p>Создание нового <strong>Dag</strong> — это процесс из трёх шагов:</p>
<ul>
<li>написание Python-кода для создания объекта Dag,</li>
<li>проверка того, что код соответствует вашим ожиданиям,</li>
<li>настройка зависимостей окружения для запуска вашего Dag</li>
</ul>
<p>В этом руководстве представлены лучшие практики для этих трёх шагов.</p>
<h2>Написание Dag</h2>
<p>Создание нового Dag в Airflow довольно простое. Однако существует множество вещей, о которых необходимо позаботиться, чтобы запуск <strong>Dag</strong> или его сбой не приводили к неожиданным результатам.</p>
<h3><strong>Создание пользовательского Operator/Hook</strong></h3>
<p>Пожалуйста, следуйте нашему <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/custom-operator.html#custom-operator" target="_blank" rel="noopener">руководству по пользовательским (custom) <strong>Operator</strong>’ам</a>.</p>
<h3><strong>Создание задачи</strong></h3>
<p>Вы должны рассматривать задачи в <strong>Airflow</strong> как эквивалент транзакций в базе данных. Это означает, что ваши задачи никогда не должны производить неполные результаты. Например, нельзя оставлять неполные данные в <strong>HDFS</strong> или <strong>S3</strong> по завершении задачи.</p>
<p><strong>Airflow</strong> может повторно запускать задачу в случае её сбоя. Следовательно, задачи должны выдавать одинаковый результат при каждом повторном запуске. Некоторые способы избежать получения различного результата:</p>
<ul>
<li>Не используйте <code>INSERT</code> при повторном запуске задачи — оператор <code>INSERT</code> может привести к появлению дублирующихся строк в базе данных. Замените его на <code>UPSERT</code>.</li>
<li>Читайте и записывайте данные в конкретный партицию. Никогда не читайте самые последние доступные данные в задаче. Кто-то может обновить входные данные между повторными запусками, что приведёт к разным результатам. Лучший подход — читать входные данные из конкретного партициона. В качестве партициона можно использовать <code>data_interval_start</code>. Этот же метод партиционирования следует применять и при записи данных в S3/HDFS.</li>
<li>Функция Python datetime <code>now()</code> возвращает текущий объект <code>datetime</code>. Эту функцию никогда не следует использовать внутри задачи, особенно для выполнения критических вычислений, так как это приводит к разным результатам при каждом запуске. Допустимо использовать её, например, для генерации временного лога.</li>
</ul>
<p><strong>Совет</strong></p>
<p>Следует определять повторяющиеся параметры, такие как <code>connection_id</code> или пути <strong>S3</strong>, в <code>default_args</code>, а не объявлять их для каждой задачи. <code>default_args</code> помогают избежать ошибок, таких как опечатки. Кроме того, большинство типов соединений имеют уникальные имена параметров в задачах, поэтому вы можете объявить соединение только один раз в <code>default_args</code> (например, <code>gcp_conn_id</code>), и оно будет автоматически использоваться всеми операторами, которые работают с данным типом соединения.</p>
<h3><strong>Удаление задачи</strong></h3>
<p>Будьте осторожны при удалении задачи из Dag. После удаления вы не сможете увидеть эту задачу в <strong>Graph View</strong>, <strong>Grid View</strong> и других представлениях, что усложнит проверку логов данной задачи через <strong>Webserver</strong>. Если такое поведение нежелательно, пожалуйста, создайте новый <strong>Dag</strong>.</p>
<h3><strong>Коммуникация</strong></h3>
<p><strong>Airflow</strong> выполняет задачи Dag на разных серверах в случае использования <strong><a href="https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/stable/kubernetes_executor.html" target="_blank" rel="noopener">Kubernetes Executor</a></strong> или <strong><a href="https://airflow.apache.org/docs/apache-airflow-providers-celery/stable/celery_executor.html" target="_blank" rel="noopener">Celery Executor</a></strong>. Поэтому не следует хранить какие-либо файлы или конфигурации в локальной файловой системе, так как следующая задача с большой вероятностью будет выполняться на другом сервере без доступа к ним — например, задача, которая загружает файл с данными, который затем обрабатывается следующей задачей. В случае использования <code>Local Executor</code> хранение файлов на диске также может усложнить повторные запуски, например если вашей задаче требуется конфигурационный файл, который удаляется другой задачей в Dag.</p>
<p>По возможности используйте <code>XCom</code> для передачи небольших сообщений между задачами, а для передачи больших объёмов данных используйте удалённое хранилище, такое как <strong>S3</strong> или <strong>HDFS</strong>. Например, если у вас есть задача, которая сохраняет обработанные данные в <strong>S3</strong>, эта задача может положить путь к выходным данным в <strong>S3</strong> в <strong>XCom</strong>, а <strong>downstream</strong>-задачи смогут получить этот путь из <strong>XCom</strong> и использовать его для чтения данных.</p>
<p>Задачи также не должны хранить внутри себя какие-либо параметры аутентификации, такие как пароли или токены. По возможности используйте <a href="https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/connections.html" target="_blank" rel="noopener"><strong>Connections</strong></a> для безопасного хранения данных в <strong>backend</strong>’е <strong>Airflow</strong> и получайте их с помощью уникального <strong>connection id</strong>.</p>
<h3><strong>Код верхнего уровня Python</strong></h3>
<p>Следует избегать написания кода верхнего уровня, который не требуется для создания <strong>Operator</strong>’ов и построения связей <strong>Dag</strong> между ними. Это связано с архитектурным решением планировщика <strong>Airflow</strong> и влиянием скорости парсинга кода верхнего уровня на производительность и масштабируемость Airflow.</p>
<p><strong>Планировщик Airflow</strong> выполняет код вне методов execute операторов с минимальным интервалом <a href="https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#config-dag-processor-min-file-process-interval" target="_blank" rel="noopener"><code>min_file_process_interval</code></a> секунд. Это делается для того, чтобы обеспечить динамическое планирование Dag’ов — когда расписание и зависимости могут со временем изменяться и влиять на следующий запуск Dag. <strong>Планировщик Airflow</strong> постоянно старается убедиться, что то, что описано в Dag’ах, корректно отражено в запланированных задачах.</p>
<p>В частности, не следует выполнять доступ к базам данных, тяжёлые вычисления и сетевые операции.</p>
<p>Одним из важных факторов, влияющих на время загрузки Dag, который часто упускают из виду Python-разработчики, является то, что импорты на верхнем уровне могут занимать неожиданно много времени и создавать значительные накладные расходы. Этого легко избежать, переместив такие импорты в локальные импорты внутри <strong>Python-callable</strong>, например.</p>
<p>Рассмотрим два примера ниже. В первом примере <strong>Dag</strong> будет парситься на дополнительные 1000 секунд дольше, чем функционально эквивалентный <strong>Dag</strong> во втором примере, где <code>expensive_api_call</code> выполняется в контексте своей задачи.</p>
<p>Неизбежание кода верхнего уровня Dag:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pendulum

from airflow.sdk import DAG
from airflow.sdk import task


def expensive_api_call():
    print("Hello from Airflow!")
    sleep(1000)


my_expensive_response = expensive_api_call()

with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:

    @task()
    def print_expensive_api_call():
        print(my_expensive_response)</pre><p>Избегание кода верхнего уровня <strong>Dag</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pendulum

from airflow.sdk import DAG
from airflow.sdk import task


def expensive_api_call():
    sleep(1000)
    return "Hello from Airflow!"


with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:

    @task()
    def print_expensive_api_call():
        my_expensive_response = expensive_api_call()
        print(my_expensive_response)</pre><p>В первом примере <code>expensive_api_call</code> выполняется каждый раз при парсинге файла <strong>Dag</strong>, что приводит к неоптимальной производительности при обработке <strong>Dag</strong>-файла. Во втором примере <code>expensive_api_call</code> вызывается только во время выполнения задачи и, таким образом, Dag может быть распарсен без потери производительности. Чтобы проверить это самостоятельно, реализуйте первый Dag и посмотрите, как строка <strong>«Hello from Airflow!»</strong> выводится в логах планировщика.</p>
<p>Обратите внимание, что операторы <code>import</code> также считаются кодом верхнего уровня. Поэтому, если у вас есть <code>import</code>, который выполняется долго, или импортируемый модуль сам выполняет код на верхнем уровне, это также может негативно сказаться на производительности планировщика. Следующий пример показывает, как работать с дорогостоящими импортами.</p><pre class="urvanov-syntax-highlighter-plain-tag"># It's ok to import modules that are not expensive to load at top-level of a Dag file
import random
import pendulum

# Expensive imports should be avoided as top level imports, because Dag files are parsed frequently, resulting in top-level code being executed.
#
# import pandas
# import torch
# import tensorflow
#

...


@task()
def do_stuff_with_pandas_and_torch():
    import pandas
    import torch

    # do some operations using pandas and torch


@task()
def do_stuff_with_tensorflow():
    import tensorflow

    # do some operations using tensorflow</pre><p></p>
<h3><strong>Как проверить, является ли мой код «кодом верхнего уровня»</strong></h3>
<p>Чтобы понять, является ли ваш код <strong>«кодом верхнего уровня»</strong> или нет, необходимо разбираться во многих тонкостях того, как работает <strong>парсинг</strong> Python. В общем случае, когда <strong>Python</strong> парсит файл, он выполняет весь код, который видит, за исключением (как правило) внутреннего кода методов, который он не выполняет.</p>
<p>Существует ряд неочевидных специальных случаев — например, к коду верхнего уровня также относится любой код, используемый для определения значений по умолчанию у методов.</p>
<p>Однако есть простой способ проверить, является ли ваш код <strong>«кодом верхнего уровня»</strong> или нет. Достаточно распарсить ваш код и посмотреть, выполняется ли данный фрагмент кода.</p>
<p>Представьте следующий код:</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import DAG
from airflow.providers.standard.operators.python import PythonOperator
import pendulum


def get_task_id():
    return "print_array_task"  # &lt;- is that code going to be executed?


def get_array():
    return [1, 2, 3]  # &lt;- is that code going to be executed?


with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:
    operator = PythonOperator(
        task_id=get_task_id(),
        python_callable=get_array,
        dag=dag,
    )</pre><p>Чтобы это проверить, вы можете добавить несколько операторов <code>print</code> в код, который хотите проверить, а затем выполнить команду <code>python &lt;my_dag_file&gt;.py</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import DAG
from airflow.providers.standard.operators.python import PythonOperator
import pendulum


def get_task_id():
    print("Executing 1")
    return "print_array_task"  # &lt;- is that code going to be executed? YES


def get_array():
    print("Executing 2")
    return [1, 2, 3]  # &lt;- is that code going to be executed? NO


with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:
    operator = PythonOperator(
        task_id=get_task_id(),
        python_callable=get_array,
        dag=dag,
    )</pre><p>При выполнении этого кода вы увидите:</p><pre class="urvanov-syntax-highlighter-plain-tag">[Breeze:3.10.19] root@cf85ab34571e:/opt/airflow# python /files/test_python.py
Executing 1</pre><p>Это означает, что <code>get_array</code> не выполняется как код верхнего уровня, а <code>get_task_id</code> — выполняется.</p>
<h3><strong>Качество кода и линтинг</strong></h3>
<p>Поддержание высокого качества кода имеет ключевое значение для надёжности и сопровождаемости ваших <strong>workflow</strong> в <strong>Airflow</strong>. Использование инструментов <strong>линтинга</strong> помогает выявлять потенциальные проблемы и обеспечивать соблюдение стандартов кодирования. Одним из таких инструментов является <strong>ruff</strong> — быстрый линтер для Python, который теперь включает специальные правила для Airflow.</p>
<p><strong>ruff</strong> помогает выявлять устаревшие возможности и паттерны, которые могут повлиять на миграцию на <strong>Airflow 3.0</strong>. Например, он включает правила с префиксом AIR, предназначенные для обнаружения потенциальных проблем.</p>
<p><em>Полный список этих правил описан в <a href="https://docs.astral.sh/ruff/rules/#airflow-air" target="_blank" rel="noopener">разделе Airflow (AIR)</a>.</em></p>
<h3><strong>Установка и использование ruff</strong></h3>
<blockquote><p><strong>ruff</strong> — это очень быстрый линтер и автоформаттер для Python, написанный на Rust (в десятки раз быстрее <code>flake8</code>, <code>isort</code>, <code>pylint</code>).</p></blockquote>
<hr />
<p>Установка: установите <code>ruff</code> с помощью <code>pip</code>:</p><pre class="urvanov-syntax-highlighter-plain-tag">pip install "ruff&gt;=0.14.10"</pre><p><strong>Запуск ruff:</strong> выполните <code>ruff</code> для проверки ваших Dag’ов на наличие потенциальных проблем:</p><pre class="urvanov-syntax-highlighter-plain-tag">ruff check dags/ --select AIR3</pre><p>Эта команда проанализирует ваши <strong>Dag</strong>’и, расположенные в директории <code>dags/</code>, и сообщит о проблемах, связанных с указанными правилами.</p>
<p><strong>Пример</strong></p>
<p>Рассмотрим <strong>legacy</strong> <strong>Dag</strong>, определённый следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow import dag
from airflow.datasets import Dataset
from airflow.sensors.filesystem import FileSensor


@dag()
def legacy_dag():
    FileSensor(task_id="wait_for_file", filepath="/tmp/test_file")</pre><p>Запуск <code>ruff</code> приведёт к следующему выводу:</p><pre class="urvanov-syntax-highlighter-plain-tag">dags/legacy_dag.py:7:2: AIR301 Dag should have an explicit schedule argument
dags/legacy_dag.py:12:6: AIR302 schedule_interval is removed in Airflow 3.0
dags/legacy_dag.py:17:15: AIR302 airflow.datasets.Dataset is removed in Airflow 3.0
dags/legacy_dag.py:19:5: AIR303 airflow.sensors.filesystem.FileSensor is moved into ``standard`` provider in Airflow 3.0</pre><p>Интегрируя <code>ruff</code> в ваш процесс разработки, вы можете заблаговременно устранять устаревшие элементы и поддерживать высокое качество кода, что облегчает переход между версиями <strong>Airflow</strong>.</p>
<h3><strong>Динамическая генерация Dag</strong></h3>
<p>Иногда написание <strong>Dag</strong>’ов вручную нецелесообразно. Возможно, у вас есть большое количество <strong>Dag</strong>’ов, которые делают одно и то же, отличаясь лишь параметрами. Или вам нужен набор Dag’ов для загрузки таблиц, но вы не хотите вручную обновлять Dag’и каждый раз при изменении этих таблиц. В этих и других случаях может быть полезно динамически генерировать <strong>Dag</strong>’и.</p>
<p>Избегание избыточной обработки в коде верхнего уровня, описанное в предыдущей главе, особенно важно в случае динамической конфигурации Dag’ов, которая, по сути, может быть реализована одним из следующих способов:</p>
<ul>
<li>через переменные окружения (не путать с <strong>Airflow Variables</strong>)</li>
<li>через внешне предоставляемый, сгенерированный <strong>Python</strong>-код, содержащий метаданные в папке <strong>Dag</strong>’ов</li>
<li>через внешний, сгенерированный файл конфигурационных метаданных в папке <strong>Dag</strong>’ов</li>
</ul>
<p>Некоторые случаи динамической генерации Dag’ов описаны в разделе <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/dynamic-dag-generation.html" target="_blank" rel="noopener"><strong>Dynamic Dag Generation</strong></a>.</p>
<h3><strong>Переменные Airflow</strong></h3>
<p>Использование переменных <strong>Airflow</strong> приводит к сетевым вызовам и обращениям к базе данных, поэтому их применение в коде Python верхнего уровня для DAG-ов следует по возможности избегать, как упоминалось в предыдущей главе Python-код верхнего уровня. Если переменные Airflow всё же необходимо использовать в коде DAG верхнего уровня, их влияние на парсинг DAG можно снизить, включив экспериментальный кэш, настроенный с разумным значением <code>ttl</code>.</p>
<p>Вы можете свободно использовать переменные Airflow внутри методов <code>execute()</code> операторов, а также передавать переменные Airflow в существующие операторы через <strong>Jinja-шаблоны</strong>, что откладывает чтение значения до момента выполнения задачи. Синтаксис шаблона для этого следующий:</p><pre class="urvanov-syntax-highlighter-plain-tag">{{ var.value.&lt;variable_name&gt; }}</pre><p>или, если требуется десериализовать JSON-объект из переменной:</p><pre class="urvanov-syntax-highlighter-plain-tag">{{ var.json.&lt;variable_name&gt; }}</pre><p>В коде верхнего уровня переменные, использующие <strong>Jinja-шаблоны</strong>, не выполняют запрос до момента запуска задачи, тогда как <code>Variable.get()</code> выполняет запрос каждый раз, когда файл DAG парсится планировщиком, если кэширование не включено. Использование <code>Variable.get()</code> без включённого кэширования приводит к неоптимальной производительности при обработке файлов DAG.</p>
<p>В некоторых случаях это может привести к тому, что файл <strong>DAG</strong> не успеет полностью распарситься и произойдёт тайм-аут.</p>
<p><strong>Плохой пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import Variable

foo_var = Variable.get("foo")  # AVOID THAT
bash_use_variable_bad_1 = BashOperator(
    task_id="bash_use_variable_bad_1", bash_command="echo variable foo=${foo_env}", env={"foo_env": foo_var}
)

bash_use_variable_bad_2 = BashOperator(
    task_id="bash_use_variable_bad_2",
    bash_command=f"echo variable foo=${Variable.get('foo')}",  # AVOID THAT
)

bash_use_variable_bad_3 = BashOperator(
    task_id="bash_use_variable_bad_3",
    bash_command="echo variable foo=${foo_env}",
    env={"foo_env": Variable.get("foo")},  # AVOID THAT
)</pre><p><strong>Хороший пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">bash_use_variable_good = BashOperator(
    task_id="bash_use_variable_good",
    bash_command="echo variable foo=${foo_env}",
    env={"foo_env": "{{ var.value.get('foo') }}"},
)

@task
def my_task():
    var = Variable.get("foo")  # This is ok since my_task is called only during task run, not during Dag scan.
    print(var)</pre><p>В целях безопасности рекомендуется использовать <strong>Secrets Backend</strong> для любых переменных, содержащих чувствительные данные.</p>
<h3><strong>Расписания (Timetables)</strong></h3>
<p>Избегайте использования <strong>переменных/подключений Airflow</strong> или обращения к базе данных <strong>Airflow</strong> на верхнем уровне кода расписаний. Доступ к базе данных должен быть отложен до момента выполнения DAG. Это означает, что не следует получать переменные/подключения в качестве аргументов при инициализации класса расписания, а также использовать <strong>Variable/Connection</strong> на верхнем уровне вашего пользовательского модуля расписания.</p>
<p><strong>Плохой пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import Variable
from airflow.timetables.interval import CronDataIntervalTimetable


class CustomTimetable(CronDataIntervalTimetable):
    def __init__(self, *args, something=Variable.get("something"), **kwargs):
        self._something = something
        super().__init__(*args, **kwargs)</pre><p><strong>Хороший пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import Variable
from airflow.timetables.interval import CronDataIntervalTimetable


class CustomTimetable(CronDataIntervalTimetable):
    def __init__(self, *args, something="something", **kwargs):
        self._something = Variable.get(something)
        super().__init__(*args, **kwargs)</pre><p></p>
<h3><strong>Запуск DAG-ов после изменений</strong></h3>
<p>Избегайте запуска <strong>DAG</strong>-ов сразу после их изменения или изменения любых сопутствующих файлов в папке DAG-ов.</p>
<p>Необходимо дать системе достаточно времени для обработки изменённых файлов. Этот процесс включает несколько этапов. Сначала файлы должны быть доставлены планировщику — обычно через распределённую файловую систему или <strong>Git-Sync</strong>, затем планировщик должен распарсить Python-файлы и сохранить их в базе данных. В зависимости от вашей конфигурации, скорости распределённой файловой системы, количества файлов, количества <strong>DAG</strong>-ов, числа изменений в файлах, размеров файлов, количества планировщиков, скорости <strong>CPU</strong>, этот процесс может занимать от нескольких секунд до нескольких минут, а в крайних случаях — многие минуты. Вам следует дождаться появления <strong>DAG</strong>-а в <strong>UI</strong>, прежде чем пытаться его запустить.</p>
<p>Если вы наблюдаете большие задержки между обновлением <strong>DAG</strong>-а и моментом, когда он становится доступен для запуска, вы можете обратить внимание на следующие параметры конфигурации и настроить их в соответствии с вашими потребностями (подробности по каждому параметру см. по ссылкам):</p>
<ul>
<li><code>scheduler_idle_sleep_time</code> &#8212; Управляет временем ожидания планировщика между циклами, но если в цикле ничего не нужно делать, то есть если что-то запланировано, то следующая итерация цикла начнется немедленно.</li>
<li><code>min_file_process_interval</code> &#8212; Количество секунд, по истечении которых происходит разбор DAG-файла. Разбор DAG-файла происходит каждые несколько секунд. Обновления DAG-файлов отражаются после этого интервала. Низкое значение этого параметра приведет к увеличению загрузки ЦП.</li>
<li><code>refresh_interval</code> &#8212; Как часто (в секундах) следует обновлять или искать новые файлы в пакете DAG.</li>
<li><code>parsing_processes</code> &#8212; Процессор DAG может запускать несколько процессов параллельно для анализа DAG. Это определяет, сколько процессов будет запущено.</li>
<li><code>file_parsing_sort_mode</code> &#8212; Один из вариантов <strong>modified_time</strong>, <strong>random_seeded_by_host</strong> и <strong>alphabetical</strong>. Процессор DAG перечислит и отсортирует файлы DAG, чтобы определить порядок их анализа.
<ul>
<li><code>modified_time</code> &#8212; Сортировка файлов по времени изменения. Это полезно в больших масштабах для предварительной обработки недавно измененных DAG-графов.</li>
<li><code>random_seeded_by_host</code> &#8212; Произвольная сортировка файлов несколькими процессорами DAG, но в одном и том же порядке на одном и том же хосте, что позволяет каждому процессору обрабатывать файлы в разном порядке.</li>
<li><code>alphabetical</code> &#8212; Сортировка по имени файла</li>
</ul>
</li>
</ul>
<h3><strong>Пример паттерна watcher с правилами триггеров</strong></h3>
<p>Паттерн <strong>watcher</strong> — это способ организации DAG-а с задачей, которая «наблюдает» за состояниями других задач. Его основное назначение — пометить запуск DAG-а как <strong>failed</strong>, если любая другая задача завершилась с ошибкой. Необходимость в этом возникла в системных тестах <strong>Airflow</strong>, которые представляют собой <strong>DAG</strong>-и с разными задачами (аналогично тесту, состоящему из шагов).</p>
<p>Обычно, когда любая задача завершается с ошибкой, все остальные задачи не выполняются, и весь запуск <strong>DAG</strong>-а также получает статус <strong>failed</strong>. Однако при использовании правил триггеров можно нарушить стандартный поток выполнения задач, и весь DAG может получить статус, отличный от ожидаемого. Например, можно иметь задачу очистки ресурсов (<strong>teardown task</strong>) с правилом триггера <code>TriggerRule.ALL_DONE</code>, которая будет выполняться независимо от состояния других задач (например, для освобождения ресурсов). В такой ситуации <strong>DAG</strong> всегда выполнит эту задачу, и запуск <strong>DAG</strong>-а получит статус именно этой задачи, в результате чего можно потерять информацию о задачах, завершившихся с ошибкой. Если требуется гарантировать, что <strong>DAG</strong> с задачей очистки завершится с ошибкой при падении любой задачи, необходимо использовать паттерн <strong>watcher</strong>.</p>
<p>Задача <strong>watcher</strong> — это задача, которая всегда завершается с ошибкой при выполнении, но она должна запускаться только в том случае, если любая другая задача завершилась с ошибкой. Для неё необходимо установить правило триггера <code>TriggerRule.ONE_FAILED</code>, а также сделать её <strong>downstream-задачей</strong> для всех остальных задач в <strong>DAG</strong>-е. Благодаря этому, если все остальные задачи завершатся <strong>успешно</strong>, watcher будет пропущена, а если произойдёт ошибка, задача <strong>watcher</strong> выполнится и завершится с ошибкой, что приведёт к статусу <strong>failed</strong> у всего запуска <strong>DAG</strong>-а.</p>
<p><strong>Примечание</strong></p>
<p>Следует учитывать, что правила триггеров опираются только на непосредственные upstream-задачи (родительские). Например, <code>TriggerRule.ONE_FAILED</code> будет игнорировать любые задачи со статусом <strong>failed</strong> (или <strong>upstream_failed</strong>), которые не являются прямыми родителями параметризуемой задачи.</p>
<p>Проще понять концепцию на примере. Предположим, у нас есть следующий DAG:</p><pre class="urvanov-syntax-highlighter-plain-tag">from datetime import datetime

from airflow.sdk import DAG
from airflow.sdk import task
from airflow.exceptions import AirflowException
from airflow.providers.standard.operators.bash import BashOperator
from airflow.utils.trigger_rule import TriggerRule


@task(trigger_rule=TriggerRule.ONE_FAILED, retries=0)
def watcher():
    raise AirflowException("Failing task because one or more upstream tasks failed.")


with DAG(
    dag_id="watcher_example",
    schedule="@once",
    start_date=datetime(2021, 1, 1),
    catchup=False,
) as dag:
    failing_task = BashOperator(task_id="failing_task", bash_command="exit 1", retries=0)
    passing_task = BashOperator(task_id="passing_task", bash_command="echo passing_task")
    teardown = BashOperator(
        task_id="teardown",
        bash_command="echo teardown",
        trigger_rule=TriggerRule.ALL_DONE,
    )

    failing_task &gt;&gt; passing_task &gt;&gt; teardown
    list(dag.tasks) &gt;&gt; watcher()</pre><p>Визуальное представление этого <strong>DAG</strong>-а после выполнения выглядит следующим образом:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag.png"><img fetchpriority="high" decoding="async" class="aligncenter size-full wp-image-2767" src="https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag.png" alt="" width="893" height="119" srcset="https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag.png 893w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-300x40.png 300w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-768x102.png 768w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-450x60.png 450w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-780x104.png 780w" sizes="(max-width: 893px) 100vw, 893px" /></a></p>
<p>В нём есть несколько задач, выполняющих разные роли:</p>
<ul>
<li><code>failing_task</code> — всегда завершается с ошибкой;</li>
<li><code>passing_task</code> — всегда завершается успешно (если выполняется);</li>
<li><code>teardown</code> — всегда запускается (независимо от состояний других задач) и должна всегда завершаться успешно;</li>
<li><code>watcher</code> — является <strong>downstream-задачей</strong> для всех остальных задач, то есть запускается, когда любая задача завершается с ошибкой, и тем самым переводит весь запуск <strong>DAG</strong>-а в состояние <strong>failed</strong>, так как является листовой задачей.</li>
</ul>
<p>Важно отметить, что без задачи <strong>watcher</strong> весь запуск <strong>DAG</strong>-а получит состояние <strong>success</strong>, поскольку единственная задача, завершающаяся с ошибкой, не является листовой, а задача <strong>teardown</strong> завершится успешно. Если мы хотим, чтобы <strong>watcher</strong> отслеживала состояние всех задач, необходимо сделать её зависимой от каждой из них по отдельности. Благодаря этому мы можем перевести запуск <strong>DAG</strong>-а в состояние <strong>failed</strong>, если любая из задач завершится с ошибкой. Обратите внимание, что для задачи watcher установлено правило триггера <strong>&#171;one_failed&#187;</strong>.</p>
<p>С другой стороны, без задачи <strong>teardown</strong> задача <strong>watcher</strong> не понадобилась бы, поскольку <strong>failing_task</strong> передала бы свой статус <strong>failed</strong> <strong>downstream-задаче</strong> <strong>passing_task</strong>, и весь запуск <strong>DAG</strong>-а также получил бы статус <strong>failed</strong>.</p>
<h3><strong>Использование исключения AirflowClusterPolicySkipDag в кластерных политиках для пропуска определённых DAG-ов</strong></h3>
<p><em>Добавлено в версии 2.7.</em></p>
<p><strong>DAG</strong>-и <strong>Airflow</strong> обычно разворачиваются и обновляются из конкретной ветки Git-репозитория с помощью <code>git-sync</code>. Однако, когда по операционным причинам требуется запускать несколько кластеров <strong>Airflow</strong>, поддержка нескольких <strong>Git-веток</strong> становится крайне неудобной. Особенно это усложняется, когда необходимо периодически синхронизировать две отдельные ветки (например, <strong>prod</strong> и <strong>beta</strong>) с использованием корректной стратегии ветвления.</p>
<ul>
<li><code>cherry-pick</code> слишком трудоёмок для сопровождения <strong>Git</strong>-репозитория;</li>
<li><code>hard-reset</code> не является рекомендуемым подходом в <strong>GitOps</strong>.</li>
</ul>
<p>Вместо этого можно рассмотреть вариант подключения нескольких кластеров Airflow к одной и той же ветке <strong>Git</strong> (например, main) и управления ими с помощью разных переменных окружения и различных конфигураций подключений с одинаковым connection_id. При необходимости также можно выбрасывать исключение <code>AirflowClusterPolicySkipDag</code> в кластерной политике, чтобы загружать определённые <strong>DAG</strong>-и в <strong>DagBag</strong> только в конкретном развертывании Airflow.</p><pre class="urvanov-syntax-highlighter-plain-tag">def dag_policy(dag: DAG):
    """Пропуск DAG-а с тегом `only_for_beta`."""

    if "only_for_beta" in dag.tags:
        raise AirflowClusterPolicySkipDag(
            f"Dag {dag.dag_id} is not loaded on the production cluster, due to `only_for_beta` tag."
        )</pre><p>Приведённый выше пример показывает фрагмент кода <strong>dag_policy</strong>, который пропускает DAG в зависимости от тегов, указанных у него.</p>
<h2><strong>Снижение сложности DAG-ов</strong></h2>
<p>Хотя Airflow хорошо справляется с обработкой большого количества DAG-ов с множеством задач и зависимостей между ними, при наличии большого числа сложных DAG-ов их сложность может негативно сказаться на производительности планирования. Одним из способов поддерживать высокую производительность и эффективное использование экземпляра Airflow является стремление к упрощению и оптимизации DAG-ов везде, где это возможно. Следует помнить, что процесс парсинга и создания DAG-а — это всего лишь выполнение Python-кода, и именно от вас зависит, насколько производительным он будет. Не существует «волшебных рецептов» для того, чтобы сделать DAG «менее сложным» — поскольку это Python-код, именно автор DAG-а контролирует сложность своего кода.</p>
<p>Не существует метрик «сложности DAG-а», и в частности нет метрик, которые могли бы однозначно сказать, является ли DAG «достаточно простым». Однако, как и в случае с любым Python-кодом, можно определить, что код DAG-а стал «проще» или «быстрее», если он оптимизирован. Если вы хотите оптимизировать свои DAG-и, можно предпринять следующие действия:</p>
<ul>
<li><strong>Сделайте загрузку DAG-а быстрее:</strong><br />
Это единственная рекомендация по улучшению, которая может быть реализована разными способами, но именно она оказывает наибольшее влияние на производительность планировщика. Если у вас есть возможность ускорить загрузку <strong>DAG</strong>-а — делайте это, если ваша цель — повышение производительности. Обратитесь к разделу <strong>Python</strong>-код верхнего уровня для получения советов, а также к <strong>Dag Loader Test</strong>, чтобы оценить время загрузки <strong>DAG</strong>-а.</li>
<li><strong>Генерируйте более простую структуру DAG-а:</strong><br />
Каждая зависимость между задачами добавляет дополнительную нагрузку на планирование и выполнение. <strong>DAG</strong> с простой линейной структурой <code>A → B → C</code> будет испытывать меньшие задержки при планировании задач, чем <strong>DAG</strong> с глубоко вложенной древовидной структурой, например с экспоненциально растущим числом зависимых задач. Если вы можете сделать свои DAG-и более линейными — так, чтобы в каждый момент времени было как можно меньше потенциальных задач-кандидатов на запуск, — это, как правило, улучшит общую производительность планирования.</li>
<li><strong>Уменьшите количество DAG-ов в одном файле:</strong><br />
Хотя <strong>Airflow 2</strong> оптимизирован для сценария, при котором в одном файле описано несколько DAG-ов, в системе есть компоненты, из-за которых такой подход иногда менее производителен или приводит к большим задержкам по сравнению с разбиением DAG-ов по нескольким файлам. Уже сам факт того, что один файл может быть обработан только одним <code>FileProcessor</code>, делает этот подход менее масштабируемым. Если у вас много DAG-ов, генерируемых из одного файла, рассмотрите возможность их разделения, особенно если вы замечаете, что изменения в файлах DAG-ов долго отражаются в <strong>UI Airflow</strong>.</li>
<li><strong>Пишите эффективный Python-код:</strong><br />
Необходимо соблюдать баланс между меньшим количеством DAG-ов в файле (как указано выше) и общим объёмом кода. Файлы <strong>Python</strong>, описывающие DAG-и, должны следовать лучшим практикам программирования и не должны рассматриваться как конфигурационные файлы. Если ваши DAG-и используют схожий код, не следует копировать его снова и снова в большое количество почти идентичных исходных файлов, так как это приведёт к ненужным повторным импортам одних и тех же ресурсов. Вместо этого следует стремиться к минимизации повторяющегося кода во всех DAG-ах, чтобы приложение работало эффективно и было проще в отладке.<br />
<em>См. раздел Dynamic Dag Generation о том, как создавать несколько DAG-ов с похожей логикой.</em></li>
</ul>
<h2>Тестирование DAG-а</h2>
<p>Пользователям Airflow следует относиться к DAG-ам как к коду промышленного уровня, и у DAG-ов должны быть различные связанные тесты, чтобы гарантировать получение ожидаемых результатов. Для DAG-а можно написать широкий спектр тестов. Рассмотрим некоторые из них.</p>
<h3><strong>Тест загрузки DAG-а (Dag Loader Test)</strong></h3>
<p>Этот тест должен гарантировать, что ваш DAG не содержит кода, который вызывает ошибку во время загрузки. Для запуска этого теста пользователю не требуется писать дополнительный код.</p><pre class="urvanov-syntax-highlighter-plain-tag">python your-dag-file.py</pre><p>Выполнение приведённой выше команды без ошибок гарантирует, что в DAG-е нет неустановленных зависимостей, синтаксических ошибок и т. д. Убедитесь, что вы загружаете DAG в окружении, соответствующем окружению планировщика — с теми же зависимостями, переменными окружения и общим кодом, на который ссылается DAG.</p>
<p>Это также отличный способ проверить, загружается ли DAG быстрее после оптимизации, если вы хотите попробовать оптимизировать время загрузки DAG-а. Просто запустите DAG и измерьте время его выполнения, но, опять же, необходимо убедиться, что DAG выполняется с теми же зависимостями, переменными окружения и общим кодом.</p>
<p>Существует множество способов измерить время выполнения, один из них в <strong>Linux</strong> — использование встроенной команды <code>time</code>. Обязательно запускайте её несколько раз подряд, чтобы учесть эффекты кэширования. Сравнивайте результаты до и после оптимизации (в одинаковых условиях — на той же машине, в том же окружении и т. д.), чтобы оценить влияние оптимизации.</p><pre class="urvanov-syntax-highlighter-plain-tag">time python airflow/example_dags/example_python_operator.py</pre><p><strong>Результат:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">real    0m0.699s
user    0m0.590s
sys     0m0.108s</pre><p>Важной метрикой является <strong>«real time»</strong>, которая показывает, сколько времени заняла обработка DAG-а. Обратите внимание, что при таком способе загрузки файла каждый раз запускается новый интерпретатор, поэтому присутствует начальное время инициализации, которого нет при парсинге DAG-а самим <strong>Airflow</strong>. Оценить время инициализации можно, выполнив:</p><pre class="urvanov-syntax-highlighter-plain-tag">time python -c ''</pre><p><strong>Результат:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">real    0m0.073s
user    0m0.037s
sys     0m0.039s</pre><p>В данном случае начальное время запуска интерпретатора составляет примерно ~0,07 с, что составляет около 10% времени, необходимого для парсинга <code>example_python_operator.py</code> выше, поэтому фактическое время парсинга для примера <strong>DAG</strong>-а составляет примерно ~0,62 с.</p>
<p><em>Подробности о том, как тестировать отдельные операторы, см. в разделе Testing a Dag.</em></p>
<h3>Юнит-тесты</h3>
<p><strong>Юнит-тесты</strong> гарантируют отсутствие некорректного кода в вашем DAG-е. Вы можете писать юнит-тесты как для отдельных задач, так и для самого DAG-а.</p>
<p>Юнит-тест загрузки DAG-а:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pytest

from airflow.models import DagBag

@pytest.fixture()
def dagbag():
    return DagBag()

def test_dag_loaded(dagbag):
    dag = dagbag.get_dag(dag_id="hello_world")
    assert dagbag.import_errors == {}
    assert dag is not None
    assert len(dag.tasks) == 1</pre><p><strong>Юнит-тест структуры DAG-а:</strong></p>
<p>Это пример теста, предназначенного для проверки структуры DAG-а, сгенерированного кодом, путём сравнения с объектом типа <code>dict</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">def assert_dag_dict_equal(source, dag):
    assert dag.task_dict.keys() == source.keys()
    for task_id, downstream_list in source.items():
        assert dag.has_task(task_id)
        task = dag.get_task(task_id)
        assert task.downstream_task_ids == set(downstream_list)

def test_dag():
    assert_dag_dict_equal(
        {
            "DummyInstruction_0": ["DummyInstruction_1"],
            "DummyInstruction_1": ["DummyInstruction_2"],
            "DummyInstruction_2": ["DummyInstruction_3"],
            "DummyInstruction_3": [],
        },
        dag,
    )</pre><p><strong>Юнит-тест</strong> для пользовательского оператора:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pendulum

from airflow.sdk import DAG, TaskInstanceState

def test_my_custom_operator_execute_no_trigger(dag):
    TEST_TASK_ID = "my_custom_operator_task"
    with DAG(
        dag_id="my_custom_operator_dag",
        schedule="@daily",
        start_date=pendulum.datetime(2021, 9, 13, tz="UTC"),
    ) as dag:
        MyCustomOperator(
            task_id=TEST_TASK_ID,
            prefix="s3://bucket/some/prefix",
        )

    dagrun = dag.test()
    ti = dagrun.get_task_instance(task_id=TEST_TASK_ID)
    assert ti.state == TaskInstanceState.SUCCESS
    # Assert something related to tasks results: ti.xcom_pull()</pre><p></p>
<h3><strong>Самопроверки (Self-Checks)</strong></h3>
<p>Вы также можете реализовать проверки непосредственно в DAG-е, чтобы убедиться, что задачи производят ожидаемые результаты. Например, если у вас есть задача, которая выгружает данные в <strong>S3</strong>, вы можете реализовать проверку в следующей задаче. Такая проверка, к примеру, может удостовериться, что партиция создана в <strong>S3</strong>, и выполнить простые проверки, чтобы определить корректность данных.</p>
<p>Аналогично, если у вас есть задача, которая запускает микросервис в <strong>Kubernetes</strong> или <strong>Mesos</strong>, следует проверить, был ли сервис успешно запущен, используя <code>airflow.providers.http.sensors.http.HttpSensor</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">task = PushToS3(...)
check = S3KeySensor(
    task_id="check_parquet_exists",
    bucket_key="s3://bucket/key/foo.parquet",
    poke_interval=0,
    timeout=0,
)
task &gt;&gt; check</pre><p></p>
<h3><strong>Staging-окружение</strong></h3>
<p>По возможности поддерживайте <strong>staging-окружение</strong> для тестирования полного выполнения DAG-а перед деплоем в <strong>production</strong>. Убедитесь, что ваш DAG параметризован и позволяет изменять переменные, например путь вывода при работе с <strong>S3</strong> или базу данных, используемую для чтения конфигурации. Не хардкодьте значения внутри DAG-а и не изменяйте их вручную в зависимости от окружения.</p>
<p>Для параметризации <strong>DAG</strong>-а вы можете использовать переменные окружения.</p><pre class="urvanov-syntax-highlighter-plain-tag">import os

dest = os.environ.get("MY_DAG_DEST_PATH", "s3://default-target/path/")</pre><p></p>
<h2>Мокирование переменных и подключений</h2>
<p>При написании тестов для кода, использующего <strong>переменные</strong> или <strong>подключения</strong>, необходимо убедиться, что они существуют во время выполнения тестов. Очевидное решение — сохранить эти объекты в базе данных, чтобы их можно было прочитать во время выполнения кода. Однако чтение и запись объектов в базу данных сопровождаются дополнительными временными затратами. Чтобы ускорить выполнение тестов, имеет смысл имитировать наличие этих объектов без сохранения их в базе данных. Для этого можно создать переменные окружения, замокировав <code>os.environ</code> с помощью <code>unittest.mock.patch.dict()</code>.</p>
<p>Для переменных используйте <code>AIRFLOW_VAR_{KEY}</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">with mock.patch.dict("os.environ", AIRFLOW_VAR_KEY="env-value"):
    assert "env-value" == Variable.get("key")</pre><p>Для подключений используйте <code>AIRFLOW_CONN_{CONN_ID}</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">conn = Connection(
    conn_type="gcpssh",
    login="cat",
    host="conn-host",
)
conn_uri = conn.get_uri()
with mock.patch.dict("os.environ", AIRFLOW_CONN_MY_CONN=conn_uri):
    assert "cat" == Connection.get_connection_from_secrets("my_conn").login</pre><p></p>
<h2>Обслуживание metadata DB</h2>
<p>Со временем база метаданных будет увеличивать занимаемое дисковое пространство по мере накопления запусков <strong>DAG</strong>-ов и задач, а также логов событий.</p>
<p>Для очистки старых данных можно использовать <strong>Airflow CLI</strong> с командой <code>airflow db clean</code>.</p>
<p>Подробности см. в разделе использования <strong>db clean</strong>.</p>
<h2>Обновления и откаты версий</h2>
<h3><strong>Резервное копирование базы данных</strong></h3>
<p>Всегда разумно делать резервную копию базы метаданных перед выполнением любых операций, изменяющих базу данных.</p>
<h3><strong>Отключение планировщика</strong></h3>
<p>Во время проведения такого обслуживания можно рассмотреть отключение <strong>кластера Airflow</strong>.</p>
<p>Один из способов — установить параметр <code>[scheduler] &gt; use_job_schedule</code> в значение <code>False</code> и дождаться завершения всех выполняющихся <strong>DAG</strong>-ов; после этого новые запуски DAG-ов не будут создаваться, если только они не будут запущены извне.</p>
<p>Лучший способ (хотя и более ручной) — использовать команду <code>dags pause</code>. Вам потребуется заранее зафиксировать список DAG-ов, которые не находятся в состоянии паузы, чтобы затем знать, какие из них нужно вернуть в активное состояние после завершения обслуживания. Сначала выполните <code>airflow dags list</code> и сохраните список не приостановленных <strong>DAG</strong>-ов. Затем используйте этот же список для выполнения <code>dags pause</code> для каждого DAG-а перед обслуживанием и <code>dags unpause</code> после его завершения. Преимущество такого подхода в том, что после обновления можно попробовать снять с паузы только один или два <strong>DAG</strong>-а (например, специальные тестовые <strong>DAG</strong>-и), чтобы убедиться, что всё работает корректно, прежде чем включать все <strong>DAG</strong>-и обратно.</p>
<h3><strong>Добавление DAG-ов для интеграционного тестирования</strong></h3>
<p>Полезно добавить несколько <strong>DAG</strong>-ов для <strong>«интеграционного тестирования»</strong>, которые используют все основные сервисы вашей экосистемы (например, <strong>S3</strong>, <strong>Snowflake</strong>, <strong>Vault</strong>), но с тестовыми ресурсами или «<strong>dev»-аккаунтами</strong>. Эти тестовые <strong>DAG</strong>-и можно запускать первыми после обновления, поскольку в случае их сбоя это не приведёт к негативным последствиям, и вы сможете откатиться к резервной копии. Если же они выполняются успешно, это подтвердит, что кластер способен выполнять задачи с использованием необходимых библиотек и сервисов.</p>
<p>Например, если вы используете внешний <strong>secrets backend</strong>, убедитесь, что у вас есть задача, которая извлекает подключение. Если вы используете <code>KubernetesPodOperator</code>, добавьте задачу, выполняющую <code>sleep 30; echo "hello"</code>. Если требуется запись в <strong>S3</strong> — реализуйте это в тестовой задаче. А если нужен доступ к базе данных, добавьте задачу, выполняющую <code>select 1</code> на сервере.</p>
<h3><strong>Очистка данных перед обновлением (Prune data)</strong></h3>
<p>Некоторые миграции базы данных могут занимать значительное время. Если база метаданных имеет очень большой размер, перед выполнением обновления стоит рассмотреть возможность очистки части старых данных с помощью команды <code>db clean</code>. Используйте с осторожностью.</p>
<h2>Работа с конфликтующими и сложными Python-зависимостями</h2>
<p><strong>Airflow</strong> имеет множество Python-зависимостей, и иногда зависимости <strong>Airflow</strong> конфликтуют с зависимостями, которые ожидает код ваших задач. Поскольку по умолчанию окружение <strong>Airflow</strong> представляет собой единый набор <strong>Python-зависимостей</strong> и одно <strong>Python-окружение</strong>, нередко возникают ситуации, когда разные задачи требуют различных зависимостей, которые при этом конфликтуют между собой.</p>
<p>Если вы используете предопределённые <strong>Operator</strong>’ы <strong>Airflow</strong> для взаимодействия с внешними сервисами, выбор обычно невелик, однако такие операторы, как правило, имеют зависимости, не конфликтующие с базовыми зависимостями <strong>Airflow</strong>. Airflow использует механизм <strong>constraints</strong>, что означает наличие «зафиксированного» набора зависимостей, с которым сообщество гарантирует корректную установку <strong>Airflow</strong> (включая все <strong>community-провайдеры</strong>) без возникновения конфликтов. При этом вы можете обновлять провайдеры независимо, и их <strong>constraints</strong> вас не ограничивают, поэтому вероятность конфликтов зависимостей ниже (хотя такие зависимости всё равно необходимо тестировать). Таким образом, при использовании предопределённых операторов вероятность столкнуться с конфликтующими зависимостями минимальна или отсутствует вовсе.</p>
<p>Однако при более «современном» подходе к использованию <strong>Airflow</strong> — когда вы применяете <strong>TaskFlow API</strong> и большинство операторов реализуете с помощью собственного Python-кода, либо когда вы пишете собственные <strong>Custom Operator</strong>’ы — вы можете столкнуться с ситуацией, когда зависимости, требуемые вашим кастомным кодом, конфликтуют с зависимостями Airflow, или даже когда зависимости нескольких ваших <strong>Custom Operator</strong>’ов конфликтуют между собой.</p>
<p>Существует несколько стратегий, которые можно использовать для смягчения этой проблемы. И хотя работа с конфликтами зависимостей в кастомных операторах может быть сложной, она значительно упрощается при использовании <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code> или <code>airflow.providers.standard.operators.python.ExternalPythonOperator</code> — как при прямом использовании классического подхода с <strong>Operator</strong>’ами, так и при использовании задач, декорированных <code>@task.virtualenv</code> или <code>@task.external_python</code>, если вы применяете <strong>TaskFlow</strong>.</p>
<p>Начнём со стратегий, которые проще всего реализовать (хотя они имеют определённые ограничения и накладные расходы), и постепенно перейдём к стратегиям, требующим изменений в развертывании Airflow.</p>
<h3><strong>Использование PythonVirtualenvOperator</strong></h3>
<p>Это самая простая в использовании и одновременно наиболее ограниченная стратегия. <code>PythonVirtualenvOperator</code> позволяет динамически создавать <code>virtualenv</code>, в котором будет выполняться ваш <strong>Python-callable</strong>. В современном подходе <strong>TaskFlow</strong>, описанном в разделе Pythonic Dags with the <strong>TaskFlow API</strong>, это также можно сделать, задекорировав callable декоратором <code>@task.virtualenv</code> (рекомендуемый способ использования оператора). Каждая задача <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code> может иметь собственный независимый <strong>Python</strong> <code>virtualenv</code> (динамически создаваемый при каждом запуске задачи) и задавать детальный набор зависимостей, которые необходимо установить для выполнения этой задачи.</p>
<p><strong>Оператор берёт на себя:</strong></p>
<ul>
<li>создание <code>virtualenv</code> на основе вашего окружения,</li>
<li>сериализацию вашего Python-callable и передачу его на выполнение Python-интерпретатору внутри <code>virtualenv</code>,</li>
<li>выполнение <code>callable</code>, получение результата и передачу его через <code>XCom</code>, если это указано.</li>
</ul>
<p><strong>Преимущества оператора:</strong></p>
<ul>
<li>Нет необходимости заранее подготавливать <code>virtualenv</code>. Он динамически создаётся перед запуском задачи и удаляется после её завершения, поэтому для использования нескольких виртуальных окружений не требуется ничего особенного (кроме наличия пакета <code>virtualenv</code> в зависимостях <strong>Airflow</strong>).</li>
<li>Вы можете запускать задачи с разными наборами зависимостей на одних и тех же воркерах — таким образом, ресурсы памяти переиспользуются (хотя см. ниже про накладные расходы на <strong>CPU</strong> при создании <code>virtualenv</code>).</li>
<li>В крупных инсталляциях авторам Dag’ов не нужно просить кого-то создавать <code>virtualenv</code> за них. Как автор <strong>Dag’а</strong>, вам достаточно иметь установленную зависимость <code>virtualenv</code>, и вы можете задавать и изменять окружения по своему усмотрению.</li>
<li>Не требуется изменений в требованиях к деплою — независимо от того, используете ли вы локальный <code>virtualenv</code>, <strong>Docker</strong> или <strong>Kubernetes</strong>, задачи будут работать без добавления чего-либо в окружение развертывания.</li>
<li>Автору Dag’ов не нужно изучать контейнеры или <strong>Kubernetes</strong>. Для такого подхода к написанию <strong>Dag’ов</strong> достаточно знания <strong>Python-зависимостей</strong>.</li>
</ul>
<p>У данного оператора есть определённые ограничения и накладные расходы:</p>
<ul>
<li>Ваш <strong>Python-callable</strong> должен быть сериализуемым. Существует множество Python-объектов, которые не сериализуются стандартной библиотекой <code>pickle</code>. Часть этих ограничений можно обойти с помощью библиотеки <code>dill</code>, однако и она не решает всех проблем сериализации.</li>
<li>Все зависимости, отсутствующие в окружении <strong>Airflow</strong>, должны импортироваться локально внутри используемого <code>callable</code>, а код верхнего уровня <strong>Dag</strong> не должен импортировать или использовать эти библиотеки.</li>
<li><code>Virtualenv</code> запускаются в рамках одной и той же операционной системы, поэтому они не могут иметь конфликтующие системные зависимости (устанавливаемые через <code>apt</code> или <code>yum</code>). Независимо могут устанавливаться только <strong>Python-зависимости</strong>.</li>
<li>Оператор добавляет накладные расходы на <strong>CPU</strong>, <strong>сеть</strong> и общее время выполнения каждой задачи — Airflow вынужден пересоздавать <code>virtualenv</code> с нуля для каждого запуска задачи.</li>
<li>Воркеры должны иметь доступ к <strong>PyPI</strong> или приватным репозиториям для установки зависимостей.</li>
<li>Динамическое создание <code>virtualenv</code> подвержено временным сбоям (например, если репозиторий недоступен или возникают сетевые проблемы при подключении к нему).</li>
<li>Легко попасть в ситуацию «слишком» динамичного окружения — устанавливаемые зависимости могут обновляться, а их транзитивные зависимости могут получать независимые обновления, в результате чего задача может перестать работать из-за выхода новой версии зависимости или вы можете стать жертвой атаки на цепочку поставок, когда новая версия зависимости оказывается вредоносной.</li>
<li>Задачи изолированы друг от друга только за счёт выполнения в разных окружениях. Это означает, что выполняющиеся задачи всё ещё могут влиять друг на друга — например, последующие задачи, выполняемые на том же воркере, могут быть затронуты предыдущими задачами, которые создавали или изменяли файлы и т. п.</li>
</ul>
<p>Подробные примеры использования <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code> приведены в соответствующем разделе руководства по <strong>TaskFlow API</strong>.</p>
<h3><strong>Использование ExternalPythonOperator</strong></h3>
<p><em>Добавлено в версии 2.4.</em></p>
<p>Более сложным в использовании, но при этом значительно менее накладным с точки зрения ресурсов, безопасности и стабильности вариантом является использование <code>airflow.providers.standard.operators.python.ExternalPythonOperator</code>. В современном подходе TaskFlow, описанном в разделе <strong>Pythonic Dags with the TaskFlow API</strong>, этого также можно добиться, задекорировав ваш callable декоратором @task.external_python (рекомендуемый способ использования оператора). Однако для этого требуется заранее подготовленное, неизменяемое Python-окружение. В отличие от <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code>, вы не можете добавлять новые зависимости в такое предсуществующее окружение. Все необходимые зависимости должны быть добавлены заранее и быть доступны на всех воркерах, если Airflow работает в распределённом окружении.</p>
<p>Таким образом, вы избегаете накладных расходов и проблем, связанных с пересозданием <code>virtualenv</code>, однако такие окружения необходимо подготовить и задеплоить вместе с установкой Airflow. Обычно в этот процесс вовлечены специалисты, отвечающие за установку Airflow, и в крупных инсталляциях это, как правило, другие люди, нежели авторы Dag’ов (DevOps/System Admins).</p>
<p>Такие <code>virtualenv</code> могут быть подготовлены разными способами: при использовании <code>LocalExecutor</code> их достаточно установить на машине, где запускается планировщик; при использовании распределённой установки <code>Celery</code> должна существовать пайплайн, который устанавливает эти <code>virtualenv</code> на нескольких машинах; наконец, если вы используете <strong>Docker-образы</strong> (например, в <strong>Kubernetes</strong>), создание <code>virtualenv</code> должно быть добавлено в пайплайн сборки вашего кастомного образа.</p>
<p><strong>Преимущества оператора:</strong></p>
<ul>
<li>Отсутствие накладных расходов при запуске задачи. <code>Virtualenv</code> уже готов в момент начала выполнения задачи.</li>
<li>Вы можете запускать задачи с разными наборами зависимостей на одних и тех же воркерах — таким образом, все ресурсы переиспользуются.</li>
<li>Воркерам не требуется доступ к <strong>PyPI</strong> или приватным репозиториям. Меньше вероятность временных сбоев, связанных с сетью.</li>
<li>Зависимости могут быть заранее проверены администраторами и командой безопасности, и никакой новый, неожиданный код не будет динамически добавляться. Это полезно как с точки зрения безопасности, так и стабильности.</li>
<li>Минимальное влияние на деплой — вам не нужно переходить на Docker-контейнеры или <strong>Kubernetes</strong>, чтобы эффективно использовать оператор.</li>
<li>Автору <strong>Dag</strong>’ов не нужно изучать контейнеры или <strong>Kubernetes</strong>. Для написания Dag’ов таким способом достаточно знания <strong>Python</strong> и работы с <strong>requirements</strong>.</li>
</ul>
<p><strong>Недостатки:</strong></p>
<ul>
<li>Окружения должны быть подготовлены заранее. Обычно это означает, что вы не можете менять их «на лету»: добавление новых зависимостей или изменение существующих требует как минимум повторного деплоя <strong>Airflow</strong>, а время итераций при разработке новых версий может увеличиться.</li>
<li>Ваш <strong>Python-callable</strong> должен быть сериализуемым. Существует множество <strong>Python-объектов</strong>, которые не сериализуются стандартной библиотекой <code>pickle</code>. Часть этих ограничений можно смягчить с помощью библиотеки <code>dill</code>, однако она также не решает всех проблем сериализации.</li>
<li>Все зависимости, отсутствующие в окружении Airflow, должны импортироваться локально внутри используемого callable, а код верхнего уровня Dag не должен импортировать или использовать эти библиотеки.</li>
<li>Virtualenv запускаются в рамках одной и той же операционной системы, поэтому они не могут иметь конфликтующие системные зависимости (устанавливаемые через apt или yum). Независимо могут устанавливаться только Python-зависимости.</li>
<li>Задачи изолированы друг от друга только за счёт выполнения в разных окружениях. Это означает, что выполняющиеся задачи всё ещё могут влиять друг на друга — например, последующие задачи, выполняемые на том же воркере, могут быть затронуты предыдущими задачами, которые создавали или изменяли файлы и т. п.</li>
</ul>
<p><code>PythonVirtualenvOperator</code> и <code>ExternalPythonOperator</code> можно рассматривать как взаимодополняющие инструменты, которые упрощают переход от этапа разработки к продакшену. Как автор Dag’ов, вы обычно будете итерироваться с зависимостями и разрабатывать Dag, используя <code>PythonVirtualenvOperator</code> (декорируя задачи <code>@task.virtualenv</code>), а после завершения итераций и внесения изменений, для продакшена, скорее всего, переключитесь на <code>ExternalPythonOperator</code> (и <code>@task.external_python</code>) после того, как команды <strong>DevOps/System Admin</strong> развернут новые зависимости в предсуществующих <code>virtualenv</code> в продакшене. Преимущество такого подхода в том, что вы в любой момент можете вернуть декоратор обратно и продолжить «динамическую» разработку с <code>PythonVirtualenvOperator</code>.</p>
<p>Подробные примеры использования <code>airflow.providers.standard.operators.python.ExternalPythonOperator</code> приведены в разделе TaskFlow External Python example.</p>
<h3><strong>Использование DockerOperator или KubernetesPodOperator</strong></h3>
<p>Ещё одной стратегией является использование <code>airflow.providers.docker.operators.docker.DockerOperator</code> и <code>airflow.providers.cncf.kubernetes.operators.pod.KubernetesPodOperator</code>. Для этого требуется, чтобы <strong>Airflow</strong> имел доступ к <strong>Docker Engine</strong> или кластеру <strong>Kubernetes</strong>.</p>
<p>Аналогично Python-операторам, декораторы <strong>TaskFlow</strong> удобны в случае, если вы хотите использовать эти операторы для выполнения вашего <strong>Python-callable</strong>.</p>
<p>Однако этот подход значительно сложнее — вам необходимо понимать, как работают<strong> Docker-контейнеры</strong> и <strong>Kubernetes Pod</strong>’ы, если вы хотите его использовать. Зато задачи полностью изолированы друг от друга, и вы даже не ограничены выполнением только Python-кода. Вы можете писать задачи на любом языке программирования. Кроме того, ваши зависимости полностью независимы от зависимостей <strong>Airflow</strong> (включая системные зависимости), поэтому если вашей задаче требуется принципиально иное окружение, это подходящий вариант.</p>
<p><em>Добавлено в версии 2.2:</em><br />
Начиная с версии <strong>Airflow 2.2</strong>, вы можете использовать декоратор <code>@task.docker</code> для запуска функций с помощью <strong>DockerOperator</strong>.</p>
<p><em>Добавлено в версии 2.4:</em><br />
Начиная с версии <strong>Airflow 2.2</strong>, вы можете использовать декоратор <code>@task.kubernetes</code> для запуска функций с помощью <code>KubernetesPodOperator</code>.</p>
<p>Преимущества использования этих операторов:</p>
<ul>
<li>Вы можете запускать задачи с разными наборами как <strong>Python-</strong>, так и системных зависимостей, а также задачи, написанные на совершенно другом языке программирования или даже под другую архитектуру процессора (<code>x86</code> vs. <code>arm</code>).</li>
<li>Окружение, в котором выполняются задачи, использует оптимизации и неизменяемость контейнеров. Похожие наборы зависимостей эффективно переиспользуют закешированные слои образов, поэтому окружение хорошо оптимизировано для случаев, когда у вас есть несколько похожих, но разных окружений.</li>
<li>Зависимости могут быть заранее проверены администраторами и командой безопасности, и никакой новый, неожиданный код не будет динамически добавляться. Это полезно как с точки зрения безопасности, так и стабильности.</li>
<li>Полная изоляция между задачами. Они не могут влиять друг на друга иначе, чем через стандартные механизмы <strong>Airflow XCom</strong>.</li>
</ul>
<p><strong>Недостатки</strong>:</p>
<ul>
<li>Существует накладной расход на запуск задач. Обычно он меньше, чем при динамическом создании <code>virtualenv</code>, но всё равно заметен (особенно для <code>KubernetesPodOperator</code>).</li>
<li>В случае использования декораторов <strong>TaskFlow</strong> весь вызываемый метод должен быть сериализован и передан в <strong>Docker-контейнер</strong> или <strong>Kubernetes Pod</strong>, при этом существуют системные ограничения на размер метода. Сериализация, передача и последующая десериализация на удалённой стороне также добавляют накладные расходы.</li>
<li>Присутствуют накладные расходы по ресурсам, связанные с необходимостью нескольких процессов. При использовании этих операторов для выполнения задач требуется как минимум два процесса: один процесс (в <strong>Docker-контейнере</strong> или <strong>Kubernetes Pod</strong>), выполняющий задачу, и процесс-наблюдатель в воркере <strong>Airflow</strong>, который отправляет задание в <strong>Docker/Kubernetes</strong> и отслеживает его выполнение.</li>
<li><strong>Контейнерные образы</strong> должны быть подготовлены заранее. Обычно это означает, что вы не можете изменять их «на лету». Добавление системных зависимостей, изменение или обновление <strong>Python-зависимостей</strong> требует пересборки и публикации образа (как правило, в приватном реестре). Время итераций при работе с новыми зависимостями обычно больше и требует от разработчика сборки и использования собственных образов во время разработки. Наличие корректного пайплайна деплоя здесь критически важно для надёжного сопровождения системы.</li>
<li>Если вы хотите запускать <strong>Python-callable</strong> через <strong>декораторы</strong>, он должен быть сериализуемым. Также в этом случае все зависимости, отсутствующие в окружении <strong>Airflow</strong>, должны импортироваться локально внутри используемого <strong>callable</strong>, а код верхнего уровня <strong>Dag</strong> не должен импортировать или использовать эти библиотеки.</li>
<li>Вам необходимо глубже понимать, как работают <strong>Docker-контейнеры</strong> или <strong>Kubernetes</strong>. Абстракции, предоставляемые этими технологиями, являются «протекающими», поэтому для написания Dag’ов с использованием этих операторов нужно разбираться в ресурсах, сетях, контейнерах и других аспектах.</li>
</ul>
<p>Подробные примеры использования <code>airflow.providers.docker.operators.docker.DockerOperator</code> приведены в разделе <strong>TaskFlow Docker example</strong>, а <code>airflow.providers.cncf.kubernetes.operators.pod.KubernetesPodOperator</code> — в разделе <strong>TaskFlow Kubernetes example</strong>.</p>
<h3><strong>Использование нескольких Docker-образов и очередей Celery</strong></h3>
<p>Существует возможность (хотя она требует глубокого понимания деплоя <strong>Airflow</strong>) запускать задачи Airflow с использованием нескольких независимых <strong>Docker-образов</strong>. Это можно реализовать путём назначения разных задач разным <strong>очередям (Queues)</strong> и настройки <strong>Celery-воркеров</strong> на использование разных образов для разных очередей. Однако такой подход (по крайней мере на данный момент) требует большого объёма ручной конфигурации деплоя и глубоких знаний того, как работают <strong>Airflow, Celery</strong> и <strong>Kubernetes</strong>. Кроме того, он вносит существенные накладные расходы при выполнении задач — снижается возможность переиспользования ресурсов, а также становится значительно сложнее точно настраивать стоимость потребляемых ресурсов без негативного влияния на производительность и стабильность.</p>
<p>Одним из возможных способов сделать этот подход более полезным является реализация <strong>AIP-46 (Runtime isolation for Airflow tasks and Dag parsing)</strong> и завершение <strong>AIP-43 (Dag Processor Separation)</strong>. До реализации этих инициатив преимуществ у данного подхода крайне мало, и он не рекомендуется к использованию.</p>
<p>Однако после реализации этих <strong>AIP</strong> откроется возможность более <strong>мультиарендного (multi-tenant) подхода</strong>, при котором несколько команд смогут иметь полностью изолированные наборы зависимостей, используемые на протяжении всего <strong>жизненного цикла Dag</strong> — от парсинга до выполнения.</p>
<h1>Создание пользовательского оператора (custom Operator)</h1>
<p>Airflow позволяет создавать новые операторы в соответствии с требованиями вас или вашей команды. Такая расширяемость — одна из ключевых возможностей, делающих Apache Airflow мощным инструментом.</p>
<p>Вы можете создать любой оператор, унаследовавшись от публичного базового класса SDK — BaseOperator.</p>
<p><strong>В производном классе необходимо переопределить два метода:</strong></p>
<ul>
<li><strong>Конструктор (__init__)</strong> — определить параметры, необходимые для оператора. Нужно указывать только аргументы, специфичные для вашего оператора. <code>default_args</code> можно задать в файле <code>Dag</code>.</li>
<li><strong>Execute</strong> — код, который будет выполнен при вызове оператора раннером. Метод принимает контекст <code>Airflow</code> в качестве параметра, который можно использовать для чтения конфигурационных значений.</li>
</ul>
<p><strong>Примечание</strong></p>
<p>При реализации пользовательских операторов не выполняйте ресурсоёмкие операции в методе <code>init</code>. Операторы создаются один раз за цикл планировщика для каждой задачи, которая их использует, и выполнение, например, запросов к базе данных может существенно замедлить планирование и привести к неэффективному использованию ресурсов.</p>
<p>Реализуем пример <code>HelloOperator</code> в новом файле <code>hello_operator.py</code>:</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import BaseOperator


class HelloOperator(BaseOperator):
    def __init__(self, name: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name

    def execute(self, context):
        message = f"Hello {self.name}"
        print(message)
        return message</pre><p><strong>Примечание</strong></p>
<p>Чтобы импорты работали корректно, файл должен находиться в директории, присутствующей в переменной окружения <code>PYTHONPATH</code>. <strong>Airflow</strong> по умолчанию добавляет директории <code>dags/</code>, <code>plugins/</code> и <code>config/</code> из домашнего каталога <strong>Airflow</strong> в <code>PYTHONPATH</code>. В нашем примере файл размещён в директории <code>custom_operator/</code>.</p>
<p>Теперь вы можете использовать созданный пользовательский оператор следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">from custom_operator.hello_operator import HelloOperator

with dag:
    hello_task = HelloOperator(task_id="sample-task", name="foo_bar")</pre><p>Вы также можете продолжать использовать папку <code>plugins</code> для хранения пользовательских операторов. Если файл <code>hello_operator.py</code> находится в директории <strong>plugins</strong>, оператор можно импортировать следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">from hello_operator import HelloOperator</pre><p>Если оператор взаимодействует с внешним сервисом (API, база данных и т. п.), рекомендуется реализовать слой взаимодействия через <strong>Hooks</strong>. Это позволит повторно использовать реализованную логику в других операторах. Такой подход обеспечивает лучшее разделение ответственности и более эффективное использование интеграции по сравнению с созданием <code>CustomServiceBaseOperator</code> для каждого внешнего сервиса.</p>
<p>Ещё один аспект — временное состояние. Если операция требует хранения состояния в памяти (например, job id, который должен использоваться в методе on_kill для отмены запроса), это состояние должно храниться в операторе, а не в hook. Таким образом, hook сервиса остаётся полностью stateless, а вся логика операции сосредоточена в одном месте — в операторе.</p>
<h2>Hooks</h2>
<p><strong>Hooks</strong> выступают интерфейсом для взаимодействия с внешними общими ресурсами в Dag. Например, нескольким задачам в <strong>Dag</strong> может потребоваться доступ к базе данных <strong>MySQL</strong>. Вместо создания отдельного подключения для каждой задачи можно получить подключение через <strong>hook</strong> и использовать его повторно.</p>
<p><strong>Hook</strong> также помогает избежать хранения параметров аутентификации подключения непосредственно в <strong>Dag</strong>.</p>
<p>Расширим предыдущий пример и получим имя из MySQL:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloDBOperator(BaseOperator):
    def __init__(self, name: str, mysql_conn_id: str, database: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name
        self.mysql_conn_id = mysql_conn_id
        self.database = database

    def execute(self, context):
        hook = MySqlHook(mysql_conn_id=self.mysql_conn_id, schema=self.database)
        sql = "select name from user"
        result = hook.get_first(sql)
        message = f"Hello {result['name']}"
        print(message)
        return message</pre><p>Когда оператор выполняет запрос через объект <strong>hook</strong>, создаётся новое подключение, если оно ещё не существует. <strong>Hook</strong> получает параметры аутентификации (например, имя пользователя и пароль) из <strong>backend</strong> Airflow и передаёт их в <code>airflow.hooks.base.BaseHook.get_connection()</code>.</p>
<p>Создавать <strong>hook</strong> следует только в методе <strong>execute</strong> или в методах, вызываемых из <strong>execute</strong>. Конструктор вызывается каждый раз при парсинге <strong>Dag</strong> (а это происходит часто), и создание <strong>hook</strong> в нём приведёт к множеству ненужных подключений к базе данных. Метод <strong>execute</strong> вызывается только во время запуска <strong>Dag</strong>.</p>
<h3>Пользовательский интерфейс</h3>
<p>Airflow позволяет разработчику управлять отображением оператора в интерфейсе Dag.</p>
<ul>
<li>Переопределите <code>ui_color</code>, чтобы изменить цвет фона оператора в <strong>UI</strong>.</li>
<li>Переопределите <code>ui_fgcolor</code>, чтобы изменить цвет текста.</li>
</ul>
<p>Переопределите <code>custom_operator_name</code>, чтобы изменить отображаемое имя (отличное от имени класса).</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    ui_color = "#ff0000"
    ui_fgcolor = "#000000"
    custom_operator_name = "Howdy"
    # ...</pre><p></p>
<h3>Шаблонизация (Templating)</h3>
<p>Вы можете использовать шаблоны <strong>Jinja</strong> для параметризации оператора. <strong>Airflow</strong> применяет шаблонизацию к полям, указанным в <code>template_fields</code>, во время рендеринга оператора.</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields: Sequence[str] = ("name",)

    def __init__(self, name: str, world: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name
        self.world = world

    def execute(self, context):
        message = f"Hello {self.world} it's {self.name}!"
        print(message)
        return message</pre><p>Использование шаблона:</p><pre class="urvanov-syntax-highlighter-plain-tag">with dag:
    hello_task = HelloOperator(
        task_id="task_id_1",
        name="{{ task_instance.task_id }}",
        world="Earth",
    )</pre><p>В этом примере <code>Jinja</code> найдёт параметр <code>name</code> и заменит <code>{{ task_instance.task_id }}</code> на <code>task_id_1</code>.</p>
<p>Параметр также может содержать имя файла, например <strong>bash-скрипта</strong> или <strong>SQL-файла</strong>. В этом случае нужно указать расширение файла в <strong>template_ext</strong>. Если поле из <strong>template_fields</strong> содержит строку, заканчивающуюся расширением из <strong>template_ext</strong>, <strong>Jinja</strong> прочитает содержимое файла и заменит шаблоны на реальные значения.</p>
<p><strong>Обратите внимание:</strong> <code>Jinja</code> подставляет значения в атрибуты оператора, а не в аргументы функции.</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields: Sequence[str] = ("guest_name",)
    template_ext = ".sql"

    def __init__(self, name: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.guest_name = name</pre><p>В этом примере <code>template_fields</code> должен быть <code>['guest_name']</code>, а не <code>['name']</code>.</p>
<p>Дополнительно вы можете указать <code>template_fields_renderers</code> — словарь, определяющий, в каком формате значение шаблонного поля будет отображаться в веб-интерфейсе. Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">class MyRequestOperator(BaseOperator):
    template_fields: Sequence[str] = ("request_body",)
    template_fields_renderers = {"request_body": "json"}

    def __init__(self, request_body: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.request_body = request_body</pre><p>В ситуации, когда <code>template_field</code> сам по себе является словарём, также можно указать путь к ключу через точку, чтобы извлекать и корректно отображать отдельные элементы. Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">class MyConfigOperator(BaseOperator):
    template_fields: Sequence[str] = ("configuration",)
    template_fields_renderers = {
        "configuration": "json",
        "configuration.query.sql": "sql",
    }

    def __init__(self, configuration: dict, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.configuration = configuration</pre><p>Использование этого шаблона:</p><pre class="urvanov-syntax-highlighter-plain-tag">with dag:
    config_task = MyConfigOperator(
        task_id="task_id_1",
        configuration={"query": {"job_id": "123", "sql": "select * from my_table"}},
    )</pre><p>В результате в <strong>UI</strong> поле <code>configuration</code> будет отображаться в формате <strong>JSON</strong>, а значение, находящееся по пути <code>configuration.query.sql</code>, будет подсвечено с использованием <strong>SQL-лексера</strong>.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path.png"><img decoding="async" class="aligncenter size-full wp-image-2868" src="https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path.png" alt="" width="1274" height="574" srcset="https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path.png 1274w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-300x135.png 300w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-1024x461.png 1024w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-768x346.png 768w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-450x203.png 450w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-780x351.png 780w" sizes="(max-width: 1274px) 100vw, 1274px" /></a></p>
<p>В настоящее время доступны следующие <strong>лексеры</strong>:</p>
<ul>
<li>bash</li>
<li>bash_command</li>
<li>doc</li>
<li>doc_json</li>
<li>doc_md</li>
<li>doc_rst</li>
<li>doc_yaml</li>
<li>doc_md</li>
<li>hql</li>
<li>html</li>
<li>jinja</li>
<li>json</li>
<li>md</li>
<li>mysql</li>
<li>postgresql</li>
<li>powershell</li>
<li>py</li>
<li>python_callable</li>
<li>rst</li>
<li>sql</li>
<li>tsql</li>
<li>yaml</li>
</ul>
<p>Если вы укажете несуществующий лексер, значение шаблонного поля будет отображено как красиво отформатированный (<strong>pretty-printed</strong>) объект.</p>
<h2>Ограничения</h2>
<p>Чтобы предотвратить неправильное использование, при определении и назначении шаблонизируемых полей в конструкторе оператора (если он определён, иначе — см. ниже) необходимо соблюдать следующие ограничения:</p>
<p><strong>1. Параметры конструктора, соответствующие шаблонным полям, должны называться точно так же, как и сами поля.</strong></p>
<p>Следующий пример некорректен, так как имя параметра конструктора не совпадает с именем шаблонного поля:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo_id) -&gt; None:  # должно быть def __init__(self, foo) -&gt; None
        self.foo = foo_id  # должно быть self.foo = foo</pre><p><strong>2. Атрибуты экземпляра, соответствующие шаблонным полям, должны быть явно присвоены из соответствующих параметров конструктора — либо напрямую, либо через вызов конструктора родительского класса (где эти поля определены как template_fields) с явной передачей параметров.</strong></p>
<p>Следующий пример некорректен, так как атрибут <code>self.foo</code> вообще не присваивается, несмотря на то, что он объявлен как шаблонное поле:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = ("foo", "bar")

    def __init__(self, foo, bar) -&gt; None:
        self.bar = bar</pre><p>Следующий пример также некорректен, так как <code>self.foo</code> в <code>MyHelloOperator</code> инициализируется неявно через <code>kwargs</code>, переданные в конструктор родителя:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo) -&gt; None:
        self.foo = foo


class MyHelloOperator(HelloOperator):
    template_fields = ("foo", "bar")

    def __init__(self, bar, **kwargs) -&gt; None:  # должно быть def __init__(self, foo, bar, **kwargs)
        super().__init__(**kwargs)  # должно быть super().__init__(foo=foo, **kwargs)
        self.bar = bar</pre><p><strong>3. Нельзя применять преобразования к параметру при его присваивании в конструкторе.</strong></p>
<p>Любые действия над значением должны выполняться в методе <code>execute()</code>.</p>
<p>Следующий пример некорректен:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo) -&gt; None:
        self.foo = foo.lower()  # должно быть только self.foo = foo</pre><p>Если оператор наследуется от базового оператора и не определяет собственный конструктор, указанные ограничения не применяются. Однако шаблонные поля должны быть корректно определены в родительском классе с соблюдением этих правил.</p>
<p>Следующий пример корректен:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo) -&gt; None:
        self.foo = foo


class MyHelloOperator(HelloOperator):
    template_fields = "foo"</pre><p>Эти ограничения проверяются <code>pre-commit</code> <strong>hook’ом</strong> с именем <code>validate-operators-init</code>.</p>
<p>Добавление шаблонных полей через наследование</p>
<p>Распространённый сценарий создания пользовательского оператора — расширение уже существующих <code>template_fields</code>. Может возникнуть ситуация, когда нужный вам оператор не объявляет определённые параметры как шаблонные, но вы хотите передавать их динамически через <strong>Jinja-выражения</strong>. Это легко реализуется через простое наследование.</p>
<p>Предположим, у вас есть ранее определённый <code>HelloOperator</code>:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields: Sequence[str] = ("name",)

    def __init__(self, name: str, world: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name
        self.world = world

    def execute(self, context):
        message = f"Hello {self.world} it's {self.name}!"
        print(message)
        return message</pre><p>Допустим, вы хотите динамически параметризовать аргумент <strong>world</strong>.</p>
<p>Поскольку <code>template_fields</code> гарантированно является <code>Sequence[str]</code> (списком или кортежем строк), можно легко создать подкласс и расширить список шаблонных полей:</p><pre class="urvanov-syntax-highlighter-plain-tag">class MyHelloOperator(HelloOperator):
    template_fields: Sequence[str] = (*HelloOperator.template_fields, "world")</pre><p>Теперь можно использовать <code>MyHelloOperator</code> следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">with dag:
    hello_task = MyHelloOperator(
        task_id="task_id_1",
        name="{{ task_instance.task_id }}",
        world="{{ var.value.my_world }}",
    )</pre><p>В этом примере аргумент <strong>world</strong> будет динамически установлен в значение переменной <strong>Airflow</strong> с именем <strong>my_world</strong> через <strong>Jinja-выражение</strong>.</p>
<h2>Определение дополнительной ссылки (Extra Link) для оператора</h2>
<p>Для своего оператора вы можете определить <strong>дополнительную ссылку (extra link)</strong>, которая будет перенаправлять пользователей во внешние системы. Например, можно добавить ссылку, ведущую на документацию или руководство по использованию оператора.</p>
<h2>Sensors</h2>
<p><strong>Airflow</strong> предоставляет специальный тип оператора — <strong>Sensor</strong>, предназначенный для регулярной проверки (<strong>polling</strong>) некоторого состояния (например, наличия файла) до тех пор, пока не будет выполнено условие успешного завершения.</p>
<p>Вы можете создать собственный сенсор, унаследовавшись от <code>airflow.sensors.base.BaseSensorOperator</code> и реализовав метод <code>poke</code>, который будет опрашивать внешнее состояние и проверять критерий успешности.</p>
<h3>Режим reschedule</h3>
<p>У сенсоров есть мощная возможность — режим <code>reschedule</code>, который позволяет задаче сенсора быть перепланированной, вместо того чтобы занимать слот воркера между проверками.</p>
<p>Это полезно, если:</p>
<ul>
<li>вы можете позволить себе более длинный интервал опроса,</li>
<li>ожидается длительное ожидание выполнения условия.</li>
</ul>
<h3>Ограничение режима reschedule</h3>
<p>Режим <code>reschedule</code> имеет важное ограничение: сенсор не может сохранять внутреннее состояние между перепланированными запусками.</p>
<p>Если ваш сенсор хранит внутреннее состояние, его следует декорировать с помощью <code>airflow.sensors.base.poke_mode_only()</code>. Это даст пользователям понять, что сенсор не подходит для использования в режиме <code>reschedule</code>.</p>
<h3>Пример сенсора с внутренним состоянием</h3>
<p>Примером сенсора, который хранит внутреннее состояние и не может использоваться в режиме reschedule, является:</p>
<p><code>airflow.providers.google.cloud.sensors.gcs.GCSUploadSessionCompleteSensor</code></p>
<p>Этот сенсор:</p>
<ul>
<li>опрашивает количество объектов по заданному префиксу (это количество является его внутренним состоянием),</li>
<li>считается успешно завершённым, если в течение определённого времени количество объектов не меняется.</li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/best-practices-airflow-3-documentation/">Best Practices &#8212; Airflow 3 Документация</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/best-practices-airflow-3-documentation/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
