Прямой ответ
Airflow — оркестратор задач с зависимостями и расписанием: cron/timer запускает «что и когда», но не «в каком порядке при провалах» (cron-предел — цепочки скриптов && не масштабируются); Airflow описывает пайплайн как DAG (граф задач: extract → transform → load → notify — вершины с зависимостями), исполняет с ретраями/таймаутами/пулами ресурсов, хранит историю прогонов (backfill «пересчитать январь» — встроен), показывает UI (граф, логи, статусы). База правильной практики: (1) DAG — код (Python-файл в dags/ — версионируется/ревьюится — структура), (2) идемпотентность задач по logical_date (прогон за дату X перезапускаемый — «перезапуск сломал данные дублями» — главная ошибка новичков: задачи пишут «в своё расписание», не «в момент запуска» — partitioned-запись/UPSERT по дате прогона — ETL-дисциплина), (3) маленькие задачи (одна задача = одна переисполняемая единица — «монолитный PythonOperator на весь ETL» — ретрай переделывает всё; разбивка по шагам даёт частичные перезапуски и читаемые логи), (4) никакой тяжёлой работы в топ-левеле DAG-файла (парсится планировщиком каждые секунды — код уровня модуля исполняется постоянно — коннекты/запросы на парсинге — антипаттерн №1; работа — внутри функций задач), (5) данные между задачами — через внешние хранилища (XCom — мелкие метаданные, не DataFrame-гигабайты — задачи могут исполняться на разных воркерах). Ретраи/таймауты — per task (retries=2, retry_delay=timedelta(minutes=5), execution_timeout=timedelta(hours=1)), алерты на failed (on_failure_callback в Slack/почту — молча падающий DAG — «отчёт просто перестал приходить»), пулы — лимит конкурентности на ресурс (не более N одновременных подключений к БД-источнику). Граница: Airflow — оркестратор батчей (минуты-часы, расписание/зависимости); событийные потоки — Kafka (не «Airflow раз в 10 секунд» — стримы); «Airflow как Celery для веб-запросов» — нецелевое (задачи — свой инструмент).
Анатомия DAG: задачи, операторы, расписание
DAG-файл: with DAG(dag_id="daily_orders_etl", schedule="0 2 * * *", start_date=..., catchup=False, default_args={...}) as dag: + задачи + стрелки зависимостей (extract >> transform >> load >> notify — или task dependencies как контекст-менеджеры/группы — TaskGroup для читаемости больших графов). Операторы — типы задач: PythonOperator (своя функция — база; логика в отдельном модуле, не inline — тестируемость), BashOperator (скрипты/CLI — dbt-прогоны часто так), специализированные провайдеры (PostgresOperator/SqlSensor/KubernetesPodOperator/S3-операторы — пакеты apache-airflow-providers-* — экосистема огромна; KubernetesPodOperator — задача в своём контейнере — изоляция зависимостей пайплайна от Airflow-окружения — рекомендуемый паттерн для тяжёлых/капризных зависимостей); сенсоры — задачи-ожидания (FileSensor/SqlSensor/ExternalTaskSensor — «ждём появления данных/завершения другого DAG» — с режимом reschedule, не poke-блокировкой воркера — «сенсор занял слот на час» — классика). Schedule и logical_date: расписание задаёт «за какой период прогон» (logical_date — начало интервала — задача пишет данные за logical_date, не за now() — идемпотентный backfill держится на этом); catchup=False по умолчанию для «только актуальные прогоны» (пропущенные не догонять — иначе после недели простоя — шквал догоняющих прогонов; catchup=True — осознанно для backfill-сценариев); data_interval — интервал данных прогона (задача читает data_interval_start/end — параметризация по периоду, не datetime.now() — now() в задачах — невоспроизводимость backfill). XCom — передача мелких результатов между задачами (id загруженной партиции, число строк — метаданные; DataFrame/файлы — через S3/БД с передачей ссылки — объёмы XCom хранятся в метаданной БД — раздувание — антипаттерн).
Надёжность: ретраи, идемпотентность, наблюдение
Ретраи — default_args (retries=2, retry_delay — экспонента через retry_exponential_backoff=True — дисциплина та же), retry-условия — на переходящие ошибки (Airflow ретраит любое исключение — бизнес-ошибки «данных нет» — лучше fail-fast с понятным сообщением или short-circuit); execution_timeout всегда (зависшая задача без таймаута занимает слот пула навсегда). Идемпотентность (главный контракт): задача, запущенная дважды за один logical_date, даёт тот же результат — запись в партицию/таблицу-по-дате с перезаписью (TRUNCATE+INSERT/UPSERT/MERGE по ключу — SQL-паттерны), не «INSERT всегда» (дубли при ретрае — «в отчёте цифры ×2 после перезапуска» — самый частый прод-инцидент новичков); проверка — учебный двойной прогон на стейдже (clear task → rerun — сверка данных — обязательный тест перед продом). Пулы/конкурентность: max_active_tasks на DAG, pools (slot-ресурсы: «не более 3 одновременных выгрузок из CRM» — внешний сервис не кладётся параллелизмом), worker-параллелизм executor'а (Celery/Kubernetes executor — архитектура знакомая). Наблюдение: UI (граф/логи/Gantt — разбор «где завис» — минуты), метрики StatsD→Prometheus (инструментация: длительности задач, failed-count, «DAG не стартовал по расписанию» — тихие пропуски страшнее падений), алерты on_failure_callback + sla_miss_callback (SLA «задача должна успеть к 6:00» — отчёт ждёт бизнес); логи задач — в централизованное хранилище (remote logging S3 — логи). Backfill — плановая операция (airflow dags backfill -s -e — диапазон дат; идемпотентные задачи делают его безопасным; неидемпотентные — катастрофа — второй аргумент за идемпотентность).
Эксплуатация и границы
Инфраструктура: метаданные — Postgres (не SQLite в проде — БД обычная), scheduler + webserver + executor-воркеры (KubernetesExecutor — задачи-поды — изоляция и масштабирование; CeleryExecutor — воркеры-машины — проще), dags — из git (sync-sidecar/CI-доставка — CI деплоит DAG-и как код); локальная разработка — docker-compose (официальный — локальный стек) или stand-alone-команда; секреты — Connections/Variables (в метаданной БД/секрет-бэкенде Vault — секреты), не в коде DAG; тестирование DAG — dag-файл импортируется без ошибок (unit-тест «все DAG парсятся» — ловит синтаксис до планировщика), логика задач — отдельные функции с моками (pytest), интеграция — прогоны на стейдже. Когда Airflow — перебор: 3 скрипта раз в день — cron/systemd достаточно (Airflow — инфраструктура: БД+scheduler+webserver — стоимость реальна); стриминг — Kafka/Flink; «оркестрация внутри запроса» — Celery; альтернативы при желании меньшей инфраструктуры — Dagster/Prefect (те же концепты DAG/идемпотентности — переносимы); выбор Airflow оправдан: много батч-пайплайнов, зависимости/расписания/backfill нужны по-настоящему, команда растёт (UI/история/алерты окупаются с масштабом).
Практический сценарий: от cron-спагетти к Airflow
Исходник: 9 cron-скриптов nightly-ETL (выгрузки из 4 источников → трансформации → отчёты в Postgres/Excel): зависимости — «sleep 900, чтобы предыдущий успел», провалы — молчаливые (отчёт не пришёл — заметили через 2 дня), пересчёт «за прошлую неделю» — ручные запуски с правкой дат в коде, дубли после перезапусков. Миграция: (1) логика скриптов — в функции модулей (import-чистые — тестируемы; BashOperator там, где скрипт проще не переписывать); (2) DAG per бизнес-поток (orders_daily: extract×4 (пул crm=2 — внешний CRM не более 2 параллельных выгрузок) → transform → load → notify; TaskGroup по источникам — граф читаем); (3) идемпотентность: load пишет в таблицу-партицию по logical_date (TRUNCATE+COPY — двойной прогон проверен — данные не дублируются); extract — «файл за период» в S3 по пути с датой прогона (перезапуск — перезапись своего пути); (4) расписание 02:00, catchup=False, execution_timeout per task, retries=2 exponential; on_failure → Slack-канал #data-alerts + sla 06:00 (бизнес видит отчёт к утру — SLA — контракт); (5) backfill-репетиция: пересчёт 3 недель истории одной командой — прошёл чисто (благодаря идемпотентности — раньше это было «два дня ручного ада»). Итоги за квартал: среднее время nightly — 47 мин (было ~2.5 ч со sleep-запасами), пропусков незамеченных — ноль (алерты срабатывали 4 раза — 2 сетевых сбоя источника (ретраи вылечили сами), 1 баг трансформации, 1 — дедлайн SLA при росте данных — добавлен воркер), пересчёты — рутина; cron остался только там, где одна самостоятельная задача (планировщики не вымерли — каждый инструмент в своей нише).
FAQ
Почему мой DAG-файл «выполняется» постоянно, хотя задача не запущена? Планировщик парсит DAG-файлы циклически (секунды) для построения графов — весь код верхнего уровня исполняется каждый парсинг; отсюда правила: коннекты/запросы/тяжёлые импорты — внутри функций задач, не в теле модуля; топ-левел — декларация DAG/задач (быстрая и без побочных эффектов); проверка — «сколько секунд парсится файл» в UI (dag_processing-метрики) — тяжёлый парсинг тормозит весь планировщик.
Airflow против Dagster/Prefect? Концепты общие (DAG/задачи/ретраи/backfill); Airflow — зрелейшая экосистема (провайдеры/UI/кадры — «знают все»), тяжелее инфраструктурно; Dagster — asset-центричность (данные как первоклассные объекты, типизация, локальная разработка легче — растущий фаворит data-команд); Prefect — «Python-native» лёгкость (меньше ceremony, динамические потоки); новый проект с data-фокусом — оценить Dagster честно; существующая экосистема/команда/провайдеры — Airflow без миграционной боли не менять (миграция оркестратора — месяцы).
Нужен ли Kubernetes для Airflow? Нет: CeleryExecutor на 2–3 машинах закрывает десятки DAG; KubernetesExecutor оправдан при: капризных зависимостях задач (каждая — свой образ — KubernetesPodOperator-паттерн), пиковых нагрузках (поды масштабируются, не простаивают), уже существующем K8s (кластер есть — исполнитель бесплатен); «Airflow в K8s потому что модно» без этих причин — операционная стоимость на пустом месте (Helm-чарт Airflow — заметная сложность — Helm).
Как передавать большие данные между задачами? Не через XCom (метаданная БД не для гигабайтов — раздувание и тормоза планировщика); паттерн: задача пишет результат во внешнее хранилище (S3-путь/таблица БД/parquet-файл — форматы), в XCom — ссылка/партиция/число строк; следующая задача читает по ссылке; плюс идемпотентности — путь детерминирован от logical_date (перезапуск перезаписывает свой артефакт — не плодит версии); «задачи в одном DAG должны быть независимы по состоянию» — правило, делающее частичные перезапуски безопасными.
Источники: документация Apache Airflow (core concepts, best practices, tasks) — ссылки в шапке.