- Зачем: - нужен единый штатный путь исполнения SQL и программных запросов ClickHouse из Airflow. - Что: - добавлен официальный провайдер и подключения типа `clickhouse`. - SQL-файлы переданы `SQLExecuteQueryOperator`, а программные запросы — `ClickHouseHook`. - разбор заказов собран в одном файле, DDL разделён по задачам, документация и смоук обновлены. - Проверка: - выполнены `make lint` и `make config-test`. - собран образ, выполнены `pip check`, `make rebuild-storage`, `make smoke` (21/21), `make check-clickhouse` (10/10) и `make check-services` (7/7).
21 KiB
Жизненный цикл мира: создание и пересоздание ведёт Airflow
Дата: 21 августа 2026 года. Решение и доводы — ADR 0013; здесь устройство, которое из него следует.
Задача
До этой работы начало оси мира собирали трое. Compose создавал топики, применял DDL и двумя одноразовыми службами заливал восемь дней. Shell-сценарии ждали, пока события доедут до ODS, и разово дёргали даг приёма заказов. Airflow вёл мир после стартовой заливки.
Граница владения не совпадала с процессом, и это видно по следствиям. Успешная
отправка в Kafka выдавалась за созданный мир: world-init завершался сразу
после записи в топик, до приёма сообщений хранилищем.
Повторный make up смешивал подъём служб с повторной заливкой данных. Узкая
пересборка хранилища сохраняла в Kafka сообщения и офсеты прежнего мира, потому
что чинила только ClickHouse. Позиция пульта правилась снаружи Airflow — тем,
кто миром не управляет.
Чему учит переезд. Менти видит один наблюдаемый граф, которому принадлежат
прикладная схема, данные мира и позиция модельного времени, — и Compose,
который держит только инфраструктуру. Зелёный world_initialize значит ровно
одно: схема создана, оба источника приняты, следующий день известен.
Решение
Граница владения одна: Compose отвечает за инфраструктуру; Airflow — за прикладной мир.
Compose держит долгоживущие службы, сети, тома, объявленные файлом учётные записи ClickHouse, инициализацию метаданных Airflow и инициализацию Superset.
Airflow держит прикладные топики Kafka, канонический DDL прикладных баз stg,
ods, dds, dm, dic, данные стартового мира, их приём и переменную
world_position.
Одноразовых служб kafka-init, clickhouse-init, world-init и
world-snapshots в графе Compose не остаётся. Ни профиля, ни вызова через
compose run им не оставляют: спрятанная за профилем служба — тот же второй
оркестратор, только менее заметный.
make инфраструктурой и ограничивается: поднимает стенд, зовёт нужный даг
через REST API Airflow и ждёт результата. Shell больше не применяет DDL, не
запускает генератор и не правит позицию.
Учётная запись жизненного цикла
Прикладную структуру создаёт и удаляет отдельная учётная запись lifecycle с
ролью lifecycle_owner, объявленная тем же файлом, что остальные учётные записи
(ADR 0007). Ей разрешено администрировать
прикладные базы — заводить и удалять их и объекты внутри, — читать их и
выполнять кластерный DDL. Ей разрешено чтение system.settings: без него
драйвер clickhouse-connect под официальным провайдером Airflow не открывает
соединение. Административные запросы к системным базам ей запрещены.
etl при этом не расширяется. Право удаления баз нужно редкой операции
восстановления и остаётся у lifecycle. Граница проверяема: при лишнем запросе
менти увидит в ошибке имя учётной записи и недостающее право.
Что Airflow получает от репозитория
Каталог /opt/airflow/world — то, что нужно для создания мира: generator/,
data/ и корневой .dockerignore смонтированы только для чтения. Из первого
собирается канонический образ генератора, из второго читается опись мира, а
последний не пускает в контекст сборки локальные окружения и кэши. Запросы
стенда приезжают прежним монтированием sql/.
Меняющиеся API сверены через Context7 и затем с установленными пакетами
стенда. Выбраны публичный REST API v2 и ждущий TriggerDagRunOperator Airflow,
сборка образа через DockerHook.get_conn().build(), futures AdminClient для
Kafka, SQLExecuteQueryOperator для исполняемых SQL-файлов ClickHouse и
ClickHouseHook для запросов, чей результат обрабатывает Python. Оператор сам
шаблонизирует файл и при split_statements=True исполняет выражения по порядку;
собственный разбор SQL дагу больше не нужен.
Контракты дагов
Оба дага публичные: без расписания, не на паузе, max_active_runs = 1. Их
запускают руками из интерфейса и командой make через REST API.
world_initialize — создать мир, если его нет
Даг безопасен по построению: на готовом мире он ничего не меняет.
- Читает
world_positionи решает, что делать дальше: инициализировать, успешно завершиться без изменений или покраснеть. Разбор состояний — в таблице ниже. - Создаёт топики:
hitsс двумя партициями,ordersс одной. Топики идут до DDL: чтец ClickHouse начинает читать топик, когда над ним появляется матвью, и топик, созданный брокером сам, приехал бы с одним разделом (ADR 0008). - Применяет канонический DDL из
sql/ddl/по порядку имён файлов, с ноды 1, учётной записьюlifecycle. Каждый файл — отдельная последовательная задачаSQLExecuteQueryOperator; выражения внутри неё исполняются по порядку сsplit_statements=True. Красная задача называет файл и не пускает последующие. Правило «матвью приёма создаётся последней» остаётся за раскладкой файлов и не переезжает в даг. - Собирает канонический образ генератора. Иначе мир мог бы приехать из старого кода: сборкой образа при подъёме больше никто не занимается.
- Играет стартовые дни одним прогоном
batchтого же образа. - Ждёт, пока события доедут до ODS. Условие ожидания то же, что у прежнего
сценария, и оно точное: сумма строк
ods.event_distза даты описи и строкods.event_errors_distне меньше суммы событий изdata/world-inventory.json. «Что-то приехало» условием не считается. - Отправляет слепки заказов вторым прогоном образа — со сдвигом, дни 0…6 (заказы в стартовом мире).
- Дёргает
orders_ingestждущим триггером и зеленеет только вслед за ним. - Последней успешной задачей ставит позицию в
WORLD_STARTING_DAYS.
Порядок задач связывает проверки: каждый шаг подтверждён предыдущим, а красная задача называет место остановки.
world_recreate — снести мир и создать заново
Сначала даг выполняет всю разрушительную часть, затем делегирует создание мира.
- Ставит позицию в
-1: прежний мир объявлен недействительным раньше, чем с ним что-то произошло. - Удаляет прикладные базы ClickHouse
stg,dm,dds,ods,dic— первым делом, чтобы чтецы Kafka перестали существовать вместе с ними. - Удаляет позиции групп потребителей
clickstream_hitsиclickstream_orders, затем топикиhitsиorders. - Ставит позицию в
0— только после успешной очистки. - Дёргает
world_initializeждущим триггером и ждёт его результата.
Общего модуля с шагами двух дагов нет. Переиспользование здесь — это делегирование: пересборка вызывает создание мира целиком. Такой модуль стал бы третьим местом с той же последовательностью, а первое же расхождение между дагами пришлось бы прятать в его параметрах.
Сторожа у работников мира
world_next_day и world_live_day краснеют, если позиции нет либо она меньше
WORLD_STARTING_DAYS. Отказ называет прочитанное значение и следующий шаг:
поднять стенд или пересобрать хранилище. Выключатель world_live своего
сторожа не получает — он краснеет вслед за работником.
orders_ingest сторожа не получает вовсе: его зовёт сама инициализация, когда
готового мира ещё нет.
Состояния позиции
world_position остаётся единственным состоянием пульта.
| Позиция | world_initialize |
world_recreate |
Работники |
|---|---|---|---|
| переменной нет | создаёт мир | очищает и создаёт | красный отказ |
-1 |
красный отказ: очистка не завершена, повторите make rebuild-storage |
очищает и создаёт | красный отказ |
0 |
создаёт мир | очищает и создаёт | красный отказ |
WORLD_STARTING_DAYS и больше |
мир уже есть: пишет это в журнал и зеленеет | очищает и создаёт | работают |
Значения между 1 и WORLD_STARTING_DAYS штатно не возникают: позиция
становится либо -1, либо 0, либо номером сыгранного дня за стартовым миром.
Попав на такое значение, world_initialize краснеет тем же отказом, что на
-1, и называет прочитанное число.
Поведение при отказах
Пересборка упала на очистке. Позиция остаётся -1. Обычный make up эту
поломку не заглаживает: world_initialize краснеет и говорит, что мир
недействителен и пересборку надо повторить. Стенд обязан назвать незавершённую
очистку вслух и не продолжать работу молча.
Очистка прошла, потомок не доработал. Позиция остаётся 0. Инициализация
детерминирована, поэтому следующий make up просто пробует её заново — с
того же места и с тем же результатом.
Позицию в WORLD_STARTING_DAYS ставит только успех обоих источников.
События доехали, слепки приняты, orders_ingest зелёный — тогда и только тогда
мир считается созданным.
Порядок очистки Kafka входит в контракт. Пока живы чтецы ClickHouse, они состоят в группах потребителей, и удаление группы недопустимо. Поэтому сначала удаляются базы, затем топики.
Позицию удалили руками на живом мире. Даг честно зальёт мир заново: сырьё в STG удвоится, ODS схлопнет повтор дедупликацией. Условие ожидания на непустом ODS истинно сразу — этим оно и ограничено, обещание «доехала именно эта заливка» ни один счёт дать не может. Сторожа на этот случай нет: ошибку менти делает себе сам и тут же видит.
Слоты Airflow. Их четыре, и вложенные ожидания в них укладываются, пока
пересборка не наложилась на движение мира: world_recreate ждёт
world_initialize, тот ждёт orders_ingest — три слота из четырёх. Одновременно
работающий выключатель добавляет свои и загоняет стенд в тупик ожиданий.
Поэтому пересборка — явная операция обслуживания: оператор ставит world_live
на паузу и даёт работникам доиграть. Предусловие остаётся в документации;
механизм блокировок стоил бы дороже этой строки.
Границы
make upне мигратор схемы. На готовом мире изменённый DDL не применяется. Обновляет схему явная пересборкаworld_recreateлибо адресная правка объекта в разработке. Размен принят с открытыми глазами.- Ни блокировок, ни статуса, ни автоматической паузы. Предусловие обслуживания названо словами.
etlне расширяется,orders_ingestне получает сторожа.make cleanсохраняет свой смысл: полное удаление постоянного состояния стенда. Ни в пересборку, ни в обычную проверку он не входит.make generate-batchиmake generate-liveостаются — с них снимаются унаследованные зависимости от исчезающих одноразовых служб.- Содержание DDL, код генератора и устройство слоёв не меняются.
Проверка
Цели make — обычные: config-test и lint без стенда, smoke,
check-clickhouse и check-services на поднятом. Сверх них проверяются
сценарии, которых у стенда раньше не было.
- Повторный
make upна живом мире проходит быстро и ничего не меняет: позиция та же, счётчикиcheck-clickhouseте же, в журнале дага сказано, что мир уже есть. make rebuild-storageвозвращает канонический мир: позиция проходит-1→0→WORLD_STARTING_DAYS, в ODS ровно даты описи, топики созданы заново, офсеты групп идут с нуля. Цена прогона называется числом и сверяется с прежними 2 м 25 с.- Красные пути проверяются своими отказами. Сторож работника при
отсутствующей, нулевой и отрицательной позиции;
world_initializeна-1; оборванная очистка, оставляющая-1. Проверка, не видевшая своей поломки, доказывает только то, что умеет печатать «ЗЕЛЁНО». - Отказ в правах читается. Учётная запись
lifecycleприменяет весь DDL без единой ошибки прав, а административный запрос к системной базе ей отклоняют — и текст отказа называет учётку. - Один холодный
make clean && make upоправдан на итоговой приёмке и только этой работой: она меняет самый первый подъём стенда, а его иначе не увидеть. Обобщать этот случай не нужно — обычная проверка по-прежнему обходится узкой областью очистки.
Замеры холодного и повторного make up и пересборки записываются в
карту проверок, раздел «Что проверено».
Отклонённые варианты
- Спрятать одноразовые службы за профилем или звать их
compose run. Дешевле всего в правках, но оставляет двух оркестраторов одного процесса — ровно то, ради чего затевался переезд. - Оставить применение DDL и заливку мира в shell, а Airflow отдать только позицию. Тогда «один наблюдаемый процесс» остаётся обещанием: половина графа живёт в журнале терминала.
- Вынести общие шаги двух дагов в отдельный модуль. Третье место для той же последовательности; расхождение дагов пряталось бы в его параметрах.
- Применять изменённый DDL при каждом
make up. Это мигратор схемы; ему нужен отдельный разговор, в граф создания мира он не помещается. - Выдать права на удаление баз учётной записи
etl. Отвергнуто ещё ADR 0013: конвейер не получает административные права из-за редкой операции. - Завести блокировку или статус обслуживания, чтобы пересборка не сталкивалась с движением мира. Механизм на редкий ручной случай дороже предусловия, названного словами.
- Сторож на
orders_ingest. Даг зовут до того, как мир существует; сторож запретил бы саму инициализацию.