Бас Харенслак, Джулиан Де Руйтер - Apache Airflow и Конвейеры Обработки Данных-ДМК Пресс (2021)
Бас Харенслак, Джулиан Де Руйтер - Apache Airflow и Конвейеры Обработки Данных-ДМК Пресс (2021)
Бас Харенслак, Джулиан Де Руйтер - Apache Airflow и Конвейеры Обработки Данных-ДМК Пресс (2021)
Джулиан де Руйтер
Apache Airflow
и конвейеры обработки данных
Data Pipelines
with Apache Airflow
SECOND EDITION
BAS HARENSLAK
and J U L I A N D E R U I T E R
Apache Airflow
и конвейеры
обработки данных
Б АС Х А Р Е Н СЛ А К
Д Ж УЛ И А Н Д Е РУ Й Т Е Р
Москва, 2022
УДК 004.4
ББК 32.372
Х20
ISBN 978-5-97060-970-5
УДК 004.4
ББК 32.372
Предисловие ...................................................................................................... 14
Благодарности ................................................................................................. 16
О книге ............................................................................................................... 18
Об авторах ....................................................................................................... 23
Об иллюстрации на обложке ........................................................................ 24
9 Тестирование ........................................................................................222
9.1 Приступаем к тестированию ...........................................................223
9.1.1 Тест на благонадежность ОАГ ..................................................223
9.1.2 Настройка конвейера непрерывной интеграции и доставки ...230
9.1.3 Пишем модульные тесты ..........................................................232
9.1.4 Структура проекта Pytest ........................................................233
9.1.5 Тестирование с файлами на диске .............................................238
9.2 Работа с ОАГ и контекстом задачи в тестах .................................241
9.2.1 Работа с внешними системами ................................................246
9.3 Использование тестов для разработки .........................................254
9.3.1 Тестирование полных ОАГ .........................................................257
9.4 Эмулируйте промышленное окружение с помощью Whirl .....257
9.5 Создание окружений .........................................................................258
Резюме .............................................................................................................258
Бас Харенслак
Я хотел бы поблагодарить своих друзей и семью за их терпение и под-
держку в течение этого приключения, длившегося полтора года, кото-
рое превратилось из второстепенного проекта в бесчисленное коли-
чество дней, ночей и выходных. Стефани, спасибо за то, что все время
терпела меня, пока я работал за компьютером. Мириам, Герд и Лотте,
Благодарности 17
спасибо за то, что терпели меня и верили в меня, пока я писал эту
книгу. Я также хотел бы поблагодарить команду GoDataDriven за их
поддержку и стремление всегда учиться и совершенствоваться. Пять
лет назад я и представить себе не мог, когда начал работать, что стану
автором книги.
Джулиан де Руйтер
Прежде всего я хотел бы поблагодарить свою жену Анн Полин и сына
Декстера за их бесконечное терпение в течение многих часов, кото-
рые я потратил на то, чтобы «еще немного поработать» над книгой.
Эта книга была бы невозможна без их непоколебимой поддержки.
Также хотел бы поблагодарить нашу семью и друзей за их поддержку
и доверие. Наконец, хочу сказать спасибо нашим коллегам из GoDa-
taDriven за их советы и поддержку, от которых я также многому на-
учился за последние годы.
О книге
Структура книги
Книга состоит из четырех разделов, которые охватывают 18 глав.
Часть I посвящена основам Airflow. В ней объясняется, что такое
Airflow, и изложены его основные концепции:
в главе 1 обсуждается концепция рабочих процессов / конвейе-
О коде
Весь исходный код в листингах или тексте набран моноширинным
шрифтом, чтобы отделить его от обычного текста. Иногда также ис-
пользуется жирный шрифт, чтобы выделить код, который изменился по
сравнению с предыдущими шагами в главе, например когда к сущест-
вующей строке кода добавляется новая функция.
Во многих случаях оригинальный исходный код был переформа-
тирован; мы добавили разрывы строк и переработали отступы, что-
бы уместить их по ширине книжных страниц. В редких случаях, когда
этого оказалось недостаточно, в листинги были добавлены символы
продолжения строки (➥). Кроме того, комментарии в исходном коде
часто удаляются из листингов, когда описание кода приводится в тек-
сте. Некоторые листинги сопровождают аннотации, выделяющие
важные понятия.
Ссылки на элементы в коде, сценарии или определенные классы,
переменные и значения Airflow часто выделяются курсивом, чтобы их
было легче отличить от окружающего текста.
Исходный код всех примеров и инструкции по их запуску с по-
мощью Docker и Docker Compose доступны в нашем репозитории
GitHub (https://github.com/BasPH/data-pipelines-with-apache-airflow),
а также на сайте издательства «ДМК Пресс» www.dmkpress.com на
странице с описанием соответствующей книги.
Отзывы и пожелания
Мы всегда рады отзывам наших читателей. Расскажите нам, что вы
думаете об этой книге, – что понравилось или, может быть, не по-
нравилось. Отзывы важны для нас, чтобы выпускать книги, которые
будут для вас максимально полезны.
Вы можете написать отзыв на нашем сайте www.dmkpress.com,
зайдя на страницу книги и оставив комментарий в разделе «Отзы-
вы и рецензии». Также можно послать письмо главному редактору
по адресу [email protected]; при этом укажите название книги
в теме письма.
22 О книге
Список опечаток
Хотя мы приняли все возможные меры для того, чтобы обеспечить
высокое качество наших текстов, ошибки все равно случаются. Если
вы найдете ошибку в одной из наших книг, мы будем очень благодар-
ны, если вы сообщите о ней главному редактору по адресу dmkpress@
gmail.com. Сделав это, вы избавите других читателей от недопонима-
ния и поможете нам улучшить последующие издания этой книги.
Извлечение
и очистка
данных
API погоды Панель управления
Легенда
Конечное состояние
Легенда
Неудовлетворенная
Открытая задача зависимость
Выполненная Удовлетворенная
задача зависимость
Рис. 1.4 Использование структуры ОАГ для выполнения задач в конвейере обработки
данных в правильном порядке: здесь показано, как меняется состояние каждой задачи
в каждой итерации алгоритма, что приводит к завершению выполнения конвейера
(конечное состояние)
Извлечение
и очистка
API погоды данных
Данные
о продажах
ответствии с требованиями.
2 Подготовьте данные о погоде, выполнив следующие действия:
получите данные прогноза погоды из API;
ветствии с требованиями.
3 Объедините наборы данных о продажах и погоде, чтобы создать
объединенный набор данных, который можно использовать
в качестве входных данных для создания модели машинного
обучения.
Знакомство с конвейерами обработки данных 33
масштабируемость
Пользовательский
Горизонтальная
Планирование
для установки
заполнение)
Бэкфиллинг
интерфейсb
Платформа
Написан на
(обратное
Способ
определения
рабочих
Название Созданa процессов
Airflow Airbnb Python Python Есть Есть Есть Любая Есть
Argo Applatix YAML Go Третья Есть Kubernetes Есть
сторонаc
Azkaban LinkedIn YAML Java Есть Нет Есть Любая
Conductor Netflix JSON Java Нет Есть Любая Есть
Luigi Spotify Python Python Нет Есть Есть Любая Есть
Make Собственный C Нет Нет Нет Любая Нет
предметно-
ориентированный
язык
Metaflow Netflix Python Python Нет Нет Любая Есть
Nifi NSA Пользовательский Java Есть Нет Есть Любая Есть
интерфейс
Oozie XML Java Есть Есть Есть Hadoop Есть
a Некоторые инструменты изначально были созданы (бывшими) сотрудниками ком-
пании; однако все они имеют открытый исходный код и не представлены одной от-
дельной компанией.
b Качество и возможности пользовательских интерфейсов сильно различаются.
c https://github.com/bitphy/argo-cron.
Сохранение Выполнение
сериализованных задач
ОАГ
Планировщик Планирование
Airflow задач
Очередь
Чтение
ОАГ
1
В англоязычных источниках также встречается термин worker process (ра-
бочий процесс), который, по сути, означает то же самое. Чтобы избежать
путаницы со словом workflow (рабочий процесс), в тексте книги использу-
ется слово «воркер». – Прим. перев.
38 Глава 1 Знакомство с Apache Airflow
3. Воркеры Airflow
2. Планировщик Airflow осуществляет его анализ выполняют задачи,
и планирует задачи в соответствии с расписанием, запускаемые по
1. Пользователь учитывая зависимости между задачами расписанию
записывает рабочий Планировщик Airflow Воркер
процесс в виде ОАГ Airlfow
Чтение ОАГ из файлов Если с расписанием
(задачи, зависимости все в порядке, Выполнение
+ интервал планируем задачи задачи
планирования)
Файл ОАГ Очередь
Пользователь (Python) выполнения
Для каждой задачи,
Ждем X секунд запускаемой по Извлечение
расписанию результатов
4. Пользователь отслеживает Проверяем задачи
выполнение + результаты зависимости
задач с помощью задач
веб-интерфейса Если зависимости
задачи удовлетворены
Добавляем
задачу
в очередь
на выполнение
Сохранение сериализованных ОАГ
Ваше имя
пользователя +
пароль
Рис. 1.10 Страница входа в веб-интерфейс Airflow. В примерах кода к этой книге
пользователю по умолчанию «admin» предоставляется пароль «admin»
Все запуски
одной задачи
Состояние
отдельной
задачи
Один запуск
рабочего процесса
1
Если сейчас это звучит для вас немного абстрактно, не волнуйтесь, мы под-
робно расскажем об этих концепциях позже.
42 Глава 1 Знакомство с Apache Airflow
Резюме
Конвейеры обработки данных могут быть представлены в виде
ОАГ, которые четко определяют задачи и их зависимости. Эти гра-
фы можно выполнять, используя преимущества параллелизма,
присущего структуре зависимостей.
Несмотря на то что на протяжении многих лет для выполнения
Анатомия ОАГ
Система
Библиотека Launch Интернет уведомлений
Извлекаем
следующие Сохраняем Сохраняем
запуски запуски фотографии
ракет Извлекаем ракет Отправляем
фотографии уведомление
ракет
Система
Библиотека Launch уведомлений
Интернет
Извлекаем
следующие Сохраняем
запуски ракет Сохраняем
запуски Извлекаем фотографии
ракет Отправляем
фотографии уведомление
ракет
import airflow
import requests
import requests.exceptions as requests_exceptions
from airflow import DAG
from airflow.operators.bash import BashOperator Создаем экземпляр объекта
from airflow.operators.python import PythonOperator ОАГ; это отправная точка
любого рабочего процесса
dag = DAG(
Имя ОАГ dag_id="download_rocket_launches",
start_date=airflow.utils.dates.days_ago(14), Дата, когда ОАГ должен
schedule_interval=None, впервые быть запущен
Интервал, с которым
)
должен запускаться ОАГ
download_launches = BashOperator(
task_id="download_launches", Применяем Bash,
Имя bash_command="curl o /tmp/launches.json L чтобы загрузить ответ
задачи 'https://ll.thespacedevs.com/2.0.0/launch/upcoming'", в виде URL-адреса
dag=dag, с помощью curl
)
Функция Python проанализирует ответ
и загрузит все изображения ракет
def _get_pictures():
# Убеждаемся, что каталог существует
pathlib.Path("/tmp/images").mkdir(parents=True, exist_ok=True)
notify = BashOperator(
task_id="notify",
bash_command='echo "There are now $(ls /tmp/images/ | wc l) images."',
dag=dag,
) Задаем порядок
выполнения задач
download_launches >> get_pictures >> notify
ОАГ
Задача Задача Задача
Оператор Оператор Оператор
def _get_pictures():
Вызываемый PythonOperator
# do work here ...
get_pictures = PythonOperator(
task_id="get_pictures",
python_callable =_get_pictures, PythonOperator
dag=dag
)
Conda: https://docs.conda.io/en/latest/;
virtualenv: https://virtualenv.pypa.io/en/latest/.
Это указывает на то, что urllib3 (т. е. HTTP-клиент для Python) пы-
тается установить соединение, но не может этого сделать. Возмож-
но, дело в правиле брандмауэра, блокирующем соединение, или от-
сутствии подключения к интернету. Предполагая, что мы устранили
проблему (например, подключили интернет-кабель), перезапустим
задачу.
Обработка неудачных задач 65
Резюме
Рабочие процессы в Airflow представлены в виде ОАГ.
Операторы представляют собой одну единицу работы.
Airflow содержит набор операторов как для универсальной, так
данных;
загрузке и повторной обработке наборов архивных данных
import pandas as pd
from airflow import DAG
from airflow.operators.bash import BashOperator
from airflow.operators.python import PythonOperator
dag = DAG(
dag_id="01_unscheduled", Определяем дату запуска ОАГ
start_date=dt.datetime(2019, 1, 1),
schedule_interval=None,
) Указываем, что это версия ОАГ
fetch_events = BashOperator( без планирования
task_id="fetch_events",
bash_command=(
"mkdir -p /data && "
Извлекаем и сохраняем
"curl -o /data/events.json "
события из API
"https://localhost:5000/events"
),
dag=dag,
)
Загружаем события
def _calculate_stats(input_path, output_path):
и рассчитываем
""Рассчитывает статистику по событиям""
необходимую статистику
events = pd.read_json(input_path)
stats = events.groupby(["date", "user"]).size().reset_index()
Path(output_path).parent.mkdir(exist_ok=True)
stats.to_csv(output_path, index=False) Убеждаемся, что выходной
каталог существует,
calculate_stats = PythonOperator( и пишем результаты в файл
task_id="calculate_stats", с расширением CSV
python_callable=_calculate_stats,
op_kwargs={
"input_path": "/data/events.json",
"output_path": "/data/stats.csv",
},
dag=dag,
)
Теперь у нас есть базовый ОАГ, но нам все еще нужно убедиться, что
Airflow регулярно запускает его. Давайте перейдем к планированию,
чтобы у нас были ежедневные обновления!
Дата Дата
начала окончания
2019-01-01 2019-01-02 2019-01-03 2019-01-04 2019-01-04 2019-01-05
00:00 00:00 00:00 00:00 00:00 00:00
пятницу в полночь;
0 0 * * MON-FRI = запускать каждый будний день в полночь;
1
https://crontab.guru переводит выражения cron на понятный человеку
язык.
Запуск через равные промежутки времени 73
Это приведет к тому, что наш ОАГ будет запускаться каждые три
дня после даты начала (4, 7, 10 и т. д. января 2019 г.). Конечно, вы так-
же можете использовать данный подход для запуска ОАГ каждые
10 минут (используя timedelta(minutes=10)) или каждые два часа
(timedelta(hours=2)).
События
Извлечение Агрегирование
День 1 events/day1.json stats/day1.csv
Извлечение Агрегирование
День 2 events/day2.json stats/day2.csv
День 3
curl O http://localhost:5000/events?start_date=20190101&end_date=20190102
Текущий
интервал
Дата начала Будущие
выполнения
2019-01-01 2019-01-02 2019-01-03 2019-01-04
00:00 00:00 00:00 00:00
Сейчас
Эту более короткую версию легче читать. Однако для более слож-
ных форматов даты (или времени и даты) вам, вероятно, все равно
придется использовать более гибкий метод strftime.
calculate_stats = PythonOperator(
task_id="calculate_stats", Передаем значения, которые
python_callable=_calculate_stats, мы хотим использовать
templates_dict={ в качестве шаблона
"input_path": "/data/events/{{ds}}.json",
"output_path": "/data/stats/{{ds}}.csv",
},
dag=dag,
)
1
Для Airflow версии 1.10.x необходимо передать дополнительный аргумент
provide_context=True в PythonOperator; в противном случае функция _cal
culate_stats не получит контекстные значения.
Сейчас
Запуск
для этого явного
интервала
Планирование
на основе интервала
Угадать, где
начинается
и заканчивается
интервал
Планирование
на основе времени ? ?
Catchup = false
Airflow начинает обработку
с текущего интервала
3.6.1 Атомарность
Термин атомарность часто используется в системах баз данных, где
атомарная транзакция считается неделимой и несводимой серией
операций с базой данных: либо происходит все, либо не происходит
ничего. Так же и в Airflow: задачи должны быть определены таким об-
разом, чтобы они были успешными и давали надлежащий результат,
либо терпели неудачу, не влияя на состояние системы (рис. 3.9).
Строка 1 Строка 1
Строка 2 Строка 2
Строка 3 Строка 3
… неудача … … неудача …
Рис. 3.9 Атомарность гарантирует, что завершится либо все, либо ничего. Вся работа
выполняется целиком, и, как следствие, в дальнейшем можно избежать получения
неверных результатов
events = pd.read_json(input_path)
stats = events.groupby(["date", "user"]).size().reset_index()
stats.to_csv(output_path, index=False)
email_stats(stats, email="[email protected]")
В ходе отправки электронного письма после записи в файл CSV создаются
две единицы работы в одной функции, что нарушает атомарность задачи
К сожалению, недостаток такого подхода состоит в том, что задача
больше не является атомарной. Понимаете, почему? Если нет, то по-
думайте, что произойдет, если наша функция _send_stats завершится
неудачно (что обязательно произойдет, если наш почтовый сервер не-
надежен). В этом случае мы уже запишем статистику в выходной файл
в output_path, создавая впечатление, что задача выполнена успешно,
даже если она закончилась неудачей.
Чтобы реализовать эту функциональность атомарно, можно было
бы просто выделить функцию отправки электронной почты в отдель-
ную задачу.
3.6.2 Идемпотентность
Еще одно важное свойство, которое следует учитывать при написании
задач Airflow, – это идемпотентность. Задачи называются идемпо-
тентными, если вызов одной и той же задачи несколько раз с одними
и теми же входными данными не имеет дополнительного эффекта.
Это означает, что повторный запуск задачи без изменения входных
данных не должен изменять общий результат.
Например, рассмотрим нашу последнюю реализацию задачи
fetch_events, которая извлекает результаты за один день и записыва-
ет их в секционированный набор данных.
Рис. 3.10 Идемпотентная задача дает один и тот же результат, независимо от того, сколько
раз вы ее запускаете. Идемпотентность обеспечивает последовательность и способность
справляться со сбоями
Резюме
ОАГ могут запускаться через равные промежутки времени, задав
интервал.
88 Глава 3 Планирование в Airflow
ния.
Идемпотентность обеспечивает возможность повторного выпол-
1
https://dumps.wikimedia.org/other/pageviews. Структура и технические
детали данных о просмотрах страниц «Википедии» можно найти здесь:
https://meta.wikimedia.org/wiki/Research:Page_view и https://wikitech.wiki-
media.org/wiki/Analytics/Data_Lake/Traffic/Pageviews.
Проверка данных для обработки с помощью Airflow 91
$ wget https://dumps.wikimedia.org/other/pageviews/
2019/201907/pageviews20190701010000.gz
$ gunzip pageviews20190701010000.gz
$ head pageviews20190701010000
aa Main_Page 1 0
aa Special:GlobalUsers/sysadmin 1 0
aa User_talk:Qoan 1 0
aa Wikipedia:Community_Portal 1 0
aa.d Main_Page 2 0
aa.m Main_Page 1 0
ab 1005 1 0
ab 105 2 0 Заархивированный файл содержит единственный
ab 1099 1 0 текстовый файл с тем же именем, что и у архива
ab 1150 1 0
Содержимое файла содержит
следующие элементы,
разделенные пробелами:
1) код домена;
2) заголовок страницы;
3) количество просмотров;
4) размер ответа в байтах.
Так, например, «en.m American_Bobtail 6 0» значит шесть просмотров страницы
https://en.m.wikipedia.org/wiki/American_Bobtail (представитель семейства кошачьих) за определенный час
Данные о просмотрах страниц обычно публикуются примерно через 45 минут
после окончания интервала; однако иногда выпуск может занять до 3–4 часов
1061202 en Example:
995600 en.m aa Main_Page 3 0
300753 ja.m af Ford_EcoSport 1 0
286381 de.m ab 1911 1 0
257751 de ab 2009 1 0
226334 ru
201930 ja aa
198182 fr.m af
193331 ru.m ab
171510 it.m ab
aa
ab
ab
af
1 aa
2 ab
1 af
2 ab
1 af
1 aa
Распаковываем
Скачиваем файл содержимое Извлекаем
с расширением .zip файла .zip просмотры страниц
dag = DAG(
dag_id="chapter4_stocksense_bashoperator",
start_date=airflow.utils.dates.days_ago(3),
schedule_interval="@hourly",
)
get_data = BashOperator(
task_id="get_data",
Двойные фигурные скобки
bash_command=(
обозначают переменную,
"curl o /tmp/wikipageviews.gz "
вставленную во время
"https://dumps.wikimedia.org/other/pageviews/"
выполнения
"{{ execution_date.year }}/"
"{{ execution_date.year }}"
"{{ '{:02}'.format(execution_date.month) }}/"
"pageviews{{ execution_date.year }}" Может быть указана
"{{ '{:02}'.format(execution_date.month) }}" любая переменная
"{{ '{:02}'.format(execution_date.day) }}" или выражение Python
"{{ '{:02}'.format(execution_date.hour) }}0000.gz"
),
dag=dag,
)
dag = DAG(
dag_id="chapter4_print_context",
start_date=airflow.utils.dates.days_ago(3),
schedule_interval="@daily",
)
def _print_context(**kwargs):
print(kwargs)
print_context = PythonOperator(
task_id="print_context",
python_callable=_print_context,
dag=dag,
)
import airflow
from airflow import DAG
from airflow.operators.python import PythonOperator
dag = DAG(
dag_id="stocksense",
start_date=airflow.utils.dates.days_ago(1),
schedule_interval="@hourly",
)
98 Глава 4 Создание шаблонов задач с использованием контекста Airflow
def _get_data(execution_date):
year, month, day, hour, *_ = execution_date.timetuple()
PythonOperator
url = (
принимает
"https://dumps.wikimedia.org/other/pageviews/"
функцию
f"{year}/{year}{month:0>2}/"
Python,
f"pageviews{year}{month:0>2}{day:0>2}{hour:0>2}0000.gz"
тогда как
)
BashOperator
output_path = "/tmp/wikipageviews.gz"
принимает
request.urlretrieve(url, output_path)
команду Bash
в качестве
get_data = PythonOperator( строки для
task_id="get_data", выполнения
python_callable=_get_data,
dag=dag,
)
Все переменные,
переданные в kwargs
1
В Python любой объект, реализующий __call __ (), считается вызываемым.
Контекст задачи и шаблонизатор Jinja 99
task_id="print_context",
python_callable=_print_context,
dag=dag,
)
print_context = PythonOperator(
task_id="print_context", python_callable=_print_context, dag=dag
)
# Выводит, например:
# Start: 20190713T14:00:00+00:00, end: 20190713T15:00:00+00:00
_get_data(output_path="/tmp/wikipageviews.gz")
f"pageviews{year}{month:0>2}{day:0>2}{hour:0>2}0000.gz"
)
request.urlretrieve(url, output_path)
airflow tasks render [dag id] [task id] [desired execution date]
fetch_pageviews = PythonOperator(
task_id="fetch_pageviews",
python_callable=_fetch_pageviews,
op_kwargs={
"pagenames": {
"Google",
"Amazon",
"Apple",
"Microsoft",
"Facebook",
}
},
dag=dag,
)
fetch_pageviews = PythonOperator(
task_id="fetch_pageviews",
python_callable=_fetch_pageviews,
op_kwargs={"pagenames": {"Google", "Amazon", "Apple", "Microsoft",
"Facebook"}},
dag=dag,
)
write_to_postgres = PostgresOperator(
Идентификатор учетных данных,
task_id="write_to_postgres",
используемых для подключения
postgres_conn_id="my_postgres",
sql="postgres_query.sql",
dag=dag, SQL-запрос или путь к файлу,
) содержащему SQL-запросы
"Amazon",19,"20190717 02:00:00"
"Amazon",13,"20190717 03:00:00"
"Amazon",12,"20190717 04:00:00"
"Amazon",12,"20190717 05:00:00"
"Amazon",11,"20190717 06:00:00"
"Amazon",14,"20190717 07:00:00"
"Amazon",15,"20190717 08:00:00"
"Amazon",17,"20190717 09:00:00"
Airflow
ОАГ
PostgresOperator
База postgres_conn_id="my_postgres"
метаданных
Airlfow sql="postgres_query.sql"
PostgresHook
Локальное … выполнить запрос …
хранилище
БД Postgres
Рис. 4.15 Запуск сценария SQL для базы данных Postgres включает несколько
компонентов. Укажите правильные настройки для PostgresOperator, а PostgresHook
выполнит всю работу под капотом
Резюме
Некоторые аргументы операторов можно шаблонизировать.
Создание шаблонов происходит во время выполнения.
Шаблонизация PythonOperator отличается от других операторов;
с помощью хуков.
Операторы описывают, что нужно делать; хуки определяют, как
это сделать.
5
Определение
зависимостей
между задачами
Эта глава:
показывает, как определять зависимости задач в ОАГ;
объясняет, как реализовать соединения с помощью правил
триггеров;
демонстрирует, как ставить задачи в зависимость
от определенных условий;
дает общее представление о том, как правила триггеров
Система
Библиотека Launch уведомлений
Интернет
Извлекаем
следующие Сохранить
запуски ракет Сохранить
пять запусков Извлечь фотографии
ракет Отправить
фотографии уведомление
ракет
Рис. 5.1 ОАГ из главы 2 (первоначально показанный на рис. 2.3) состоит из трех задач:
скачивание метаданных, извлечение изображений и отправка уведомления
В этом ОАГ каждая задача должна быть завершена перед тем, как
перейти к следующей, потому что результат предыдущей задачи
требуется в качестве ввода для следующей. Как уже было показано,
116 Глава 5 Определение зависимостей между задачами
2a 3a
Извлечение Очистка данных
1 данных о погоде о погоде 4 5 6
Объединение Тренировка Развертывание
Запуск наборов данных модели модели
Извлечение Очистка данных
данных о продажах
о продажах
2b 3b
Рис. 5.4 Порядок выполнения задач в ОАГ с номерами, указывающими порядок выполнения
задач. Airflow запускается с выполнения задачи start, после чего он может запускать задачи
fetch_sales и fetch_weather и задачи по очистке параллельно (на что указывает суффикс
a/b). Обратите внимание, это означает, что пути weather и sales работают независимо,
а значит, 3b может, например, начать выполнение до 2a. После выполнения обеих задач clean
остальная часть ОАГ линейно переходит к выполнению задач join, train и deployment
5.2 Ветвление
Представьте, что вы только что закончили вводить данные о прода-
жах в свой ОАГ, когда приходит ваш коллега с новостями. Судя по все-
му, руководство решило, что будет переходить на ERP-системы, а это
значит, что данные о продажах будут поступать из другого источника
(и, конечно, в другом формате) через одну-две недели. Очевидно, что
такое изменение не должно привести к прерыванию обучения нашей
модели. Более того, они бы хотели, чтобы мы поддерживали совмес-
тимость нашего потока как со старой, так и с новой системами, что-
бы мы могли продолжать использовать прошлые данные о продажах
в будущем анализе. Как бы вы подошли к решению этой проблемы?
...
clean_sales_data = PythonOperator(
task_id="clean_sales",
python_callable=_clean_sales,
)
Рис. 5.5 Возможный пример различных наборов задач между двумя ERP-системами. Если
между разными случаями много общего, возможно, вам удастся обойтись одним набором
задач и внутренним ветвлением. Однако если между двумя потоками есть много различий
(например, как показано здесь), вероятно, лучше выбрать другой подход
Ветвление 121
Рис. 5.6 Пример запуска ОАГ, который разветвляется между двумя ERP-системами
в рамках задач fetch_sales и clean_sales. Поскольку это ветвление происходит в этих
двух задачах, невозможно увидеть, какая ERP-система использовалась в данном запуске.
Это означает, что нам нужно будет проверить свой код (или, возможно, журналы), чтобы
определить это
Рис. 5.7 Поддержка двух ERP-систем с использованием ветвления внутри ОАГ, реализуя
разные наборы задач для обеих систем. Airflow может выбирать между этими двумя
ветвями, используя определенную задачу ветвления (здесь это «Выбрать ERP-систему»),
которая сообщает Airflow, какой набор нижестоящих задач выполнить
fetch_sales_new = PythonOperator(...)
clean_sales_new = PythonOperator(...)
pick_erp_system = BranchPythonOperator(
task_id="pick_erp_system",
python_callable=_pick_erp_system,
)
pick_erp_system = BranchPythonOperator(
task_id="pick_erp_system",
python_callable=_pick_erp_system,
)
Можно ожидать, что соединить две задачи clean так же просто, как
добавить зависимость между задачами clean и задачей join_datasets
(аналогично предыдущей ситуации, когда задача clean_sales была
подключена к задаче join_datasets).
лись, прежде чем сама задача может быть выполнена. Подключив обе
задачи clean к задаче join_datasets, мы создали ситуацию, когда это-
го не может произойти, поскольку выполняется только одна из задач
clean. В результате задача join_datasets так и не сможет быть выпол-
нена, и Airflow пропустит ее (рис. 5.8).
Рис. 5.9 Ветвление в ОАГ с использованием правила триггеров none_failed для задачи
join_datasets, что позволяет ей (и ее нижестоящим зависимостям) по-прежнему
выполняться
Рис. 5.10 Чтобы сделать структуру ветвления более понятной, можно добавить
дополнительную задачу соединения после ветви, которая связывает разные ветви перед
продолжением работы с остальной частью ОАГ. У этой задачи имеется дополнительное
преимущество, заключающееся в том, что вам не нужно изменять какие-либо правила
триггеров для других задач в ОАГ, поскольку вы можете задать необходимое правило для
задачи соединения. (Обратите внимание: это означает, что вам больше не нужно задавать
правило триггеров для задачи join_datasets)
join_branch = DummyOperator(
126 Глава 5 Определение зависимостей между задачами
task_id="join_erp_branch",
trigger_rule="none_failed"
)
deploy = PythonOperator(
task_id="deploy_model",
python_callable=_deploy,
)
Рис. 5.11 Пример запуска ОАГ с условием внутри задачи deploy_model. Это гарантирует,
что развертывание выполняется только для последнего запуска. Поскольку условие
проверяется внутри задачи deploy_model, исходя из этого представления, нельзя
определить, действительно ли модель была развернута
latest_only = PythonOperator(
task_id="latest_only",
python_callable=_latest_only,
dag=dag,
)
Теперь это означает, что наш ОАГ должен выглядеть примерно так,
как показано на рис. 5.12, с задачей train_model, подключенной к но-
вой задаче, и задачей deploy_model после этой новой задачи.
128 Глава 5 Определение зависимостей между задачами
def _latest_only(**context):
left_window = context["dag"].following_schedule(context["execution_date"])
Находим right_window = context["dag"].following_schedule(left_window)
границы для Проверяем, находится ли наше
нашего окна now = pendulum.utcnow() текущее время в рамках окна
выполнения if not left_window < now <= right_window:
raise AirflowSkipException("Not the most recent run!")
Рис. 5.13 Результат условия latest_only для трех запусков ОАГ. Это древовидное
представление показывает, что задача развертывания была запущена только для
самого последнего окна выполнения, поскольку задача развертывания была пропущена
при предыдущих выполнениях. Видно, что наше условие и в самом деле работает,
как и ожидалось
latest_only = LatestOnlyOperator(
task_id="latest_only",
dag=dag,
)
Рис. 5.14 Отслеживание выполнения базового ОАГ (рис. 5.4) с использованием правила
триггеров по умолчанию, all_success. (A) Airflow изначально запускает выполнение
ОАГ, выполняя единственную задачу, у которой нет предыдущих задач, которые не были
выполнены успешно: задача start. (B) После ее успешного выполнения другие задачи уже
готовы к выполнению, и Airflow переходит к ним
1
XCom – это сокращение от слова cross-communication.
134 Глава 5 Определение зависимостей между задачами
deploy_model = PythonOperator(
task_id="deploy_model",
python_callable=_deploy_model,
)
Рис. 5.16 Обзор зарегистрированных значений XCom (в разделе Admin > XComs
в веб-интерфейсе)
1
Вы можете указать иные значения для получения значений из других ОАГ
или иные даты выполнения, но мы настоятельно рекомендуем не делать
этого, если у вас нет на это очень веских причин.
136 Глава 5 Определение зависимостей между задачами
deploy_model = PythonOperator(
task_id="deploy_model",
python_callable=_deploy_model,
templates_dict={
"model_id": "{{task_instance.xcom_pull(
➥ task_ids='train_model', key='model_id')}}"
},
)
class CustomXComBackend(BaseXCom):
@staticmethod
def serialize_value(value: Any):
...
@staticmethod
def deserialize_value(result) > Any:
...
def _train_model(**context):
model_id = str(uuid.uuid4())
context["task_instance"].xcom_push(key="model_id", value=model_id)
Отправка идентификатора
def _deploy_model(**context): модели с помощью
model_id = context["task_instance"].xcom_pull( механизма XCom
task_ids="train_model", key="model_id"
)
print(f"Deploying model {model_id}")
train_model = PythonOperator(
task_id="train_model",
python_callable=_train_model, Создание задач обучения и развертывания
) с помощью оператора PythonOperator
deploy_model = PythonOperator(
task_id="deploy_model",
python_callable=_deploy_model,
)
Установка зависимостей
... между задачами
join_datasets >> train_model >> deploy_model
Рис. 5.18 Подмножество предыдущего ОАГ, содержащее задачи train и deploy, в которых
задачи и их зависимости определены с помощью Taskflow API
@task
def train_model():
model_id = str(uuid.uuid4())
return model_id
Использование Taskflow API
@task для задач и зависимостей
def deploy_model(model_id: str): Python
print(f"Deploying model {model_id}")
model_id = train_model()
deploy_model(model_id)
Резюме
Зависимости базовых задач Airflow можно использовать для опре-
деления линейных зависимостей задач и структур «один-ко-мно-
гим» и «многие-к-одному» в ОАГ.
Используя оператор BranchPythonOperator, можно встраивать ветви
ханизма XCom.
Taskflow API может помочь упростить ОАГ, интенсивно использую-
свой код в репозиторий или раздел в таблице Hive. Что угодно из вы-
шеперечисленного может стать причиной, чтобы приступить к запус-
ку рабочего процесса.
Старт рабочего
Супермаркет 1 Супермаркет 2 Супермаркет 3 Супермаркет 4 процесса
Старт рабочего
Супермаркет 1 Супермаркет 2 Супермаркет 3 Супермаркет 4 процесса
Время ожидания
Супермаркет 1 (9,5 часа)
Супермаркет 2 (7,25 часа)
Супермаркет 3 (4,75 часа)
Супермаркет 4 (1,5 часа)
wait_for_supermarket_1 = FileSensor(
task_id="wait_for_supermarket_1",
filepath="/data/supermarket1/data.csv",
)
Старт рабочего
процесса Супермаркет 1 Супермаркет 2 Супермаркет 3 Супермаркет 4
1
Настраивается аргументом poke_interval.
150 Глава 6 Запуск рабочих процессов
Рис. 6.8 Тупик сенсора: все выполняемые задачи – это сенсоры, ожидающие выполнения
условия, чего никогда не произойдет, и, таким образом, они занимают все слоты
wait_for_supermarket1 = PythonSensor(
task_id="wait_for_supermarket_1",
python_callable=_wait_for_supermarket,
op_kwargs={"supermarket_id": "supermarket1"},
mode="reschedule",
dag=dag,
)
Рис. 6.9 Сенсоры с mode="reschedule" освобождают свой слот после покинга, позволяя
запускать другие задачи
Запуск других ОАГ 155
Рис. 6.10 Разная логика выполнения между задачами для конкретного супермаркета,
и задача create_metrics указывает на потенциальное разделение на отдельные ОАГ
жете вызывать ОАГ 2 несколько раз из ОАГ 1 вместо одного ОАГ, со-
держащего несколько (дублированных) задач из ОАГ 2. Возможно ли
это или желательно, зависит от многих факторов, таких как сложность
рабочего процесса. Если, например, вы хотите иметь возможность
создавать метрики, не дожидаясь завершения рабочего процесса в со-
ответствии с его расписанием, а вместо этого запускать его вручную,
когда захотите, то имеет смысл разделить его на два отдельных ОАГ.
dag1 = DAG(
dag_id="ingest_supermarket_data",
start_date=airflow.utils.dates.days_ago(3),
schedule_interval="0 16 * * *",
)
Запуск других ОАГ 157
Рис. 6.13 ОАГ поделены на два, причем ОАГ 1 запускает ОАГ 2 с помощью
TriggerDagRunOperator. Логика в ОАГ 2 теперь определяется только один раз, что упрощает
ситуацию, показанную на рис. 6.12
нию;
backfill__, чтобы указать, что ОАГ запускается с использовани-
ем обратного заполнения;
manual__, чтобы указать, что ОАГ запускается вручную (напри-
Запускаемые
по расписанию
Запускаемые
вручную или
автоматически
Рис. 6.14 Черная рамка означает запуск по расписанию; отсутствие рамки указывает
на запуск вручную или автоматически
ОАГ 1, 2 и 3
ОАГ 1 ОАГ 2, 3 и 4
ОАГ 1, 2 и 3
ОАГ 4
1
Этот плагин Airflow визуализирует зависимости между ОАГ, сканируя все
ваши ОАГ на предмет использования TriggerDagRunOperator и ExternalTask
Sensor: https://github.com/ms32035/airflow-dag-dependencies.
Запуск других ОАГ 161
ОАГ 1, 2 и 3
ОАГ 4
Рис. 6.19 Вместо того чтобы
размещать выполнение с помощью
TriggerDagRunOperator, в некоторых
ситуациях, таких как обеспечение
завершенного состояния для ОАГ 1, 2 и 3,
нужно извлечь выполнение к ОАГ 4
с помощью ExternalTaskSensor
import airflow.utils.dates
from airflow import DAG
from airflow.operators.dummy import DummyOperator
from airflow.sensors.external_task import ExternalTaskSensor
dag1 = DAG(dag_id="ingest_supermarket_data", schedule_interval="0 16 * * *", ...)
dag2 = DAG(schedule_interval="0 16 * * *", ...)
DummyOperator(task_id="copy_to_raw", dag=dag1) >> DummyOperator(task_id="process_supermarket", dag=dag1)
wait = ExternalTaskSensor(
task_id="wait_for_process_supermarket",
external_dag_id="ingest_supermarket_data",
external_task_id="process_supermarket",
dag=dag2,
)
report = DummyOperator(task_id="report", dag=dag2)
wait >> report
dag = DAG(
dag_id="print_dag_run_conf",
start_date=airflow.utils.dates.days_ago(3),
schedule_interval=None,
)
164 Глава 6 Запуск рабочих процессов
def print_conf(**context):
print(context["dag_run"].conf)
Конфигурация, предоставленная
при запуске ОАГ, доступна
process = PythonOperator( в контексте задачи
task_id="process",
python_callable=print_conf,
dag=dag,
)
С конфигурацией
запуска ОАГ
Рис. 6.23 Упрощаем ОАГ за счет предоставления полезной нагрузки во время выполнения
H "ContentType: application/json" \
-d '{"conf": {}}'
Конечной точке требуется часть данных, даже если
{ не задана дополнительная конфигурация
"conf": {},
"dag_id": "print_dag_run_conf",
"dag_run_id": "manual__20201219T18:31:39.097040+00:00",
"end_date": null,
"execution_date": "20201219T18:31:39.097040+00:00",
"external_trigger": true,
"start_date": "20201219T18:31:39.102408+00:00",
"state": "running"
}
curl \
u admin:admin \
X POST \
"http://localhost:8080/api/v1/dags/print_dag_run_conf/dagRuns" \
H "ContentType: application/json" \
d '{"conf": {"supermarket": 1}}'
{
"conf": {
"supermarket": 1
},
"dag_id": "listing_6_08",
"dag_run_id": "manual__20201219T18:33:58.142200+00:00",
"end_date": null,
"execution_date": "20201219T18:33:58.142200+00:00",
"external_trigger": true,
"start_date": "20201219T18:33:58.153941+00:00",
"state": "running"
}
Это может быть удобно при запуске ОАГ за пределами Airflow, на-
пример из системы непрерывной интеграции и доставки.
Резюме
Сенсоры – это особый тип операторов, которые непрерывно вы-
полняют опрос, чтобы проверить, является ли заданное условие
истинным.
Airflow предоставляет набор сенсоров для различных систем / сце-
от A до B;
тестировании задач при подключении к внешним системам.
flow. Это может быть, например, Microsoft Azure Blob Storage, кластер
Apache Spark или хранилище данных Google BigQuery.
Чтобы увидеть, когда и как использовать такие операторы, в этой
главе мы займемся разработкой двух ОАГ, которые подключаются
к внешним системам и перемещают и преобразуют данные, исполь-
зуя эти системы. Мы рассмотрим различные варианты, которые есть
(и которых нет)1 у Airflow, для работы с этим вариантом и внешними
системами.
В разделе 7.1 мы разработаем модель машинного обучения для
AWS, работая с бакетами AWS S3 и AWS SageMaker, решением для
разработки и развертывания моделей машинного обучения. Далее,
в разделе 7.2, мы продемонстрируем, как перемещать данные между
различными системами, используя базу данных Postgres, содержа-
щую информацию об аренде жилья в Амстердаме с помощью сервиса
Airbnb. Данные поступают с сайта Inside Airbnb (http://insideairbnb.
com), которым управляет Airbnb, содержащего записи о списках, об-
зорах и многом другом. Раз в день мы будем скачивать последние
данные из базы данных Postgres в наш бакет AWS S3. После этого мы
запустим задание Pandas в контейнере Docker, чтобы определить ко-
лебания цен, а результат будет сохранен в S3.
1
Операторы всегда находятся в стадии разработки. Эта глава была написана
в 2020 году; обратите внимание, что на момент ее прочтения могут по-
явиться новые операторы, подходящие для вашего варианта использова-
ния, которые не были описаны здесь.
168 Глава 7 Обмен данными с внешними системами
Оператор
Airflow
Cloud SDK
S3CopyObjectOperator(
Бакет, из которого нужно копировать
task_id="...",
source_bucket_name="databucket", Имя объекта для копирования
source_bucket_key="/data/{{ ds }}.json", Бакет, в который нужно
dest_bucket_name="backupbucket", копировать
dest_bucket_key="/data/{{ ds }}-backup.json",
) Имя целевого объекта
Подключение к облачным сервисам 169
Режим offline
Обучение
модели на наборе
изображений
Ранее неизвестное
рукописное Классификация: 4
написание цифры
Режим online
Рис. 7.3 Примерный план того, как модель обучается на одном этапе и классифицирует
ранее неизвестные образцы на другом
1
Если просмотреть реализацию оператора, то можно увидеть, что внутри он
вызывает метод copy_object().
170 Глава 7 Обмен данными с внешними системами
Преобразовываем
Копируем данные данные в полезный Разворачиваем
в свой аккаунт формат Обучаем модель модель
import airflow.utils.dates
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.amazon.aws.hooks.s3 import S3Hook
➥ from airflow.providers.amazon.aws.operators.s3_copy_object import
S3CopyObjectOperator
➥ from airflow.providers.amazon.aws.operators.sagemaker_endpoint import
SageMakerEndpointOperator
➥ from airflow.providers.amazon.aws.operators.sagemaker_training import
SageMakerTrainingOperator
from sagemaker.amazon.common import write_numpy_to_dense_tensor
dag = DAG(
dag_id="chapter7_aws_handwritten_digits_classifier",
schedule_interval=None,
start_date=airflow.utils.dates.days_ago(3), S3CopyObjectOperator копирует
) объекты между двумя
местоположениями S3
download_mnist_data = S3CopyObjectOperator(
task_id="download_mnist_data",
source_bucket_name="sagemakersampledataeuwest1",
source_bucket_key="algorithms/kmeans/mnist/mnist.pkl.gz",
dest_bucket_name="[yourbucket]",
dest_bucket_key="mnist.pkl.gz",
dag=dag, Иногда желаемая функциональность
) не поддерживается ни одним
из операторов, и нужно реализовать
логику самостоятельно
def _extract_mnist_data():
172 Глава 7 Обмен данными с внешними системами
"S3DataDistributionType": "FullyReplicated",
}
},
}
],
➥ "OutputDataConfig": {"S3OutputPath": "s3://[yourbucket]/
mnistclassifieroutput"},
"ResourceConfig": {
"InstanceType": "ml.c4.xlarge",
"InstanceCount": 1,
"VolumeSizeInGB": 10,
},
➥ "RoleArn": "arn:aws:iam::297623009465:role/servicerole/
AmazonSageMakerExecutionRole20180905T153196",
"StoppingCondition": {"MaxRuntimeInSeconds": 24 * 60 * 60},
},
wait_for_completion=True, Оператор ожидает завершения
print_log=True, обучения и выводит журналы
check_interval=10, CloudWatch во время обучения
dag=dag,
)
sagemaker_deploy_model = SageMakerEndpointOperator(
task_id="sagemaker_deploy_model", SageMakerEndpointOperator развертывает
wait_for_completion=True, обученную модель, что делает ее доступной
config={ за конечной точкой HTTP
"Model": {
➥ "ModelName": "mnistclassifier{{ execution_date.strftime('%Y
%m%d%H%M%S') }}",
"PrimaryContainer": {
➥ "Image": "438346466558.dkr.ecr.euwest1.amazonaws.com/
kmeans:1",
"ModelDataUrl": (
"s3://[yourbucket]/mnistclassifieroutput/"
➥ "mnistclassifier{{ execution_date.strftime('%Y%m%d
%H%M%S') }}/"
"output/model.tar.gz"
), # Это свяжет модель и задачу по обучению
},
➥ "ExecutionRoleArn": "arn:aws:iam::297623009465:role/service
role/AmazonSageMakerExecutionRole20180905T153196",
},
"EndpointConfig": {
➥ "EndpointConfigName": "mnistclassifier{{
execution_date.strftime('%Y%m%d%H%M%S') }}",
"ProductionVariants": [
{
"InitialInstanceCount": 1,
"InstanceType": "ml.t2.medium",
"ModelName": "mnistclassifier",
"VariantName": "AllTraffic",
}
],
174 Глава 7 Обмен данными с внешними системами
},
"Endpoint": {
➥ "EndpointConfigName": "mnistclassifier{{
execution_date.strftime('%Y%m%d%H%M%S') }}",
"EndpointName": "mnistclassifier",
},
},
dag=dag,
)
export AWS_PROFILE=myaws
export AWS_DEFAULT_REGION=euwest1 Инициализируем локальную базу
export AIRFLOW_HOME=[your project dir] метаданных Airflow
airflow db init
airflow tasks test chapter7_aws_handwritten_digits_classifier
download_mnist_data 2020-01-01 Запускаем одну задачу
Рис. 7.6 После локального запуска задачи с помощью команды airflow tasks
test данные копируются в наш бакет AWS S3
1
База данных будет создана в файле airflow.db в каталоге, заданном пере-
менной AIRFLOW_HOME. Его можно открыть и проверить, например с по-
мощью DBeaver.
2
Документацию по MIME-типу application/xrecordioprotobuf можно
найти на странице https://docs.aws.amazon.com/de_de/sagemaker/latest/dg/
cdf-inference.html.
Подключение к облачным сервисам 177
extract_mnist_data = PythonOperator(
task_id="extract_mnist_data",
python_callable=_extract_mnist_data,
dag=dag,
)
Рис. 7.8 Операторы SageMaker срабатывают успешно только после успешного завершения
задания в AWS
Рис. 7.9 В меню модели SageMaker видно, что модель развернута, а конечная точка
находится в рабочем состоянии
180 Глава 7 Обмен данными с внешними системами
import numpy as np
from PIL import Image
from chalice import Chalice, Response
from sagemaker.amazon.common import numpy_to_record_serializer
app = Chalice(app_name="numberclassifier")
@app.route("/", methods=["POST"], content_types=["image/jpeg"])
def predict():
""
Предоставляем этой конечной точке изображение в формате jpeg.
1
Chalice (https://github.com/aws/chalice) – это фреймворк на языке Python,
аналогичный Flask, который используется для разработки API и автомати-
ческого создания базового шлюза API и лямбда-ресурсов в AWS.
Подключение к облачным сервисам 181
{
"predictions": [
{
"distance_to_cluster": 2284.0478515625,
"closest_cluster": 2.0
}
]
}
Рис. 7.11 Пример ввода и вывода API. В результате у вас может получиться
прекрасный пользовательский интерфейс для загрузки изображений
и отображения прогнозируемого числа
Скачивание данных
+
Запись результатов
results = MongoHook(self.mongo_conn_id).find(
mongo_collection=self.mongo_collection, Создается экземпляр
query=self.mongo_query, MongoHook, который
mongo_db=self.mongo_db используется для запроса
) данных
docs_str = self._stringify(self.transform(results))
Результаты
# Загрузка в S3 трансформируются
s3_conn.load_string(
string_data=docs_str, Вызывается метод load_string()
key=self.s3_key, для записи преобразованных
bucket_name=self.s3_bucket, результатов
replace=self.replace
)
Этот оператор, опять же, создает два хука: SSHHook (SFTP – это FTP
через SSH) и S3Hook. Однако в этом операторе промежуточный резуль-
тат записывается в NamedTemporaryFile, временное место в локальной
файловой системе экземпляра Airflow. В этой ситуации мы не сохра-
няем в памяти весь результат, но должны убедиться, что на диске до-
статочно места.
У обоих операторов есть два общих хука: один для обмена данными
с системой A и второй для системы B. Однако способ извлечения и пе-
редачи данных из системы A в систему B отличается и зависит от того,
кто реализует конкретный оператор. В конкретном случае с Postgres
курсоры базы данных могут выполнять итерацию для извлечения
и загрузки фрагментов результатов. Однако такие подробности реа-
лизации выходят за рамки этой книги. Будем проще и предположим,
что промежуточный результат соответствует границам ресурсов эк-
земпляра Airflow.
Очень минимальная реализация оператора PostgresToS3Operator
может выглядеть следующим образом.
results = postgres_hook.get_records(self._query)
s3_hook.load_string( Извлекаем записи из базы
string_data=str(results), данных PostgreSQL
bucket_name=self._s3_bucket, Загружаем записи в объект S3
key=self._s3_key,
)
1
Как указано в спецификации PEP 249.
2
Объекты в памяти с методами для операций с файлами для чтения или
записи.
Перенос данных из одной системы в другую 187
Резюме
Операторы внешних систем предоставляют функции, вызывая
клиента для данной системы.
Иногда эти операторы просто передают аргументы клиенту Python.
Одна из сильных сторон Airflow состоит в том, что его можно легко
расширить для координации заданий в различных типах систем. Мы
уже видели некоторые из этих функций в предыдущих главах, где
нам удалось выполнить задание по обучению модели в Amazon Sage-
Maker с помощью оператора S3CopyObjectOperator, но можно также
использовать Airflow (например) для выполнения заданий в кластере
ECS (Elastic Container Service) в AWS с помощью ECSOperator для вы-
полнения запросов к базе данных Postgres с PostgresOperator и делать
многое другое.
Однако в какой-то момент вам может понадобиться выполнить за-
дачу в системе, которая не поддерживается Airflow, или у вас может
быть задача, которую можно реализовать с помощью PythonOpera
tor, но для этого требуется много шаблонного кода, что не позволяет
Начнем с PythonOperator 191
1
Код API доступен в репозитории, прилагаемом к этой книге.
Начнем с PythonOperator 193
http://localhost:5000/ratings?offset=100
http://localhost:5000/ratings?limit=1000
http://localhost:5000/ratings?start_date=2019-01-01&end_date=2019-
01-02
response = session.get("http://localhost:5000/ratings")
1
API предоставляет данные только 30-дневной давности, поэтому обяза-
тельно обновите параметры start_date и end_date на более свежие даты,
чтобы получить результаты.
Начнем с PythonOperator 195
MOVIELENS_USER = os.environ["MOVIELENS_USER"]
MOVIELENS_PASSWORD = os.environ["MOVIELENS_PASSWORD"] Извлекаем имя пользователя
session = requests.Session()
session.auth = (MOVIELENS_USER, MOVIELENS_PASSWORD)
base_url = f"{MOVIELENS_SCHEMA}://{MOVIELENS_HOST}:{MOVIELENS_PORT}"
Позже это позволит нам легко изменять данные параметры при за-
пуске нашего сценария, определяя значения переменных окружения.
Теперь, когда у нас есть элементарная настройка для сеанса re-
quests, нам нужно реализовать функции, которые будут прозрачно
обрабатывать пагинацию API.
Один из способов сделать это – обернуть наш вызов session.get
в код, который проверяет ответ API и продолжает запрашивать новые
страницы, пока мы не достигнем общего количества записей.
start_date = templates_dict["start_date"]
Извлекаем end_date = templates_dict["end_date"]
шаблонные output_path = templates_dict["output_path"]
даты начала logger.info(f"Fetching ratings for {start_date} to {end_date}")
и окончания ratings = list(
и выходной _get_ratings( С помощью функции _get_ratings
путь start_date=start_date, извлекаем записи о рейтингах
end_date=end_date,
batch_size=batch_size,
)
)
logger.info(f"Fetched {len(ratings)} ratings")
fetch_ratings = PythonOperator(
task_id="fetch_ratings", Создаем задачу
python_callable=_fetch_ratings, с помощью
templates_dict={ PythonOperator
"start_date": "{{ds}}",
"end_date": "{{next_ds}}",
"output_path": "/data/python/ratings/{{ds}}.json",
},
)
output_dir = os.path.dirname(output_path)
os.makedirs(output_dir, exist_ok=True) Создаем выходной
каталог, если его
ranking.to_csv(output_path, index=True) не существует
Используем вспомогательную
Используем
функцию для ранжирования
rank_movies = PythonOperator( функцию
фильмов
task_id="rank_movies", _rank_movies
python_callable=_rank_movies, в PythonOperator
Записываем templates_dict={
ранжированные "input_path": "/data/python/ratings/{{ds}}.json",
фильмы "output_path": "/data/python/rankings/{{ds}}.csv",
в файл CSV },
) Подключаем задачи
по извлечению и ранжированию
fetch_ratings >> rank_movies
class MovielensHook(BaseHook):
…
class MovielensHook(BaseHook):
1
В Airflow 1 конструктор класса BaseHook требует передачи аргумента
source. Обычно можно просто передать source = None, так как вы не будете
нигде его использовать.
Создание собственного хука 201
...
def get_conn(self):
session = requests.Session()
session.auth = (MOVIELENS_USER, MOVIELENS_PASSWORD)
schema = MOVIELENS_SCHEMA
host = MOVIELENS_HOST
port = MOVIELENS_PORT
base_url = f"{schema}://{host}:{port}"
config = self.get_connection(self._conn_id)
base_url = f"{schema}://{host}:{port}/"
Создание собственного хука 203
if self._session is None:
Проверяем, есть config = self.get_connection(self._conn_id)
ли у нас активный ...
сеанс, прежде чем self._base_url = f"{schema}://{config.host}:{port}"
создавать его self._session = requests.Session()
...
offset = 0
total = None
while total is None or offset < total:
response = session.get(
url, params={
**params,
**{"offset": offset, "limit": batch_size}
}
)
206 Глава 8 Создание пользовательских компонентов
response.raise_for_status()
response_json = response.json()
offset += batch_size
total = response_json["total"]
1
Позже в этой главе мы покажем другой подход на базе пакетов.
Создание собственного хука 207
start_date = templates_dict["start_date"]
end_date = templates_dict["end_date"]
output_path = templates_dict["output_path"]
PythonOperator(
task_id="fetch_ratings",
python_callable=_fetch_ratings,
op_kwargs={"conn_id": "movielens"},
templates_dict={ Указываем, какое подключение
"start_date": "{{ds}}", использовать
"end_date": "{{next_ds}}",
208 Глава 8 Создание пользовательских компонентов
"output_path": "/data/custom_hook/{{ds}}.json",
},
)
with DAG(
...
default_args=default_args
) as dag:
MyCustomOperator(
...
)
Параметры
–––––
conn_id : str
ID подключения, который будет использоваться для подключения к API
Movielens.
Ожидается, что подключение будет включать в себя данные аутентификации
(логин/пароль) и хост, обслуживающий API.
output_path : str
Путь для записи полученных рейтингов.
start_date : str
(Шаблонная) дата начала, с которой начинается извлечение рейтингов
(включительно).
Ожидаемый формат – ГГГГ-ММ-ДД (соответствует форматам ds в Airflow).
end_date : str
(Шаблонная) дата окончания для извлечения рейтингов до (исключая).
Ожидаемый формат – ГГГГ-ММ-ДД (соответствует форматам ds в Airflow).
"""
Создание собственного оператора 211
@apply_defaults
def __init__(
self, conn_id, output_path, start_date, end_date, **kwargs,
):
super(MovielensFetchRatingsOperator, self).__init__(**kwargs)
self._conn_id = conn_id
self._output_path = output_path
self._start_date = start_date
self._end_date = end_date
self._conn_id = conn_id
self._output_path = output_path
Создание нестандартных сенсоров 213
self._start_date = start_date
self._end_date = end_date
fetch_ratings = MovielensFetchRatingsOperator(
task_id="fetch_ratings",
conn_id="movielens",
start_date="{{ds}}",
end_date="{{next_ds}}",
output_path="/data/custom_operator/{{ds}}.json"
)
class MyCustomSensor(BaseSensorOperator):
...
214 Глава 8 Создание пользовательских компонентов
class MovielensRatingsSensor(BaseSensorOperator):
"""
Сенсор, ожидающий, пока API Movielens получит рейтинги за определенный период
времени.
start_date : str
(Шаблонная) дата начала, с которой начинается извлечение рейтингов
(включительно).
Ожидаемый формат – ГГГГ-ММ-ДД (соответствует форматам ds в Airflow).
end_date : str
(Шаблонная) дата окончания для извлечения рейтингов до (исключая).
Ожидаемый формат – ГГГГ-ММ-ДД (соответствует форматам ds в Airflow).
"""
Создание нестандартных сенсоров 215
except StopIteration:
Если с StopIteration
self.log.info(
значит, набор
with DAG(
dag_id="04_sensor",
description="Fetches ratings with a custom sensor.",
start_date=airflow_utils.dates.days_ago(7),
schedule_interval="@daily", Сенсор, ожидающий, когда
) as dag: записи будут доступны
wait_for_ratings = MovielensRatingsSensor(
task_id="wait_for_ratings",
conn_id="movielens",
start_date="{{ds}}",
end_date="{{next_ds}}", Оператор, извлекающий
) записи после завершения
работы сенсора
fetch_ratings = MovielensFetchRatingsOperator(
task_id="fetch_ratings",
conn_id="movielens",
start_date="{{ds}}",
end_date="{{next_ds}}",
output_path="/data/custom_sensor/{{ds}}.json"
)
...
1
Более подробное обсуждение упаковки и различных подходов к ней вы-
ходит за рамки этого издания и объясняется во многих книгах по Python
и/или онлайн-статьях.
2
Технически с использованием стандарта PEP420 файл __init__.py уже не
нужен, но мы хотим, чтобы все было явным.
218 Глава 8 Создание пользовательских компонентов
Теперь, когда у нас есть базовая структура, все, что нам нужно сде-
лать, чтобы превратить это в пакет, – включить сюда файл setup.py,
сообщающий setuptools, как его установить. Базовый файл setup.py
обычно выглядит примерно так:
setuptools.setup(
Название, name="airflow_movielens",
версия version="0.1.0",
и описание description="Hooks, sensors and operators for the Movielens API.",
нашего author="Anonymous",
пакета author_email="[email protected]", Сведения об авторе (метаданные)
install_requires=requirements,
Информирует setuptools
packages=setuptools.find_packages("src"),
о наших зависимостях
package_dir={"": "src"},
url="https://github.com/examplerepo/airflow_movielens",
license="MIT license", Домашняя
) Лицензия MIT страница пакета
Сообщает setuptools, где искать файлы
Python нашего пакета
Самая важная часть этого файла – это вызов setuptools.setup, ко-
торый предоставляет setuptools подробные метаданные о нашем па-
кете. Наиболее важные поля в этом вызове:
name – определяет имя пакета (как он будет называться при уста-
новке);
version – номер версии пакета;
install_requires – список зависимостей, необходимых для пакета;
packages/package_dir – сообщает setuptools, какие пакеты следует
1
См. этот блог для получения более подробной информации о структурах
на базе src и других вариантах: https://blog.ionelmc.ro/2014/05/25/python-
packaging/#the-structure.
2
Чтобы получить полный справочник параметров, которые можно передать
в setuptools.setup, обратитесь к документации по setuptools.
Упаковка компонентов 219
Это означает, что теперь у нас есть базовый пакет Python airflow_
movielens, который можно попробовать установить в следующем раз-
деле.
Конечно, более сложные пакеты обычно включают в себя тесты, до-
кументацию и т. д. Все это мы здесь не описываем. Если вы хотите
увидеть обширную настройку упаковки в Python, то рекомендуем об-
ратиться к большому количеству шаблонов, доступных в интернете
(например, https://github.com/audreyfeldroy/cookiecutter-pypackage),
которые служат отличной отправной точкой для разработки пакетов
Python.
$ python
Python 3.7.3 | packaged by condaforge | (default, Jul 1 2019, 14:38:56)
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from airflow_movielens.hooks import MovielensHook
>>> MovielensHook
<class 'airflow_movielens.hooks.MovielensHook'>
Резюме
Можно расширить встроенную функциональность Airflow, созда-
вая собственные компоненты, которые подходят для ваших кон-
кретных случаев использования. По нашему опыту, два варианта
использования, в которых пользовательские операторы являются
особенно мощными, – это:
– запуск задач в системах, которые изначально не поддержива-
ются Airflow (например, новые облачные сервисы, базы данных
и т. д.);
– предоставление операторов, сенсоров или хуков для часто вы-
полняемых операций, чтобы члены вашей команды могли с лег-
костью реализовать их в ОАГ.
Конечно, это далеко не полный список, и может возникнуть много
9
Эта глава рассказывает:
о тестировании задач Airflow в конвейере непрерывной
интеграции и доставки;
о структурировании проекта для тестирования с pytest;
контейнеров.
Collecting pytest
................
Installing collected packages: pytest
Successfully installed pytest5.2.2
.
├── dags
│ ├── dag1.py
│ ├── dag2.py
│ └── dag3.py
└── mypackage
├── airflow
│ ├── hooks
│ │ ├── __init__.py
│ │ └── movielens_hook.py
│ ├── operators
│ │ ├── __init__.py
│ │ └── movielens_operator.py
│ └── sensors
│ ├── __init__.py
│ └── movielens_sensors.py
└── movielens
├── __init__.py Рис. 9.2 Пример структуры
└── utils.py пакета Python
.
├── dags
├── mypackage
└── tests
├── dags
│ └── test_dag_integrity.py
└── mypackage
├── airflow
│ ├── hooks
│ │ └── test_movielens_hook.py
│ ├── operators
│ │ └── test_movielens_operator.py
│ └── sensors
│ └── test_movielens_sensor.py Рис. 9.3 Структура каталога
└── movielens tests/ следует структуре,
└── test_utils.py показанной на рис. 9.2
1
Pytest называет такую структуру «Тесты вне кода приложения». Другая
поддерживаемая pytest структура – хранение тестовых файлов непосред-
ственно рядом с кодом вашего приложения, который он называет «тесты
как часть кода вашего приложения».
226 Глава 9
import pytest
from airflow.models import DAG
DAG_PATH = os.path.join(
os.path.dirname(__file__), "..", "..", "dags/**/*.py"
)
DAG_FILES = glob.glob(DAG_PATH, recursive=True)
@pytest.mark.parametrize("dag_file", DAG_FILES)
def test_dag_integrity(dag_file):
module_name, _ = os.path.splitext(dag_file)
module_path = os.path.join(DAG_PATH, dag_file)
➥ mod_spec = importlib.util.spec_from_file_location(module_name,
module_path)
module = importlib.util.module_from_spec(mod_spec)
mod_spec.loader.exec_module(module)
assert dag_objects
1
В pytest параметры обнаружения тестов можно настроить, например, для
поддержки тестовых файлов с именем check_ *.
Приступаем к тестированию 227
.
├─dags
│ ├─dag1.py
│ ├─ dag2.py
│ ├─ dag3.py
└─tests
└─dags
└─test_dag_integrity.py
@pytest.mark.parametrize("dag_file", DAG_FILES)
def test_dag_integrity(dag_file):
Запускаем тест для каждого
элемента в DAG_FILES
tests/dags/test_dag_integrity.py F
[100%]
dag_file = '..../dag_cycle.py'
@pytest.mark.parametrize("dag_file", DAG_FILES)
def test_dag_integrity(dag_file):
"" Импорт файлов ОАГ и проверка на наличие ОАГ.""
module_name, _ = os.path.splitext(dag_file)
module_path = os.path.join(DAG_PATH, dag_file)
➥ mod_spec = importlib.util.spec_from_file_location(module_name,
module_path)
module = importlib.util.module_from_spec(mod_spec)
mod_spec.loader.exec_module(module)
➥ dag_objects = [
var for var in vars(module).values() if isinstance(var, DAG)
]
assert dag_objects
tests/dags/test_dag_integrity.py:29:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.../sitepackages/airflow/models/dag.py:1427: in test_cycle
self._test_cycle_helper(visit_map, task_id)
.../sitepackages/airflow/models/dag.py:1449: in _test_cycle_helper
self._test_cycle_helper(visit_map, descendant_id)
.../sitepackages/airflow/models/dag.py:1449: in _test_cycle_helper
self._test_cycle_helper(visit_map, descendant_id)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
➥ self = <DAG: chapter8_dag_cycle>, visit_map = defaultdict(<class 'int'>,
{'t1': 1, 't2': 1, 't3': 1}), task_id = 't3'
task = self.task_dict[task_id]
for descendant_id in task.get_direct_relative_
if visit_map[descendant_id] == DagBag.CYCLE_IN_PROGRESS:
➥ msg = "Cycle detected in DAG. Faulty task: {0} to
{1}".format(
task_id, descendant_id)
> raise AirflowDagCycleException(msg)
➥ E airflow.exceptions.AirflowDagCycleException: Cycle
detected in DAG. Faulty task: t3 to t1
..../airflow/models/dag.py:1447: AirflowDagCycleException
========================== 1 failed in 0.21s ==========================
230 Глава 9 Тестирование
on: [push]
jobs:
testing:
runs-on: ubuntu18.04
steps:
– uses: actions/checkout@v1
– name: Setup Python
uses: actions/setuppython@v1
with:
python-version: 3.6.9
architecture: x64
start_date,
end_date,
min_ratings=4,
top_n=5,
**kwargs,
):
super().__init__(**kwargs)
self._conn_id = conn_id
self._start_date = start_date
self._end_date = end_date
self._min_ratings = min_ratings
self._top_n = top_n
Получаем
def execute(self, context):
необработанные
with MovielensHook(self._conn_id) as hook:
рейтинги
ratings = hook.get_ratings(
start_date=self._start_date,
end_date=self._end_date,
) Суммируем рейтинги
по идентификатору
rating_sums = defaultdict(Counter)
фильма
for rating in ratings:
➥ rating_sums[rating["movieId"]].update(count=1,
rating=rating["rating"])
Фильтруем min_ratings
и рассчитываем averages = {
средний рейтинг ➥ movie_id: (rating_counter["rating"] /
на каждый rating_counter["count"], rating_counter["count"])
идентификатор фильма for movie_id, rating_counter in rating_sums.items()
if rating_counter["count"] >= self._min_ratings
}
➥ return sorted(averages.items(), key=lambda x: x[1],
reverse=True)[: self._top_n]
Возвращаем результат, отсортированный
по среднему рейтингу и количеству рейтингов
tests/dags/chapter9/custom/test_operators.py .
1
Аргумент xcom_push=True возвращает стандартный вывод в Bash_command
в виде строки, которую мы используем в этом тесте для выборки и про-
верки Bash_command. В реальной ситуации Airflow любой объект, возвращае-
мый оператором, автоматически отправляется в XCom.
Приступаем к тестированию 235
top_n=5,
)
result = task.execute(context={})
assert len(result) == 5
tests/dags/chapter9/custom/test_operators.py F
[100%]
=============================== FAILURES ===============================
___________________ test_movielenspopularityoperator ___________________
tests/dags/chapter9/custom/test_operators.py:30: TypeError
========================== 1 failed in 0.10s ==========================
1
Если вы хотите типизировать свои аргументы, то mocker имеет тип pytest_
mock.MockFixture.
Приступаем к тестированию 237
MovielensHook,
Объект, "get_connection", Функция, к которой нужно применить патч
к которому ➥ return_value=Connection(conn_id="test", login="airflow",
нужно password="airflow"),
применить ) Возвращаемое
патч task = MovielensPopularityOperator(...) значение
1
Для двух этих утверждений существует удобный метод assert_called_
once_with().
238 Глава 9 Тестирование
1
Это объяснено в документации Python: https://docs.python.org/3/library/
unittest.mock.html#whereto-patch, а также продемонстрировано на стра-
нице https://alexmarandon.com/articles/python_mock_gotchas/.
Приступаем к тестированию 239
[
name,age,sex
{"name": "bob", "age": 41, "sex": "M"},
bob,41,M
{"name": "alice", "age": 24, "sex": "F"},
alice,24,F
{"name": "carol", "age": 60, "sex": "F"}
carol,60,F
]
input_data = [
{"name": "bob", "age": "41", "sex": "M"},
{"name": "alice", "age": "24", "sex": "F"},
{"name": "carol", "age": "60", "sex": "F"}, Сохраняем
] входной файл
with open(input_path, "w") as f:
f.write(json.dumps(input_data))
operator = JsonToCsvOperator(
task_id="test",
input_path=input_path,
output_path=output_path, Выполняем
) JsonToCsvOperator
operator.execute(context={})
print(tmp_path.as_posix())
test-of-basharenslak/pytest-20/test_json_to_csv_operator0.
Существуют и другие фикстуры, которые можно использовать,
а у фикстур pytest есть много функций, которые не демонстрируются
в этой книге. Если вы серьезно интересуетесь функциями pytest, об-
ратитесь к документации.
1
Поищите информацию о «pytest scope», если хотите узнать, как использо-
вать фикстуры в тестах.
Работа с ОАГ и контекстом задачи в тестах 241
{
"execution_date": Pendulum(...),
Создать контекст экземпляра задачи "ds": "2020-01-01",
(1) (т. е. собрать все переменные) "ds_nodash": "20200101",
...
}
Визуализировать
(3) шаблонные переменные
"today is {{ ds }}" > "today is 2020-01-01"
(4) Выполнить
operator.pre_execute()
1
В TaskInstance это _run_raw_task().
242 Глава 9 Тестирование
def __init__(
self,
conn_id,
start_date,
end_date,
output_path,
**kwargs,
):
super().__init__(**kwargs)
self._conn_id = conn_id
self._start_date = start_date
self._end_date = end_date
self._output_path = output_path
dag = DAG(
"test_dag",
default_args={
"owner": "airflow",
"start_date": datetime.datetime(2019, 1, 1),
},
schedule_interval="@daily",
)
task = MovielensDownloadOperator(
task_id="test",
conn_id="testconn",
start_date="{{ prev_ds }}",
end_date="{{ ds }}",
output_path=str(tmp_path / "{{ ds }}.json"),
dag=dag,
)
task.run(
start_date=dag.default_args["start_date"],
end_date=dag.default_args["start_date"],
)
244 Глава 9 Тестирование
The above exception was the direct cause of the following exception:
➥ > task.run(start_date=dag.default_args["start_date"],
end_date=dag.default_args["start_date"])
...
cursor = <sqlite3.Cursor object at 0x1110fae30>
➥ statement = 'SELECT task_instance.try_number AS task_instance_try_number,
task_instance.task_id AS task_instance_task_id, task_ins...\nWHERE
task_instance.dag_id = ? AND task_instance.task_id = ? AND
task_instance.execution_date = ?\n LIMIT ? OFFSET ?'
parameters = ('test_dag', 'test', '20150101 00:00:00.000000', 1, 0)
...
Рис. 9.9 Вызов метода task.run() приводит к сохранению сведений о запуске задачи
в базе данных
import pytest
from airflow.models import DAG
@pytest.fixture
def test_dag():
return DAG(
"test_dag",
1
Чтобы ваши тесты выполнялись изолированно, можно использовать кон-
тейнер Docker с пустой инициализированной базой данных Airflow.
246 Глава 9 Тестирование
default_args={
"owner": "airflow",
"start_date": datetime.datetime(2019, 1, 1),
},
schedule_interval="@daily",
)
task = MovielensDownloadOperator(
task_id="test",
conn_id="testconn",
start_date="{{ prev_ds }}",
end_date="{{ ds }}",
output_path=str(tmp_path / "{{ ds }}.json"),
dag=test_dag,
)
task.run(
start_date=dag.default_args["start_date"],
end_date=dag.default_args["start_date"],
)
class MovielensToPostgresOperator(BaseOperator):
template_fields = ("_start_date", "_end_date", "_insert_query")
def __init__(
self,
movielens_conn_id,
start_date,
end_date,
postgres_conn_id,
insert_query,
**kwargs,
):
super().__init__(**kwargs)
self._movielens_conn_id = movielens_conn_id
self._start_date = start_date
self._end_date = end_date
self._postgres_conn_id = postgres_conn_id
self._insert_query = insert_query
postgres_hook = PostgresHook(
postgres_conn_id=self._postgres_conn_id
)
insert_queries = [
➥ self._insert_query.format(",".join([str(_[1]) for _ in
sorted(rating.items())]))
for rating in ratings
]
postgres_hook.run(insert_queries)
248 Глава 9 Тестирование
postgres_image = fetch(repository="postgres:11.1-alpine")
postgres_image = fetch(repository="postgres:11.1-alpine")
def test_call_fixture(postgres_image):
print(postgres_image.id)
postgres_container = container(
image="{postgres_image.id}",
ports={"5432/tcp": None},
)
def test_call_fixture(postgres_container):
print(
f"Running Postgres container named {postgres_container.name} "
f"on port {postgres_container.ports['5432/tcp'][0]}."
)
250 Глава 9 Тестирование
import pytest
from airflow.models import Connection
252 Глава 9 Тестирование
postgres_image = fetch(repository="postgres:11.1alpine")
postgres = container(
image="{postgres_image.id}",
environment={
"POSTGRES_USER": "testuser",
"POSTGRES_PASSWORD": "testpass",
},
ports={"5432/tcp": None},
volumes={
os.path.join(os.path.dirname(__file__), "postgresinit.sql"): {
"bind": "/dockerentrypointinitdb.d/postgresinit.sql"
}
},
)
task = MovielensToPostgresOperator(
task_id="test",
movielens_conn_id="movielens_id",
start_date="{{ prev_ds }}",
end_date="{{ ds }}",
postgres_conn_id="postgres_id",
insert_query=(
"INSERT INTO movielens
(movieId,rating,ratingTimestamp,userId,scrapeTime) "
"VALUES ({0}, '{{ macros.datetime.now() }}')"
Работа с ОАГ и контекстом задачи в тестах 253
),
dag=test_dag,
)
pg_hook = PostgresHook()
task.run(
start_date=test_dag.default_args["start_date"],
end_date=test_dag.default_args["start_date"],
)
1
В Python 3.7 и PEP553 представлен новый способ установки точек остано-
ва: просто вызовите метод breakpoint().
256 Глава 9 Тестирование
Вот пример:
Резюме
Тест на благонадежность фильтрует основные ошибки в ваших ОАГ.
Модульное тестирование проверяет правильность отдельных опе-
раторов.
pytest и плагины предоставляют ряд полезных конструкций для
Docker;
о высокоуровневом обзоре рабочих процессов при
ОАГ
1
К сожалению, это не редкость, особенно в случае с малораспространенны-
ми и не особо часто используемыми операторами Airflow.
Проблемы, вызываемые множеством разных операторов 261
1
Просто посмотрите на файл setup.py, чтобы получить представление об
огромном количестве зависимостей, участвующих в поддержке всех опе-
раторов Airflow.
262 Глава 10 Запуск задач в контейнерах
Конфликт Конфликт
Библиотека X Библиотека Y
v3.0 v2.0
Рис. 10.2 Запуск большого количества ОАГ в одном окружении может привести
к конфликтам, когда ОАГ зависят от разных версий одних и тех же (или связанных) пакетов.
В частности, Python не поддерживает установку разных версий одного и того же пакета
в одном окружении. Это означает, что любые конфликты в пакетах (справа) необходимо
будет решить, заново написав ОАГ (или их зависимости), чтобы использовать те же версии
пакета
1
Для полного введения мы с радостью дадим вам ссылки на множество книг
о виртуализации на основе контейнеров и связанных с ними технологиях,
таких как Docker/Kubernetes.
Представляем контейнеры 263
Инфраструктура Инфраструктура
Options:
output_path FILE Optional file to write output to.
help Show this message and exit.
$ docker ps
$ docker rm <container_id>
Docker
ОАГ
Http-контейнер
DockerOperator
Контейнер
DockerOperator рекомендательной системы
DockerOperator
Контейнер MySQL
ОАГ 2
Задача 1 Задача 2 Задача 3
Библиотека Y Библиотека Y
v2.0 v1.0
уЛучшенная тестируемость
Наконец, еще одно преимущество использования образов контей-
неров состоит в том, что их можно разрабатывать и сопровождать
отдельно от ОАГ Airflow, в котором они работают. Это означает, что
у каждого образа может быть собственный жизненный цикл разра-
ботки и его можно подвергнуть специальному тесту (например, за-
пуск с имитированными данными), чтобы проверить, делает ли про-
граммное обеспечение в образе то, что мы ожидаем. Разделение на
контейнеры делает это тестирование проще, чем, например, при ис-
пользовании PythonOperator, который часто включает в себя задачи,
тесно связанные с самим ОАГ, что затрудняет тестирование функций
отдельно от слоя оркестровки Airflow.
1
Для Airflow версии 1.10.x можно установить DockerOperator, используя па-
кет apacheairflowbackportsizesdockerbackport.
Запуск задач в Docker 273
Рис. 10.7 Иллюстрация того, что происходит, когда задача выполняется с помощью
DockerOperator. В реестре образов хранится коллекция образов Docker. Это может быть
закрытый реестр (содержащий наши собственные образы) или общедоступный реестр,
такой как DockerHub (который используется по умолчанию при извлечении образов).
При извлечении образы кешируются локально, поэтому вам нужно делать это только один
раз (за исключением обновлений образа)
274 Глава 10 Запуск задач в контейнерах
1
Вы, конечно же, также можете использовать встроенную библиотеку ar
gparse, но лично нам очень нравится краткость API библиотеки click для
создания приложения командной строки.
Запуск задач в Docker 275
)
@click.option(
... Добавляет дополнительные параметры,
) необходимые для команды
@click.option(
... Параметры передаются в качестве
) ключевых аргументов в функцию main
... и могут использоваться с этого момента
def main(start_date, ...):
"""CLI script for fetching ratings from the movielens API."""
...
Способ, которым Python гарантирует,
if __name__ == "__main__": что функция или команда main вызываются
main() при выполнении этого сценария
_get_ratings(
session=session,
host=host, Использует нашу функцию _get_ratings
start_date=start_date, (опущена для краткости) для извлечения
end_date=end_date, рейтингов с помощью предоставленного сеанса
batch_size=batch_size,
)
)
logging.info("Retrieved %d ratings!", len(ratings))
1
Функция _get_ratings здесь опущена для краткости, но доступна в исход-
ном коде, прилагаемом к книге.
2
Это cделано для того, чтобы мы могли запустить сценарий с помощью
команды fetch-ratings, вместо того чтобы указывать полный путь к нему.
Запуск задач в Docker 277
CLI script for fetching movie ratings from the movielens API.
Options:
start_date [%Y%m%d] Start date for ratings. [required]
end_date [%Y%m%d] End date for ratings. [required]
output_path FILE Output file path. [required]
host TEXT Movielens API URL.
user TEXT Movielens API user. [required]
password TEXT Movielens API password. [required]
batch_size INTEGER Batch size for retrieving records.
help Show this message and exit.
Это означает, что теперь у нас есть образ контейнера для нашей
первой задачи. Можно использовать аналогичный подход к созда-
нию разных образов для других задач. В зависимости от количества
общего кода вы также можете создавать образы, которые используют-
ся в задачах, но могут запускаться с разными аргументами или даже
с разными сценариями. Как вы все это организуете, зависит от вас.
with DAG(
dag_id="01_docker",
description="Fetches ratings from the Movielens API using Docker.",
start_date=dt.datetime(2019, 1, 1),
end_date=dt.datetime(2019, 1, 3),
schedule_interval="@daily",
) as dag:
Fetch
ratings = DockerOperator( Сообщаем DockerOperator, чтобы он
task_id="fetch_ratings", использовал образ movielens-fetch
image="manningairflow/movielensfetch",
command=[
"fetchratings",
"start_date", Запускаем сценарий fetch-rating
"{{ds}}", в контейнере с необходимыми аргументами
"end_date",
"{{next_ds}}",
"output_path",
"/data/ratings/{{ds}}.json", Предоставляем информацию о хосте
"user",
и аутентификации для нашего API
os.environ["MOVIELENS_USER"],
"password",
os.environ["MOVIELENS_PASSWORD"], Монтируем том для хранения
"host", данных. Обратите внимание, что
os.environ["MOVIELENS_HOST"], этот путь к хосту находится на хосте
], Docker, а не в контейнере Airflow
volumes=["/tmp/airflow/data:/data"],
network_mode="airflow",
) Убеждаемся, что контейнер подключен к сети
Docker, чтобы он мог получить доступ к API
(который работает в той же сети)
1
Мы не будем здесь подробно рассматривать работу в сети, говоря о Docker,
поскольку это небольшая деталь реализации; вам не нужно настраивать
сеть, если вы обращаетесь к API в интернете. Если вам интересно, то для
знакомства с этой темой воспользуйтесь хорошей книгой по Docker или
онлайн-документацией.
2
Мы оставим третью задачу по загрузке рекомендаций в базу данных в ка-
честве упражнения.
Образ Контейнер
(1)
Разработчик (2) Собирает образ (7) Запускает контейнер
сообщает Docker, (3) (6)
что нужно собрать Docker Помещает Образ Извлекает Docker
и отправить образ образ образ
в реестр из реестра
Машина для разработки Реестр образов Рабочий процесс Airflow
(5)
(4) Airflow
Разработчик создает ОАГ планирует задачу
с помощью операторов DockerOperator
DockerOperator
Airflow
Рис. 10.9 Обычный рабочий процесс для работы с образами Docker в Airflow
1
Чтобы получить полный обзор Kubernetes, рекомендуем вам прочитать ис-
черпывающую книгу по данной теме, например «Kubernetes в действии»
(М.: ДМК Пресс, 2019).
Запуск задач в Kubernetes 283
Мастер Kubernetes
Контроллер/
Сервер API Планировщик Хранилище Секреты
менеджер
Извлекает образы
Реестр
образов
$ kubectl clusterinfo.
284 Глава 10 Запуск задач в контейнерах
1
Для Airflow версии 1.10.x можно установить KubernetesPodOperator, исполь-
зуя пакет apacheairflowbackportproviderscncfkubernetes backport.
Запуск задач в Kubernetes 287
...
volume_claim = k8s.V1PersistentVolumeClaimVolumeSource(
Ссылки на ранее
claim_name="datavolume"
созданный том
)
хранилища и заявку
volume = k8s.V1Volume(
name="datavolume",
persistent_volume_claim=volume_claim
)
volume_mount = k8s.V1VolumeMount(
name="datavolume",
mount_path="/data", Куда монтировать том
sub_path=None,
read_only=False, Монтируем том как доступный для записи
)
image_pull_policy="Never",
is_delete_operator_pod=True,
)
with DAG(
dag_id="02_kubernetes",
description="Fetches ratings from the Movielens API using kubernetes.",
start_date=dt.datetime(2019, 1, 1),
end_date=dt.datetime(2019, 1, 3),
schedule_interval="@daily",
) as dag:
volume_claim = k8s.V1PersistentVolumeClaimVolumeSource(...)
volume = k8s.V1Volume(...)
volume_mount = k8s.V1VolumeMount(...)
fetch_ratings = KubernetesPodOperator(...)
rank_movies = KubernetesPodOperator(...)
Узлы
(1) Образ
Разработчик
сообщает Docker, (2) Собирает образ (3) (7) (8) Запускает под
что нужно собрать Помещает Извлекает на узле
Docker образ Образ образ
и отправить образ Master
в реестр из реестра
Машина для разработки Реестр образов Кластер Kubernetes
(6)
KubernetesPodOperator
сообщает кластеру, что задача
(4) должна запускаться как под
Разработчик создает ОАГ (5)
с помощью операторов Airflow планирует задачу Рабочий
KubernetesPodOperator KubernetesPodOperator процесс Airflow
Airflow
Резюме
Развертываниями Airflow может быть сложно управлять, если они
связаны со множеством различных операторов, так как это требует
знания различных API и усложняет отладку и управление зависи-
мостями.
Один из способов решения этой проблемы – использовать систему
i=i+1
submitted +=1
my_list = [
1, 2, 3,
4, 5, 6,
]
i = i + 1 Согласованные пробелы
submitted += 1 вокруг операторов
Написание чистых ОАГ 297
Эта команда запускает Flake8 для всех файлов Python в папке ОАГ,
предоставляя отчет о воспринимаемом качестве кода этих файлов.
Обычно отчет выглядит примерно так.
1
Можно считать это сильной или слабой стороной Pylint, в зависимости от
предпочтений, поскольку некоторые находят его слишком педантичным.
298 Глава 11 Лучшие практики
1
Предполагая, что Airflow сконфигурирован с учетом стандартов безопас-
ности. См. главы 12 и 13 для получения дополнительной информации о на-
стройке развертываний и безопасности в Airflow.
Написание чистых ОАГ 301
input_path = Variable.get("dag1_input_path")
output_path = Variable.get("dag1_output_path") Извлечение глобальных
переменных с помощью
fetch_data = PythonOperator( механизма Airflow Variables
task_id="fetch_data",
op_kwargs={
"input_path": input_path,
"output_path": output_path,
},
...
)
1
Обратите внимание: вы должны быть осторожны и не хранить конфиден-
циальные данные в таких файлах конфигурации, поскольку, как правило,
они хранятся в виде обычного текста. Если вы храните их в конфигураци-
онных файлах, убедитесь, что только нужные люди имеют полномочия для
доступа к файлу. В противном случае рассмотрите возможность хранения
этих данных в более безопасных местах, таких как база метаданных Airflow.
2
Обратите внимание, что извлечение таких переменных в глобальной об-
ласти видимости вашего ОАГ обычно плохо сказывается на его произво-
дительности. Прочитайте следующий подраздел, чтобы узнать, почему.
Написание чистых ОАГ 303
fetch_data = PythonOperator(
op_kwargs={"config_path": "config.yaml"},
...
)
304 Глава 11 Лучшие практики
PythonOperator(
Написание чистых ОАГ 305
task_id="my_not_so_efficient_task",
... Здесь значение будет вычисляться
op_kwargs={ каждый раз при анализе ОАГ
"value": calc_expensive_value()
}
)
def _my_more_efficient_task(...):
value = calc_expensive_value()
... Перенося вычисление в задачу,
PythonOperator( значение будет вычисляться
task_id="my_more_efficient_task", только при выполнении задачи
python_callable=_my_more_efficient_task,
...
)
api_config = BaseHook.get_connection("my_api_conn")
api_key = api_config.login Этот вызов будет обращаться к базе
api_secret = api_config.password данных каждый раз при анализе ОАГ
task1 = PythonOperator(
op_kwargs={"api_key": api_key, "api_secret": api_secret},
...
)
...
preprocess_task = BashOperator(
task_id=f"preprocess_{dataset_name}",
bash_command=f"echo '{preprocess_script} {raw_path}
➥ {processed_path}'",
dag=dag,
)
export_task = BashOperator(
task_id=f"export_{dataset_name}",
bash_command=f"echo 'cp {processed_path} {output_path}'",
dag=dag,
)
with DAG(
dag_id="01_task_factory",
start_date=airflow.utils.dates.days_ago(5),
schedule_interval="@daily",
308 Глава 11 Лучшие практики
) as dag:
Создание наборов задач с разными
for dataset in ["sales", "customers"]:
значениями конфигурации
generate_tasks(
dataset_name=dataset,
raw_dir="/data/raw",
processed_dir="/data/processed", Передача экземпляра ОАГ
output_dir="/data/output", для подключения задач к ОАГ
preprocess_script=f"preprocess_{dataset}.py",
dag=dag,
)
Задачи, генерируемые
одним вызовом
фабричной функции
Задачи, созданные
другим вызовом
fetch_task = BashOperator(...)
preprocess_task = BashOperator(...)
return dag
Написание чистых ОАГ 309
Рис. 11.3 Группы задач могут помочь организовать ОАГ путем группировки связанных
задач. Изначально группы задач изображены в виде отдельных узлов в ОАГ, как показано
для группы задач customers на этом рисунке. Нажав на группу задач, вы можете развернуть
ее и просмотреть задачи в группе, как показано здесь для группы задач sales. Обратите
внимание, что группы задач могут быть вложенными. Это означает, что у вас могут быть
группы задач внутри других групп
Рис. 11.4 Использование групп задач для организации ОАГ из главы 5. Здесь группировка
задачи для наборов по извлечению и очистке данных о погоде и продажах помогает
значительно упростить сложные структуры задач, участвующих в этих процессах (пример
кода приведен в dags/04_task_groups_umbrella.py)
312 Глава 11 Лучшие практики
Продажи Фильтрация
и агрегирование
Объединяем
наборы данных
через соединение
Агрегируем продажи
по каждому покупателю
Клиенты
Агрегирование
Агрегированные
Продажи продажи
Объединяем
наборы данных
через соединение
Агрегируем продажи
по каждому покупателю
Фильтрация
нужных клиентов
Избранные клиенты
Клиенты
Запуск 1
Монолитный
запуск Запуск 2
Запуск 3
Совокупный
результат
Пакеты Пакеты Инкрементные
данных данных результаты
Рис. 11.6 Иллюстрация монолитной обработки (A), при которой весь набор
данных обрабатывается при каждом запуске, по сравнению с инкрементальной
обработкой (B), при которой набор данных анализируется инкрементными
партиями по мере поступления данных
Извлечение Предварительная
API записей обработка
Необработанные Предварительно
данные обработанные данные
Рис. 11.7 Сохранение промежуточных данных из задач гарантирует, что каждую задачу
можно легко запустить повторно независимо от других задач. В этом примере облачное
хранилище (обозначенное бакетом) используется для хранения промежуточных
результатов задач fetch/preprocess
Название пула
default_args = {
"sla": timedelta(hours=2),
...
}
with DAG(
dag_id="...",
...
default_args=default_args,
) as dag:
...
...
with DAG(
...
sla_miss_callback=sla_miss_callback
) as dag:
...
Резюме
Принятие общих соглашений о стилях наряду с вспомогательными
инструментами проверки соблюдения стандарта оформления кода
и форматирования может значительно повысить читабельность
кода вашего ОАГ.
Фабричные методы позволяют эффективно создавать повторяю-
Airflow;
об отправке оповещений при сбое задачи.
Конфигурация Airflow
В этой главе мы часто упоминаем конфигурацию Airflow. Она интерпре-
тируется в следующем порядке предпочтения:
1 переменная окружения (AIRFLOW__[SECTION]__[KEY]);
2 переменная окружения команды (AIRFLOW__[SECTION]__[KEY]_CMD);
3 в airflow.cfg;
4 команда в airflow.cfg;
5 значение по умолчанию.
Всякий раз, когда речь идет о параметрах конфигурации, мы будем де-
монстрировать вариант 1. Например, возьмем элемент конфигурации
web_server_port из раздела webserver. Он будет продемонстрирован
как AIRFLOW__WEBSERVER__WEB_SERVER_PORT.
Чтобы найти текущее значение любого элемента конфигурации, можно про-
крутить вниз страницу Configurations (Конфигурации) в пользовательском
интерфейсе Airflow до таблицы Running Configuration (Текущая конфигу-
рация), где показаны все параметры конфигурации, их текущее значение
и какой из пяти вариантов был использован для настройки конфигурации.
планировщик;
база данных.
То, что вы видите, – это вывод Alembic, еще одного фреймворка, ис-
пользуемого для миграции баз данных, написанного на Python. Каж-
дая строка в листинге 12.1 – это вывод миграции одной-единственной
базы данных. При обновлении до более новой версии Airflow, содер-
жащей миграции базы данных (в примечаниях к выпуску указано, со-
держит ли новая версия обновления базы данных), необходимо также
обновить соответствующую базу данных. Выполняя команду airflow
db upgrade, вы проверяете, на каком этапе миграции находится ваша
текущая база данных, и применяете этапы миграции, которые были
добавлены в новом выпуске.
На данном этапе у вас есть полнофункциональная база данных Air-
flow, и вы можете выполнить команды airflow webserver и airflow
scheduler. При открытии веб-сервера по адресу http://localhost:8080
вы увидите много примеров ОАГ и подключений (рис. 12.2).
Рис. 12.2 По умолчанию Airflow загружает примеры ОАГ (и подключений, которые здесь
не отображаются)
в базе данных;
определение того, какие задачи готовы к выполнению, и их раз-
мещение в очереди;
извлечение и выполнение задач в очереди.
процессор оаг
Планировщик Airflow периодически обрабатывает файлы Python
в каталоге ОАГ (каталог, заданный AIRFLOW__CORE__DAGS_FOLDER). Это
означает, что даже если в файл ОАГ не было внесено никаких измене-
ний1, он периодически проверяет каждый файл и сохраняет найден-
ные ОАГ в базе метаданных Airflow, потому что вы можете создавать
динамические ОАГ (эта структура изменений на основе внешнего ис-
точника в Airflow), в то время как код остается прежним. В качестве
примера можно привести ОАГ, в котором считывается файл YAML
и на основе его содержимого создаются задачи. Чтобы работать с из-
менениями в динамических ОАГ, планировщик периодически обра-
батывает файлы.
Обработка ОАГ требует вычислительных мощностей. Чем больше
вы повторно обрабатываете файлы, тем быстрее будет идти работа
с изменениями, но за счет увеличения мощности процессора. Если
вы знаете, что ваши ОАГ не изменяются динамически, то можно уве-
личить интервалы по умолчанию, чтобы уменьшить нагрузку на про-
цессор, ничего не опасаясь. Интервал обработки ОАГ связан с четырь-
мя конфигурациями (см. табл. 12.2).
1
В то время как в сообществе Airflow продолжаются дискуссии по поводу
того, чтобы сделать анализ ОАГ событийно-ориентированным, путем про-
слушивания изменений в файлах и явной настройки ОАГ для повторной
обработки, если это необходимо, что могло бы облегчить использование
ЦП планировщиком, на момент написания этой книги такой возможности
пока не существует.
332 Глава 12 Эксплуатация Airflow в промышленном окружении
File Path PID Runtime # DAGs # Errors Last Runtime Last Run
.../dag1.py 1 0 0.09s ... 18:55:15
.../dag2.py 1 0 0.09s ... 18:55:15
.../dag3.py 1 0 0.10s ... 18:55:15
.../dag4.py 358 0.00s 1 0 0.08s ... 18:55:15
.../dag5.py 359 0.07s 1 0 0.08s ... 18:55:15
=================================================================
... – Finding 'running' jobs without a recent heartbeat
... – Failing jobs without heartbeat after 2020-12-20 18:50:22.255611
... – Finding 'running' jobs without a recent heartbeat
... – Failing jobs without heartbeat after 2020-12-20 18:50:32.267603
... – Finding 'running' jobs without a recent heartbeat
... – Failing jobs without heartbeat after 2020-12-20 18:50:42.320578
пЛанировщик заданий
Планировщик отвечает за определение того, какие экземпляры зада-
чи могут быть выполнены. Цикл while True периодически проверяет
для каждого экземпляра задачи, выполняется ли набор условий, на-
пример (среди прочего) удовлетворены ли все вышестоящие зависи-
мости, достигнут ли конец интервала, успешно ли запущен экземпляр
задачи в предыдущем ОАГ, если для depends_on_past задано значение
True, и так далее. Всякий раз, когда экземпляр задачи соответствует
Архитектура Airflow 333
испоЛнитеЛь задач
Как правило, исполнитель задачи будет ждать, пока планировщик
разместит экземпляры задач для выполнения в очереди. После поме-
щения в очередь исполнитель извлекает экземпляр задачи из очере-
ди и выполняет его. Airflow регистрирует каждое изменение состоя-
ния в базе метаданных. Сообщение, помещенное в очередь, содержит
несколько деталей экземпляра задачи. В исполнителе выполнение за-
дач означает создание нового процесса для задачи, которую нужно
запустить, чтобы Airflow не отключился, если что-то пойдет не так.
В новом процессе он выполняет команду airflow tasks для запуска
одного экземпляра задачи, как показано в следующем примере (ис-
пользуется LocalExecutor).
For example:
➥ airflow tasks run chapter12_task_sla sleeptask 20200404T00:00:00+00:00
local pool default_pool sd /..../dags/chapter12/task_sla.py
334 Глава 12 Эксплуатация Airflow в промышленном окружении
не выполнена;
если она не завершена,
Планировщик
Подпроцесс
ОАГ
Веб-сервер База данных Планировщик
airflow webserver.
Подпроцесс
Подпроцесс
Подпроцесс
Планировщик …
Веб-сервер База данных
Подпроцесс
ОАГ
А вот что касается папки ОАГ, здесь могут возникнуть сложности с на-
стройкой. Вы делаете ОАГ доступными для всех машин либо через
общую файловую систему, либо используя контейнеризацию, когда
ОАГ встраиваются в образ с помощью Airflow. При использовании
контейнеризации любое изменение в коде ОАГ приведет к повторно-
му развертыванию программного обеспечения.
Подпроцесс
Воркер Celery
Подпроцесс
Веб-сервер База данных Подпроцесс
…
Подпроцесс
Подпроцесс
Воркер Celery
Подпроцесс
Подпроцесс
…
Очередь
Подпроцесс
Планировщик
Подпроцесс
Воркер Celery
Подпроцесс
ОАГ
Подпроцесс
…
Подпроцесс
localhost/airflow;
PostgreSQL: AIRFLOW__CELERY__RESULT_BACKEND=db+postgresql://
user:pass@localhost/airflow.
Убедитесь, что папка ОАГ также доступна на машинах, где функ-
ционируют воркеры, по тому же пути, как настроено в AIRFLOW__
CORE__DAGS_FOLDER. После этого вы должны быть готовы:
1 запустить веб-сервер Airflow;
2 запустить планировщик;
3 запустить воркер Celery.
airflow celery worker – это небольшая команда-оболочка, запуска-
ющая воркер Celery. Теперь все должно быть готово и работать.
Рис. 12.8 Панель управления Flower показывает состояние всех воркеров Celery
Установка исполнителей 339
Успешные задачи указывают на то, что Celery Задачи, поставленные в очередь, готовы
может читать и выполнять задачи из очереди к обработке, но на данный момент у Celery
и что установка CeleryExecutor прошла успешно недостаточно рабочих слотов
Рис. 12.9 Вкладка мониторинга Flower показывает графики, что помогает получить
представление о производительности системы Celery
Задача
Airlfow
Задача
Airlfow
Планировщик Задача
ОАГ Airlfow
…
База
данных
Задача
Airlfow
Веб-сервер
Airflow dashboard:
➥ kubectl portforward svc/airflowwebserver 8080:8080 namespace airflow
Планировщик
Airlfow
Общая файловая
Машина система
для разработки
Задача Airflow
Плани-
FTP ровщик NFS Задача Airflow
Машина Airlfow
для разработки Контроль версий
Рис. 12.12 Файлы нельзя записывать напрямую в NFS, потому что у нее нет
интернет-интерфейса. Для отправки и получения файлов по сети можно было бы
использовать FTP, чтобы хранить файлы на том же компьютере, куда монтируется NFS
dag = DAG(
зависимости; всегда
dag_id="dag_puller",
запускаем задачи
Игнорируем все
fetch_code = BashOperator(
task_id="fetch_code",
bash_command=( Требует установки
"cd /airflow/dags && " и настройки Git
"git reset --hard origin/master"
),
dag=dag,
)
Планировщик
Задача Airflow NFS
Машина Airlfow
для разработки Контроль версий
tory/airflow;
AIRFLOW__KUBERNETES__GIT_BRANCH = master;
AIRFLOW__KUBERNETES__GIT_SUBPATH = dags;
AIRFLOW__KUBERNETES__GIT_USER = username;
AIRFLOW__KUBERNETES__GIT_PASSWORD = password;
AIRFLOW__KUBERNETES__GIT_SSH_KEY_SECRET_NAME = airflowse
crets;
AIRFLOW__KUBERNETES__GIT_DAGS_FOLDER_MOUNT_POINT = /opt/air
flow/dags;
AIRFLOW__KUBERNETES__GIT_SYNC_CONTAINER_REPOSITORY = k8s.gcr.
io/gitsync;
AIRFLOW__KUBERNETES__GIT_SYNC_CONTAINER_TAG = v3.1.2;
AIRFLOW__KUBERNETES__GIT_SYNC_INIT_CONTAINER_NAME = gitsync
clone.
Хотя не нужно заполнять все данные, если задать значение для GIT_
REPO и учетные данные (USER + PASSWORD или GIT_SSH_KEY_SECRET_NAME),
то вы активируете синхронизацию с Git. Airflow создаст контейнер
синхронизации, который извлекает код из сконфигурированного ре-
позитория перед запуском задачи.
er
o ck
од
а аD
аз
иек обр
щ ен н ие
зме зда Airflow + Airflow +
Ра Со
Задача
Машина
для разработки Контроль версий
Рис. 12.14 После размещения изменений в систему управления версиями создается новый
образ Docker
жении;
конфликты между новыми зависимостями обнаруживаются во
1
Оба файла Dockerfile предназначены для демонстрационных целей.
346 Глава 12 Эксплуатация Airflow в промышленном окружении
0.0.0.0:8080 (90649);
[2020-04-13 12:22:51 +0200] [90649] [INFO] Using worker: sync;
90652.
Оба типа журналов можно записать в файл, указав параметр при
запуске веб-сервера Airflow:
airflow webserver --access_logfile [filename];
│ ├── world
│ │ └── 2020-04-14T16:00:00+00:00
│ │ ├── 1.logе
│ │ └── 2.log
└── second_dag
└── print_context
├── 2020-04-11T00:00:00+00:00
│ └── 1.log
└── 2020-04-12T00:00:00+00:00
└── 1.log
[microsoft.azure]);
Elasticsearch (требуется команда pip install apacheairflow
[elasticsearch]);
Google Cloud Storage (требуется команда pip install apacheair
flow [google]).
Чтобы настроить Airflow для удаленного журналирования, задайте
следующие конфигурации:
AIRFLOW__CORE__REMOTE_LOGGING=True;
AIRFLOW__CORE__REMOTE_LOG_CONN_ID=.
отправка и извЛечение
При сравнении систем метрик часто ведется дискуссия по поводу мо-
дели push vs pull. В случае с push-моделью метрики отправляются (push)
в систему сбора метрик. В pull-модели доступ к метрикам предостав-
ляется системой для мониторинга определенной конечной точки,
352 Глава 12 Эксплуатация Airflow в промышленном окружении
нию);
AIRFLOW__METRICS__STATSD_PORT=9125;
нию).
Что касается Airlfow, то здесь все готово. В этой конфигурации Air-
flow будет отправлять события на порт 9125 (по протоколу UDP).
Щелкните Add your first data source (Добавить свой первый ис-
точник данных), чтобы добавить Prometheus в качестве источника
данных. Вы увидите список доступных источников данных. Щелкните
Prometheus, чтобы настроить его (рис. 12.19).
Визуализация и мониторинг метрик Airflow 357
Рис. 12.21 График количества секунд для обработки всех файлов ОАГ. Мы видим две точки
изменения, в которых были добавлены дополнительные файлы ОАГ. Резкий всплеск на этом
графике может указывать на проблему с планировщиком Airflow или файлом ОАГ
задержка
Сколько времени уходит на обработку служебных запросов? Поду-
майте, сколько времени требуется веб-серверу, чтобы ответить, или
сколько времени нужно планировщику, чтобы переместить задачу из
состояния очереди в состояние запуска. Эти показатели выражаются
Визуализация и мониторинг метрик Airflow 359
трафик
Насколько востребована система? Подумайте, сколько задач ваша си-
стема Airflow должна обработать, или сколько открытых слотов пула
доступно Airflow. Эти показатели обычно выражаются как среднее
значение за продолжительность (например, «количество выполняе-
мых задач в минуту» или «открытых слотов пула в секунду»).
ошибки
Какие ошибки возникли? В контексте Airflow это может быть «число
задач-зомби» (задач, в которых основной процесс исчез), «количество
ответов на веб-сервере, чей код состояния не относится к 200» или
«количество истекших задач».
насыщенность
Какая часть мощности вашей системы используется? Измерение
метрик машины, на которой работает Airflow, может быть хорошим
индикатором, например «текущая загрузка процессора» или «коли-
чество выполняемых в данный момент задач». Чтобы определить,
насколько заполнена система, вы должны знать ее верхний предел,
который иногда бывает непросто определить.
Prometheus предлагает широкий спектр экспортеров, предостав-
ляющих всевозможные метрики системы. Начните с установки не-
скольких экспортеров Prometheus, чтобы узнать больше обо всех за-
действованных системах:
экспортер узлов – для мониторинга компьютеров, на которых ра-
данных;
один из нескольких (неофициальных) экспортеров Celery – для мо-
AIRFLOW__SMTP__SMTP_PASSWORD=abcdefghijklmnop
AIRFLOW__SMTP__SMTP_PORT=587
AIRFLOW__SMTP__SMTP_SSL=False
AIRFLOW__SMTP__SMTP_STARTTLS=True
[email protected]
Blocking tasks:
=, .=
=.| ,. |.=
=.| "(:::::)" |.=
\\__/`.|.'\__//
`| .::| .::|' Pillendreher
_|`._|_.'|_ (Scarabaeus sacer)
/.| | .::|.\
// ,| .::|::::|. \\
|| //\::::|::' /\\ ||
/'\|| `.__|__.' ||/'\
^ \\ // ^
/'\ /'\
^ ^
Да, в письме был этот жук в кодировке ASCII! Хотя задача из листин-
га 12.22 служит примером, желательно настроить SLA для обнаруже-
ния отклонений в вашем задании. Например, если входные данные
вашего задания внезапно увеличиваются в пять раз, а это приводит
к тому, что задание занимает значительно больше времени, можно
рассмотреть возможность повторного вычисления определенных па-
раметров задания. Дрейф в размере данных и результирующую про-
должительность задания можно обнаружить с помощью SLA.
Сообщение по электронной почте уведомляет вас только о наруше-
нии SLA, поэтому можно рассмотреть что-то другое, кроме электрон-
ной почты или вашего собственного формата. Этого можно добиться
с помощью аргумента sla_miss_callback. Как ни странно, это аргу-
мент класса DAG, а не класса BaseOperator.
Если вы ищете максимальное время выполнения задачи, настрой-
те аргумент execution_timeout для своего оператора. Если продолжи-
тельность задачи превышает настроенное значение execution_time
out, она завершается ошибкой.
LocalExecutor;
CeleryExecutor;
KubernetesExecutor.
task_concurrency = 2
airflow scheduler
368 Глава 12 Эксплуатация Airflow в промышленном окружении
Резюме
SequentialExecutor и LocalExecutor ограничены одной машиной,
но их легко настроить.
Для настройки CeleryExecutor и KubernetesExecutor требуется
данных;
об обеспечении безопасности трафика между вашим
браузером и веб-сервером;
об извлечении секретов из центральной системы
управления секретами.
Интерфейсы Airflow
В Airflow 1.x есть два интерфейса:
оригинальный интерфейс, разработанный поверх Flask-Admin;
интерфейс RBAC, разработанный поверх Flask-AppBuilder (FAB).
Первоначально Airflow поставлялся с исходным интерфейсом и впервые
представил интерфейс управления доступом на основе ролей (RBAC) в Air-
flow версии 1.10.0. Интерфейс RBAC предоставляет механизм, который
ограничивает доступ, определяя роли с соответствующими полномочиями
и назначая эти роли пользователям. Исходный интерфейс по умолчанию
открыт для всех. Интерфейс RBAC имеет больше функций безопасности.
Во время написания этой книги исходный интерфейс устарел и был уда-
лен в Airflow 2.0. RBAC теперь является единственным интерфейсом, по-
этому в этой главе мы рассматриваем только его. Чтобы активировать его
интерфейс RBAC с Airflow 1.x, задайте для AIRFLOW__WEBSERVER__RBAC
значение True.
1. Перечислить роли
База данных
1
В любом облаке легко можно предоставить доступ к службе через интер-
нет. Простые меры, которые можно предпринять, чтобы избежать этого,
включают в себя отказ от использования внешнего IP-адреса и/или блоки-
ровку всего трафика, допуская только свой диапазон IP-адресов.
376 Глава 13 Безопасность в Airflow
Ключ Fernet
Шифрование Расшифровка
Пароль Пароль
Рис. 13.10 Ключ Fernet шифрует данные, перед тем как сохранить их в базе данных,
и расшифровывает данные перед их чтением из базы данных. Без доступа к ключу Fernet
пароли бесполезны для злоумышленника. Когда один и тот же ключ используется для
шифрования и дешифровки, такой способ называется симметричным шифрованием
fernet_key = Fernet.generate_key()
print(fernet_key.decode())
# YlCImzjge_TeZc7jPJ7Jz2pgOtb4yTssA1pVyqIADWg=
AIRFLOW__CORE__FERNET_KEY=YlCImzjge_TeZc7jPJ7Jz2pgOtb4yTssA1pVyqIADWg=
dc = com
dc = apacheairflow
1
Эти стандарты определены в RFC 4510-4519.
380 Глава 13 Безопасность в Airflow
1
ldapsearch требует установки пакета ldap-utils.
Шифрование трафика на веб-сервер 381
Веб-сервер
1
Можно вручную отредактировать таблицу ab_user_role в базе метаданных,
чтобы назначить другую роль (после первого входа).
382 Глава 13 Безопасность в Airflow
(2) Веб-страница
Симметричное шифрование
(Один) ключ шифрования
Шифрование Расшифровка
Пароль Пароль
Асимметричное шифрование
Открытый ключ Закрытый ключ
Шифрование Расшифровка
Пароль Пароль
cate.pem;
AIRFLOW__WEBSERVER__WEB_SERVER_SSL_KEY=/путь/к/pri-
vatekey.pem.
Запустите веб-сервер, и вы увидите, что он больше не обслуживает-
ся по адресу http://localhost: 8080. Теперь адрес выглядит так: https://
localhost:8080 (рис. 13.19).
На данном этапе трафик между вашим браузером и веб-сервером
Airflow зашифрован. Хотя злоумышленник может перехватить тра-
фик, для него он будет бесполезен, поскольку он зашифрован, а следо-
вательно, его нельзя прочитать. Расшифровать данные можно только
с помощью закрытого ключа; вот почему так важно никогда и никому
не давать закрытый ключ и хранить его в надежном месте.
При использовании самозаверенного сертификата, созданного
в листинге 13.5, вы сначала получите предупреждение (на рисунке
показана страница из Chrome 13.20).
На вашем компьютере есть список доверенных сертификатов и их
расположение в зависимости от операционной системы. В боль-
шинстве систем Linux доверенные сертификаты хранятся в ката-
логе /etc/ssl/certs. Эти сертификаты предоставляются с вашей опе-
рационной системой и согласованы с различными органами. Они
386 Глава 13 Безопасность в Airflow
1
Для ясности различные технические детали опущены. Хранить миллиарды
доверенных сертификатов для всех сайтов непрактично. Вместо этого на
вашем компьютере хранится несколько сертификатов наверху цепочки.
Сертификаты выдаются определенными доверенными центрами.
Шифрование трафика на веб-сервер 387
Выбираем System
Всегда доверять
SSL, используя
самозаверенный
сертификат
Как видно из листинга 13.5, в коде вашего ОАГ нет явной ссылки на
HashiCorp Vault. SimpleHttpOperator выполняет HTTP-запрос, в дан-
ном случае к URL-адресу, заданному в подключении. Раньше нужно
было сохранять URL-адреса в подключении. Теперь мы можем сохра-
нить его (среди прочего) в HashiCorp Vault. При этом следует отметить
несколько моментов:
бэкенды секретов должны быть настроены с помощью AIRFLOW__
SECRETS__BACKEND и AIRFLOW__SECRETS__BACKEND_KWARGS;
все секреты должны иметь общий префикс;
Systems- ManagerParameterStoreBackend;
Извлечение учетных данных из систем управления секретами 391
Рис. 13.24 Для сохранения сведений о подключении Airflow в Vault необходимо задать
ключ: conn_uri
{"url":"http://vault:8200","token":"airflow","connections_path":"connections"}.
Резюме
В целом безопасность не сосредоточивается на одном элементе,
а включает в себя обеспечение различных уровней вашего прило-
жения, чтобы ограничить потенциальную поверхность атаки.
В интерфейсе RBAC есть механизм безопасности на основе ролей,
похожих преобразований.
low Cab;
MinIO, хранилище объектов, поддерживающее протокол S3;
Рис. 14.1 Файл Docker Compose создает несколько сервисов. Наша задача –
загрузить данные из REST API, поделиться ими и преобразовать их, чтобы
в конечном итоге просмотреть самый быстрый способ передвижения на
получившейся веб-странице
1
Некоторые идеи в этой главе основаны на посте из блога Тодда Шнайдера
(https://toddwschneider.com/posts/taxi-vs-citi-bike-nyc/), где он анализирует
самый быстрый способ передвижения, применяя симуляцию Монте-Карло.
Проект: поиск самого быстрого способа передвижения по Нью-Йорку 395
flow);
5433: база данных Postgres для такси Нью-Йорка (taxi/ridetlc);
Нью-Йорку (nyc/tr4N5p0RT4TI0N);
8080: веб-сервер Airflow (airflow/airflow);
fiCYEXAMPLEKEY).
Данные о поездках как для Yellow Cab, так и для Citi Bikes предо-
ставляются ежемесячными партиями:
желтое такси Нью-Йорка: https://www1.nyc.gov/site/tlc/about/tlc-
trip-record-data.page;
Citi Bike: https://www.citibikenyc.com/system-data.
Рис. 14.2 Зоны желтого такси Нью-Йорка с указанием местоположения станций Citi Bike
http://localhost:8082/recent/<period>/<amount>
где <period> может быть минута, час или день. <amount> – это целое
число, обозначающее количество заданных периодов. Например, за-
прос http://localhost:8082/latest/day/3 вернет все поездки Citi Bike,
завершенные за последние три дня.
API не знает ограничений по размеру запроса. Теоретически мы
могли бы запрашивать данные за бесконечное количество дней. На
Разбираемся с данными 399
Рис. 14.3 Сопоставление станций Citi Bike (точки) с зонами Yellow Cab
обеспечивает точное сравнение, но не учитывает тот факт, что поездки
в пределах одной зоны могут отличаться по расстоянию. Поездка A, очевидно,
короче, чем поездка B. Усредняя время поездки из юга Гринвич-Виллидж до Ист-
Виллидж, вы теряете эту информацию
dag = DAG(
dag_id="nyc_dag", Запуск каждые 15 минут
schedule_interval="*/15 * * * *",
start_date=airflow.utils.dates.days_ago(1),
catchup=False,
)
import requests
from airflow.hooks.base import BaseHook
1
Задав для xcom_push значение True, можно сохранить вывод в XCom.
Извлечение данных 401
citibike_conn = BaseHook.get_connection(conn_id="citibike")
➥ url = f"http://{citibike_conn.host}:{citibike_conn.port}/recent/minute/15"
➥ response = requests.get(url, auth=HTTPBasicAuth(citibike_conn.login,
citibike_conn.password))
data = response.json() Используем S3Hook для обмена
данными с MinIO
s3_hook = S3Hook(aws_conn_id="s3")
s3_hook.load_string(
string_data=json.dumps(data),
key=f"raw/citibike/{ts_nodash}.json",
bucket_name="datalake"
)
download_citi_bike_data = PythonOperator(
task_id="download_citi_bike_data",
python_callable=_download_citi_bike_data,
dag=dag,
)
AIRFLOW_CONN_S3=s3://@?host=http://minio:9000&aws_access_key_id=...&aws_secret_access_key=...
Рис. 14.4 Пользовательское имя хоста S3 можно задать, но не там, где вы этого ожидали
в формате JSON;
мы не знаем заранее имена файлов; чтобы получить список фай-
download_taxi_data = PythonOperator(
task_id="download_taxi_data",
python_callable=_download_taxi_data,
dag=dag,
)
url = f"http://{taxi_conn.host}"
response = requests.get(url)
files = response.json()
Данные для API Citi Bike и файлового ресурса Yellow Cab загружа-
ются в хранилище MinIO (рис. 14.7).
Применение аналогичных преобразований к данным 405
Каждые 15 минут новый экспорт сохраняется в озере данных для обоих наборов данных
/raw/taxi/*.csv.
/processed/taxi/{ts_nodash}.parquet.
@apply_defaults
def __init__(
self,
input_callable,
output_callable,
Применение аналогичных преобразований к данным 407
transform_callable=None,
input_callable_kwargs=None,
transform_callable_kwargs=None,
output_callable_kwargs=None,
**kwargs,
):
super().__init__(**kwargs)
if self._transform_callable:
df = self._transform_callable(
Применяем df,
преобразования **self._transform_callable_kwargs,
к DataFrame )
Записываем logging.info("DataFrame shape after transform: %s.", df.shape)
DataFrame
self._output_callable(df, **self._output_callable_kwargs)
input_callable=get_minio_object,
Читаем CSV-файл
input_callable_kwargs={
из хранилища MinIO
"pandas_read_callable": pd.read_csv,
"bucket": "datalake",
"paths": "{{ ti.xcom_pull(task_ids='download_taxi_data') }}",
},
transform_callable=transform_taxi_data,
output_callable=write_minio_object,
output_callable_kwargs={ Пишем файл
Применяем "bucket": "datalake", с расширением
преобразования "path": "processed/taxi/{{ ts_nodash }}.parquet", .parquet в хранилище
к DataFrame "pandas_write_callable": pd.DataFrame.to_parquet, MinIO
"pandas_write_callable_kwargs": {"engine": "auto"},
},
dag=dag,
)
if isinstance(paths, str):
paths = [paths]
if pandas_read_callable_kwargs is None:
pandas_read_callable_kwargs = {}
dfs = []
for path in paths:
minio_object = minio_client.get_object(
bucket_name=bucket,
object_name=path,
)
Применение аналогичных преобразований к данным 409
pandas_write_method(bytes_buffer, **pandas_write_callable_kwargs)
nbytes = bytes_buffer.tell() Вызываем метод записи DataFrame
bytes_buffer.seek(0) для записи DataFrame в байтовый буфер,
minio_client.put_object( который можно хранить в MinIO
bucket_name=bucket,
object_name=path, Сохраняем байтовый
length=nbytes, буфер в MinIO
data=bytes_buffer,
)
Собственные системы
airflow_execution_date TIMESTAMP
);
Резюме
Разработка идемпотентных задач может быть разной в зависимо-
сти от случая.
Сохранение промежуточных данных гарантирует, что мы можем
Планировщик
Веб-сервер (+ воркеры)
1
В Airflow 1 и веб-серверу Airflow, и планировщику по умолчанию требуется
доступ к хранилищу ОАГ. В Airflow версии 1.10.10 была добавлена опция,
чтобы веб-сервер хранил ОАГ в базе метаданных, поэтому ему больше не
требуется доступ к хранилищу ОАГ, если эта опция активирована. В Air-
flow 2 эта опция всегда активирована, поэтому веб-серверу никогда не тре-
буется доступ к хранилищу ОАГ.
Проектирование стратегий (облачного) развертывания 419
Веб-сервер База
метаданных
Интернет
Планировщик
(+ воркеры)
Веб-сервер База
метаданных
Интернет
Планировщик
Брокер
сообщений
Воркер
15.3.1 Astronomer.io
Astronomer.io – это решение на базе Kubernetes для Airflow, которое
можно использовать как SaaS-решение (англ. software as a service –
программное обеспечение как услуга) (облако Astronomer) или раз-
вернуть в собственном кластере Kubernetes (Astronomer Enterprise).
По сравнению с Airflow, Astronomer также предоставляет дополни-
тельные инструменты, которые помогут с легкостью развернуть эк-
земпляры Airflow из пользовательского интерфейса или из их на-
страиваемого интерфейса командной строки. Интерфейс командной
строки также позволяет запускать локальные экземпляры Airflow для
разработки, что может упростить разработку ОАГ (при условии что
Kubernetes доступен на вашем компьютере, используемом для раз-
работки).
Будучи созданным на базе Kubernetes, Astronomer.io должен хоро-
шо интегрироваться с любыми рабочими процессами на основе Ku-
bernetes и Docker, к которым вы, возможно, привыкли. Это упрощает
(например) выполнение задач в контейнерах с помощью Kuberne
tesExecutor и KubernetesPodOperator. Также поддерживаются другие
режимы развертывания с использованием LocalExecutor или Cele
ryExecutor, что обеспечивает значительную гибкость при выполне-
нии заданий. Astronomer также позволяет настроить развертывание
Airflow, указав дополнительную ОС или зависимости Python, которые
422 Глава 15 Airflow и облако
1
Используется Cloud Composer для хранения ОАГ и журналов и т. д.
Выбор стратегии развертывания 423
1
Обратите внимание, что не обязательно применять Google Composer для
использования этих операторов, поскольку они отлично работают и в Air-
flow (при правильной настройке полномочий).
424 Глава 15 Airflow и облако
Как уже видно из этого краткого списка, при выборе решения для
развертывания кластера Airflow необходимо учитывать множество
факторов. Хотя мы не можем принять это решение за вас, мы надеем-
ся, что вы учтете данные рекомендации.
Резюме
Airflow состоит из нескольких компонентов (веб-сервер, плани-
ровщик, база метаданных, хранилище), которые необходимо реа-
лизовать с использованием облачных сервисов при развертывании
в облаке.
При развертывании Airflow с разными исполнителями (например,
их использовать.
1
Elastic Compute Service похож на Fargate, но требует, чтобы вы самостоя-
тельно управляли базовыми машинами.
2
Elastic Kubernetes Service, управляемое решение AWS для развертывания
и запуска Kubernetes.
3
Amazon RDS предлагает на выбор различные базы данных, такие как Post-
greSQL, MySQL и Aurora.
428 Глава 16 Airflow и AWS
Веб-сервер Планировщик
Airflow Airflow + рабочие
(Fargate) процессы
(Fargate)
NAT Gateway
Elastic Веб-сервер Airflow Хранилище
network (Fargate) метаданных Airflow
interface (Amazon RDS)
Интернет Интернет-шлюз
Балансировщик
нагрузки Локальное Планировщик Airlfow +
приложений хранилище ОАГ воркеры (Fargate)
(EFS)
NAT Gateway
Elastic Веб-сервер Airflow Хранилище
network (Fargate) метаданных Airflow
interface (Amazon RDS)
Интернет Интернет-шлюз
Задача
синхронизации
ОАГ (Lambda)
NAT Gateway
Elastic Веб-сервер Airflow Хранилище
network (Fargate) метаданных Airflow
interface (Amazon RDS)
Интернет Интернет-шлюз
Брокер
Воркеры Airflow сообщений
(Fargate) (SQS)
Хранилище Локальное
журналов (S3) хранилище
ОАГ (EFS)
1
Их можно установить в Airflow 2 с использованием пакета apacheairflow
provideramazon или в Airflow 1.10 с помощью пакета backport apacheair
flowbackportsizesamazon.
Хуки и операторы, предназначенные для AWS 433
hook = AwsBaseHook("my_aws_conn")
glue_client = hook.get_client_type("glue")
1
В следующем разделе мы приведем пример того, как получить эти сведения.
434 Глава 16 Airflow и AWS
Рис. 16.5 Создание подключения для хука AWS в Airflow. Обратите внимание, что ключ
доступа и секрет следует вводить в формате JSON в поле Extra, а не в поля Login и Password
(вопреки тому, что вы могли ожидать)
16.3.1 Обзор
В этом примере нас интересует использование бессерверных сервисов
в AWS (S3, Glue, Athena) для анализа данных фильмов, с которыми мы
Пример использования: бессерверное ранжирование фильмов с AWS Athena 435
API
рейтингов
рования;
436 Глава 16 Airflow и AWS
Рис. 16.8 Обзор созданного стека CloudFormation в консоли AWS. На этой странице
показано общее состояние стека. При необходимости она также предоставляет вам
элементы управления для обновления или удаления стека
Рис. 16.9 Обзор созданных стеком CloudFormation ресурсов. Данный вывод можно
использовать для перехода к различным ресурсам, созданным стеком
fetch_ratings = PythonOperator(
task_id="fetch_ratings",
python_callable=_fetch_ratings,
op_kwargs={
"api_conn_id": "movielens",
"s3_conn_id": "my_aws_conn",
"s3_bucket": "my_ratings_bucket",
},
)
1
См. главу 8 для получения дополнительной информации о создании соб-
ственных операторов.
Пример использования: бессерверное ранжирование фильмов с AWS Athena 441
class GlueTriggerCrawlerOperator(BaseOperator):
"""
Оператор, инициирующий запуск робота в AWS Glue.
Параметры
aws_conn_id
Соединие для подключения к AWS. Для этого необходимы соответствующие
полномочия (Glue:StartCrawler and Glue:GetCrawler) в AWS.
crawler_name
Имя робота.
region_name
Имя региона AWS, в котором находится робот.
kwargs
Любые аргументы kwargs, передаваемые оператору BaseOperator.
"""
@apply_defaults
def __init__(
self,
aws_conn_id: str,
crawler_name: str,
region_name: str = None,
**kwargs
):
super().__init__(**kwargs)
self._aws_conn_id = aws_conn_id
self._crawler_name = crawler_name
self._region_name = region_name
1
Этот пример, возможно, можно было бы сделать понадежнее, добавив
больше проверок неожиданных ответов, статусов и т. д.
442 Глава 16 Airflow и AWS
if crawler_state == "READY":
self.log.info("Crawler finished running")
break Останавливаемся, как только робот завершил
работу (отображено состояние READY)
trigger_crawler = GlueTriggerCrawlerOperator(
aws_conn_id="my_aws_conn",
task_id="trigger_crawler",
crawler_name="ratingscrawler",
)
rank_movies = AWSAthenaOperator(
task_id="rank_movies",
aws_conn_id="my_aws_conn",
database="airflow",
query="""
Пример использования: бессерверное ранжирование фильмов с AWS Athena 443
import pandas as pd
16.3.4 Очистка
После завершения этого примера не забудьте очистить все ресурсы,
которые вы создали в AWS, чтобы избежать ненужных затрат. Если
вы использовали шаблон CloudFormation для создания ресурсов, то
можете убрать большую часть, удалив стек. Обратите внимание, что
некоторые ресурсы, такие как бакеты S3, придется удалить вручную,
даже если вы используете шаблон, поскольку CloudFormation не по-
зволит автоматически удалить заполненные бакеты. Обязательно
проверьте, все ли созданные ресурсы были успешно удалены, уделяя
особое внимание проверке всех ресурсов, которые вы могли создать
вручную.
Резюме
Airflow можно развернуть в AWS с помощью таких сервисов, как
ECS/Fargate для запуска процессов планировщика и веб-сервера,
EFS/S3 для хранения и Amazon RDS для базы метаданных Airflow.
Airflow предоставляет множество хуков и операторов, предназна-
использовать.
Веб-сервер Планировщик
Airlfow (Azure Airflow + рабочие
App Service) процессы (ACI)
Планировщик
Airflow + рабочие
процессы (ACI)
Рис. 17.2 Проецируем наши компоненты на схему частной виртуальной сети. Частная
виртуальная сеть изолирует наши внутренние ресурсы (например, базу метаданных
и планировщик) от открытого доступа через интернет. Веб-сервер доступен в интернете
через Azure App Service, поэтому к нему можно получить удаленный доступ. Интеграция
с виртуальной сетью осуществляется с использованием частной конечной точки, чтобы
веб-сервер мог подключиться к базе метаданных. Стрелки указывают направление потока
информации между службами. Здесь сервисы хранения не ограничены виртуальной сетью,
но при желании такое возможно1
1
Доступность сервисов хранения может быть ограничена виртуальной
сетью с помощью комбинации частных конечных точек и правил бранд-
мауэра для обеспечения дополнительного уровня безопасности.
Планировщик
Airflow (ACI)
1
Можно установить в Airflow 2 с помощью пакета поставщиков apacheair
flowprovidersmicrosoftazure или в Airflow 1.10 с использованием паке-
та apacheairflowbackportsizesmicrosoftazure.
452 Глава 17 Airflow и Azure
17.3.1 Обзор
Хотя, вероятно, в Azure существует много разных способов выполне-
ния такого рода анализа, мы сосредоточимся на использовании службы
аналитики Azure Synapse, поскольку она позволяет выполнять бессер-
верные SQL-запросы с использованием возможности «SQL по запро-
су». Это означает, что нам нужно платить только за объем данных, ко-
торые мы обрабатываем в Azure Synapse, и нам не нужно беспокоиться
о расходах и сопровождении используемых вычислительных ресурсов.
Чтобы реализовать наш пример с помощью Synapse, необходимо
выполнить следующие шаги.
1 Извлечь рейтинги за определенный месяц из API рейтингов
и загрузить их в Azure Blob Storage для дальнейшего анализа.
2 Использовать Azure Synapse, чтобы выполнить SQL-запрос, ко-
торый ранжирует наши фильмы. Полученный список ранжиро-
ванных фильмов будет записан обратно в Azure Blob Storage для
дальнейшего потребления.
Пример: бессерверное ранжирование фильмов с Azure Synapse 453
API
рейтингов
Рис. 17.6 Первая страница мастера создания рабочего пространства Synapse. Обязательно
укажите правильную группу ресурсов и имя вашей рабочей области. Чтобы настроить
хранилище, нажмите Create new (Создать новое) для учетной записи и файловой системы
и введите их имена
Рис. 17.10 Получение имени учетной записи и ключа для доступа к учетной записи
хранилища BLOB-объектов из Airflow
Пример: бессерверное ранжирование фильмов с Azure Synapse 457
fetch_ratings = PythonOperator(
458 Глава 17 Airflow и Azure
task_id="upload_ratings",
python_callable=_upload_ratings,
op_kwargs={
"wasb_conn_id": "my_wasb_conn",
"container": "ratings"
},
)
Рис. 17.11 Создание подключения Airflow для учетной записи Azure Blob Storage
с помощью имени учетной записи и ключа, полученных на портале Azure
1
Можно установить в Airflow 2 с помощью пакета apacheairflowprovider
odbc или в Airflow 1.10 с использованием пакета apacheairflowbackport
sizesodbc.
Пример: бессерверное ранжирование фильмов с Azure Synapse 459
query = RANK_QUERY.format(
year=year, Добавляем параметры запуска
month=month, в SQL-запрос
blob_account_name=blob_account_name,
blob_container=ratings_container,
)
logging.info(f"Executing query: {query}") Подключаемся к Synapse
с помощью хука ODBC
odbc_hook = OdbcHook(
odbc_conn_id,
driver="ODBC Driver 17 for SQL Server",
)
with odbc_hook.get_conn() as conn:
with conn.cursor() as cursor:
cursor.execute(query)
Выполняем запрос и получаем
rows = cursor.fetchall() результирующие строки
colnames = [field[0] for field in cursor.description]
1
Обратите внимание, что для этого необходимо установить соответствую-
щие драйверы ODBC. Этот драйвер уже должен быть установлен в нашем
образе Docker. Если вы не используете его, то можете найти дополнитель-
ные сведения о том, как установить драйверы самостоятельно, на сайте
Microsoft. Убедитесь, что вы используете правильную версию для своей
операционной системы.
Пример: бессерверное ранжирование фильмов с Azure Synapse 461
import pandas as pd
RANK_QUERY = ...
with DAG(
dag_id="01_azure_usecase",
description="DAG demonstrating some Azure hooks and operators.",
start_date=dt.datetime(year=2019, month=1, day=1),
end_date=dt.datetime(year=2019, month=3, day=1), Задаем даты начала
schedule_interval="@monthly", и окончания в соответствии
default_args={"depends_on_past": True}, с набором рейтинговых данных
) as dag:
Используем depends_on_past, чтобы
fetch_ratings = PythonOperator(...)
избежать выполнения запросов до
rank_movies = PythonOperator(...)
того, как будут загружены архивные
upload_ratings >> rank_movies
данные (что может привести
к неполным результатам)
Рис. 17.13 Успешное создание рейтингов фильмов с помощью Azure Synapse в ОАГ
17.3.4 Очистка
После того как вы закончите работать с этим примером в Azure Syn-
apse, можете избавиться ото всех созданных ресурсов, удалив группу
ресурсов, которую мы создали вначале. Для этого откройте страницу
группы ресурсов Overview (Обзор) на портале Azure и щелкните De-
lete resource group (Удалить группу ресурсов) (рис. 17.14). Подтвер-
дите удаление.
464 Глава 17 Airflow и Azure
Рис. 17.14 Очистка созданных ресурсов путем удаления соответствующей группы ресурсов
Резюме
Airflow можно развернуть в Azure с помощью таких сервисов, как
ACI и App Service для запуска процессов планировщика и веб-
сервера, Azure File и Blob Storages для хранения файлов и Azure SQL
Database для базы метаданных Airflow.
Airflow предоставляет ряд хуков и операторов специально для
использовать.
NOTES:
Thank you for installing Airflow!
➥ You can now access your dashboard(s) by executing the following command(s)
and visiting the corresponding port at localhost in your browser:
ClusterIP LoadBalancer
Прокси Балансировщик
нагрузки
GCP
Прокси
GKE Контроллер Контроллер
Ingress Ingress
GKE
Под Под Под
pository (GCR);
мы можем настроить удаленное журналирование в GCS (описано
в разделе 12.3.4).
➥ You can now access your dashboard(s) by executing the following command(s)
and visiting the corresponding port at localhost in your browser:
GCP-проект
Пиринг*
Cloud SQL
Рис. 18.6 Пример схемы сети GCP с Airflow, работающим в GKE, Cloud SQL
для базы метаданных и веб-сервер Airflow, доступный через балансировщик
нагрузки
Подпроцесс
Воркер Celery
Подпроцесс
Подпроцесс
База данных …
Подпроцесс
Веб-сервер
Подпроцесс
Воркер Celery
Подпроцесс
Подпроцесс
…
Планировщик Очередь
Подпроцесс
Подпроцесс
Воркер Celery
Подпроцесс
ОАГ
Подпроцесс
…
Подпроцесс
helm install \
set image.keda=docker.io/kedacore/keda:1.2.0 \
➥ set image.metricsAdapter=docker.io/kedacore/kedametricsadapter:1.2.0 \
namespace keda \
keda kedacore/keda
476 Глава 18 Airflow в GCP
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/...",
"client_x509_cert_url": "https://...iam.gserviceaccount.com"
}
Указываем путь
к файлу ключа
в формате JSON
Указываем
идентификатор
GCP-проекта
Выбираем «Google
Cloud», чтобы настроить
дополнительные поля,
относящиеся к GCP
Заполняем поле
«Keyfile JSON»
import pandas as pd
from airflow.models import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.google.cloud.hooks.gcs import GCSHook
482 Глава 18 Airflow в GCP
dag = DAG(
"gcp_movie_ranking",
start_date=datetime.datetime(year=2019, month=1, day=1),
end_date=datetime.datetime(year=2019, month=3, day=1),
schedule_interval="@monthly",
default_args={"depends_on_past": True},
)
api_hook = MovielensHook(conn_id=api_conn_id)
ratings = pd.DataFrame.from_records(
api_hook.get_ratings_for_month(year=year, month=month),
columns=["userId", "movieId", "rating", "timestamp"],
)
logging.info(f"Fetched {ratings.shape[0]} rows") Сначала извлекаем
и записываем результаты
with tempfile.TemporaryDirectory() as tmp_dir: в локальный файл
tmp_path = path.join(tmp_dir, "ratings.csv")
ratings.to_csv(tmp_path, index=False)
Инициализируем
подключение # Загружаем файл в GCS.
к GCS logging.info(f"Writing results to ratings/{year}/{month:02d}.csv")
gcs_hook = GCSHook (gcp_conn_id)
Бакет GCS, в который
gcs_hook.upload(
будет загружен файл
Загружаем bucket_name=gcs_bucket,
локальный файл object_name=f"ratings/{year}/{month:02d}.csv",
в GCS filename=tmp_path,
Ключ GCS, в который будут
)
записываться данные
fetch_ratings = PythonOperator(
task_id="fetch_ratings",
python_callable=_fetch_ratings,
op_kwargs={
"api_conn_id": "movielens",
"gcp_conn_id": "gcp",
"gcs_bucket": os.environ["RATINGS_BUCKET"],
},
dag=dag,
)
Если все прошло успешно, то теперь у нас есть данные в бакете GCS,
показанной на рис. 18.13.
Пример использования: бессерверный рейтинг фильмов в GCP 483
import_in_bigquery = GCSToBigQueryOperator (
task_id="import_in_bigquery",
bucket="airflow_movie_ratings",
source_objects=[
"ratings/{{ execution_date.year }}/{{ execution_date.month }}.csv"
],
Создаем таблицу,
source_format="CSV",
если ее не существует
create_disposition="CREATE_IF_NEEDED",
write_disposition="WRITE_TRUNCATE",
Перезаписываем данные секции,
bigquery_conn_id="gcp",
если они уже существуют
autodetect=True,
484 Глава 18 Airflow в GCP
destination_project_dataset_table=(
Пытаемся "airflowpipelines:",
автоматически "airflow.ratings${{ ds_nodash }}",
определить ), Значение после символа $
схему dag=dag, определяет секцию для записи
) под названием «декоратор секции»
import_in_bigquery = GCSToBigQueryOperator (
task_id="import_in_bigquery",
bucket="airflow_movie_ratings",
source_objects=[
"ratings/{{ execution_date.year }}/{{ execution_date.month }}.csv"
],
source_format="CSV",
create_disposition="CREATE_IF_NEEDED",
write_disposition="WRITE_TRUNCATE",
bigquery_conn_id="gcp", Пропускаем строку заголовка
skip_leading_rows=1,
schema_fields=[ Определяем схему вручную
{"name": "userId", "type": "INTEGER"},
{"name": "movieId", "type": "INTEGER"},
{"name": "rating", "type": "FLOAT"},
{"name": "timestamp", "type": "TIMESTAMP"},
],
destination_project_dataset_table=(
"airflowpipelines:",
"airflow.ratings${{ ds_nodash }}",
),
dag=dag,
)
query_top_ratings = BigQueryExecuteQueryOperator (
task_id="query_top_ratings",
destination_dataset_table=(
"airflow-pipelines:", Таблица BigQuery
"airflow.ratings_{{ ds_nodash }}",
),
sql="""SELECT
movieid,
AVG(rating) as avg_rating,
COUNT(*) as num_ratings
FROM airflow.ratings
WHERE DATE(timestamp) <= DATE("{{ ds }}")
GROUP BY movieid
ORDER BY avg_rating DESC SQL-запрос для выполнения
""",
write_disposition="WRITE_TRUNCATE",
create_disposition="CREATE_IF_NEEDED",
bigquery_conn_id="gcp",
dag=dag,
)
extract_top_ratings = BigQueryToGCSOperator(
task_id="extract_top_ratings",
source_project_dataset_table=(
"airflow-pipelines:", Таблица BigQuery для извлечения
"airflow.ratings_{{ ds_nodash }}",
),
destination_cloud_storage_uris=(
"gs://airflow_movie_results/{{ ds_nodash }}.csv"
Извлекаем целевой путь
Пример использования: бессерверный рейтинг фильмов в GCP 487
),
export_format="CSV",
bigquery_conn_id="gcp",
dag=dag,
)
delete_result_table = BigQueryTableDeleteOperator(
task_id="delete_result_table",
deletion_dataset_table=(
Таблица BigQuery,
"airflow-pipelines:",
которую нужно удалить
"airflow.ratings_{{ ds_nodash }}",
),
bigquery_conn_id="gcp",
dag=dag,
)
Рис. 18.17 Полный ОАГ для скачивания рейтингов, загрузки и обработки с использованием
BigQuery
Резюме
Самый простой способ установить и запустить Airflow в GCP – это
GKE, используя диаграмму Helm в качестве отправной точки.
Airflow предоставляет множество привязок и операторов для GCP,
$ docker-compose up –build
планировщик Airflow;
$ dockercompose up build d
Prometheus;
Flower;
Redis.
$ docker ps
CONTAINER ID IMAGE ... NAMES
d7c68a1b9937 apache/airflow:2.0.0python3.8 ... chapter02_scheduler_1
557e97741309 apache/airflow:2.0.0python3.8 ... chapter02_webserver_1
742194dd2ef5 postgres:12alpine ... chapter02_postgres_1
$ dockercompose down v
$ docker ps a
$ docker rm <container_id>
Запуск окружения Docker 493
и импортируйте airflow.providers.postgres.operators.postgres.
PostgresOperator.
match: "airflow.dag_processing.total_parse_time"
help: Number of seconds taken to process all DAG files
name: "airflow_dag_processing_time"
match: "airflow.dag.*.*.duration"
name: "airflow_task_duration"
labels:
dag_id: "$1"
task_id: "$2"
match: "airflow.dagbag_size"
help: Number of DAGs
name: "airflow_dag_count"
Структура пакета Airflow 2 499
match: "airflow.dag_processing.import_errors"
help: The number of errors encountered when processing DAGs
name: "airflow_dag_errors"
match: "airflow.dag.loadingduration.*"
help: Loading duration of DAGs grouped by file. If multiple DAGs are found
in one file, DAG ids are concatenated by an underscore in the label.
name: "airflow_dag_loading_duration"
labels:
dag_ids: "$1"
match: "airflow.dag_processing.last_duration.*"
name: "airflow_dag_processing_last_duration"
labels:
filename: "$1"
match: "airflow.dag_processing.last_run.seconds_ago.*"
name: "airflow_dag_processing_last_run_seconds_ago"
labels:
filename: "$1"
match: "airflow.dag_processing.last_runtime.*"
name: "airflow_dag_processing_last_runtime"
labels:
filename: "$1"
match: "airflow.dagrun.dependencycheck.*"
name: "airflow_dag_processing_last_runtime"
labels:
dag_id: "$1"
match: "airflow.dagrun.duration.success.*"
name: "airflow_dagrun_success_duration"
labels:
dag_id: "$1"
match: "airflow.dagrun.schedule_delay.*"
name: "airflow_dagrun_schedule_delay"
labels:
dag_id: "$1"
match: "airflow.executor.open_slots"
help: The number of open executor slots
name: "airflow_executor_open_slots"
match: "airflow.executor.queued_tasks"
help: The number of queued tasks
name: "airflow_executor_queued_tasks"
match: "airflow.executor.running_tasks"
help: The number of running tasks
name: "airflow_executor_running_tasks"