feat(airflow): подключён официальный провайдер ClickHouse
- Зачем: - нужен единый штатный путь исполнения 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).
This commit is contained in:
1 parent
587e392220
commit
8d82679bc1
17 files changed
+393
-438
No files matched your search
@@ -32,13 +32,18 @@ JSON-файл отдельного тома `airflow_auth`. Новый имен
|
||||
|
||||
Airflow получает два подготовленных подключения к `clickhouse-01`: рабочее
|
||||
`clickhouse_default` и административное `clickhouse_lifecycle` для дагов
|
||||
жизненного цикла мира ([ADR 0013](0013-world-lifecycle.md)). Superset получает
|
||||
подключение `clickhousedb://` к `clickhouse-02`. Провайдер ClickHouse для
|
||||
Airflow не нужен: локальный образ содержит прямой клиент.
|
||||
Пробник создаёт служебные таблицы `ON CLUSTER`, пишет на первой ноде и через
|
||||
`remote` читает `Distributed` на второй. Разные ноды сохраняют учебную
|
||||
ловушку: забытый `ON CLUSTER` проявится в Superset, даже если операция Airflow
|
||||
на первой ноде прошла успешно.
|
||||
жизненного цикла мира ([ADR 0013](0013-world-lifecycle.md)). Оба имеют тип
|
||||
`clickhouse`, который регистрирует официальный провайдер
|
||||
`apache-airflow-providers-clickhousedb`. SQL-файлы исполняет общий
|
||||
`SQLExecuteQueryOperator`, а программные запросы идут через `ClickHouseHook`.
|
||||
Сам провайдер работает через `clickhouse-connect`; его версия закреплена
|
||||
отдельно, чтобы сборка не меняла транспорт незаметно.
|
||||
|
||||
Superset получает подключение `clickhousedb://` к `clickhouse-02`. Пробник
|
||||
Airflow создаёт служебные таблицы `ON CLUSTER`, пишет на первой ноде и через
|
||||
`remote` читает `Distributed` на второй. Разные ноды сохраняют учебную ловушку:
|
||||
забытый `ON CLUSTER` проявится в Superset, даже если операция Airflow на первой
|
||||
ноде прошла успешно.
|
||||
|
||||
Клиент Kafka также добавлен прямо в образ Airflow. Официальный провайдер
|
||||
использует тот же `confluent-kafka`, а пробнику не нужны его подключение,
|
||||
@@ -84,10 +89,18 @@ Prometheus читает встроенные точки метрик двух с
|
||||
Оттуда взяты `airflow.sdk`, обязательный отдельный обработчик DAG, возможность
|
||||
не запускать triggerer и проверка конкретных компонентов здоровья.
|
||||
|
||||
Для интеграционных пробников проверены `Connection.get` в публичном
|
||||
`airflow.sdk`, запросы через `clickhouse-connect` и состав официального
|
||||
провайдера Kafka. Выбран прямой `confluent-kafka`: провайдер строит свои
|
||||
подключения и обёртки поверх него, которые двум коротким пробникам не нужны.
|
||||
Для ClickHouse через Context7 и официальную документацию Airflow проверены
|
||||
`ClickHouseHook`, тип подключения `clickhouse` и работа
|
||||
`SQLExecuteQueryOperator` с SQL-файлом, параметрами и несколькими выражениями.
|
||||
Закреплён `apache-airflow-providers-clickhousedb==1.0.0`: на 21 августа
|
||||
2026 года это последний стабильный выпуск; `1.0.1` опубликован только как
|
||||
предварительная версия. Зависимость провайдера допускает любой
|
||||
`clickhouse-connect>=1.3.0`, поэтому прежний `clickhouse-connect==1.6.0`
|
||||
оставлен прямой зависимостью образа.
|
||||
|
||||
Для Kafka проверен состав официального провайдера. Выбран прямой
|
||||
`confluent-kafka`: провайдер строит свои подключения и обёртки поверх него,
|
||||
которые двум коротким пробникам не нужны.
|
||||
|
||||
Драйверы и строки подключения проверены по документации Superset 6.1.0:
|
||||
[подключения к базам](https://superset.apache.org/user-docs/6.1.0/databases/),
|
||||
|
||||
@@ -39,24 +39,27 @@
|
||||
конца прогона. Все строки получают `_load_id`, равный `run_id` Airflow.
|
||||
`_load_ts` вычисляется при этой записи и дальше переносится без пересчёта.
|
||||
Запрос забора лежит в [`sql/stg/orders_raw_load.sql`](../../../sql/stg/orders_raw_load.sql):
|
||||
даг задаёт порядок и параметры, а преобразование остаётся в SQL своего слоя.
|
||||
`SQLExecuteQueryOperator` задаёт подключение и передаёт `run_id` как параметр
|
||||
ClickHouse `load_id`, а преобразование остаётся в SQL своего слоя.
|
||||
|
||||
Один следующий `task_id` отвечает за весь переход STG → ODS. Внутри него два
|
||||
последовательных `INSERT SELECT` читают неизменный срез по `_load_id`: первый
|
||||
пишет годные строки в `ods.order`, второй — брак в `ods.order_errors`.
|
||||
Транзакции между запросами нет. При частичном сбое Airflow повторяет весь
|
||||
`task_id`; одинаковые исходные строки и служебные метки не вычисляются заново.
|
||||
Запросы лежат рядом с целями:
|
||||
[`order_load.sql`](../../../sql/ods/order_load.sql) и
|
||||
[`order_errors_load.sql`](../../../sql/ods/order_errors_load.sql).
|
||||
Обе вставки лежат в одном самодостаточном файле
|
||||
[`order_parse.sql`](../../../sql/ods/order_parse.sql). Airflow сначала
|
||||
шаблонизирует файл, затем делит получившийся SQL на выражения и исполняет их по
|
||||
порядку в одной задаче `parse_batch`.
|
||||
|
||||
Условия запросов взаимодополняющие: один общий предикат определяет брак, а
|
||||
годная ветвь использует его буквальное отрицание. Все функции предиката
|
||||
Условия запросов взаимодополняющие: один общий предикат определяет годную
|
||||
строку, а ветвь брака использует его буквальное отрицание. Все функции предиката
|
||||
возвращают результат без исключения, а сам предикат всегда заканчивается в
|
||||
`true` или `false`, не в `NULL`. Постоянный классификатор между STG и ODS для
|
||||
этого не нужен. Обе ветви включают один файл
|
||||
[`_order_wire_contract.sql`](../../../sql/ods/_order_wire_contract.sql), поэтому
|
||||
предикат нельзя случайно исправить только в одной из них.
|
||||
этого не нужен. Предикат объявлен локальным макросом Jinja внутри
|
||||
`order_parse.sql` и разворачивается в оба выражения. Макрос убирает
|
||||
дублирование, но не вводит отдельный фрагмент, который нельзя исполнить
|
||||
самостоятельно.
|
||||
|
||||
## Граница строгого приёма
|
||||
|
||||
@@ -178,6 +181,11 @@ JSON-объектом с точным набором ключей: `order_id`, `
|
||||
`20260608-0007`, который прежде приходил с отрицательным итогом, теперь
|
||||
`5490.00 − 823.50 + 299.00 = 4965.50`.
|
||||
|
||||
При исполнении #124 21 августа 2026 года адресная пересборка мира прошла через
|
||||
`SQLExecuteQueryOperator`: `pull_batch` и `parse_batch` завершились успешно. В
|
||||
`system.query_log` на первой ноде осталось ровно по одному завершённому запросу
|
||||
пользователя `etl` для забора в STG, записи годных строк и записи брака.
|
||||
|
||||
Забор из Kafka в STG снят на живом стенде 18 августа 2026 года при исполнении
|
||||
#93: одно прямое чтение приносит весь слепок дня, метаданные доставки доступны,
|
||||
офсеты коммитятся, а сбой забора не двигает позицию мира. Числа — [ADR
|
||||
|
||||
@@ -450,16 +450,18 @@ kafka_offset)`: смотрят такую таблицу от класса, а
|
||||
|
||||
Исполняемые дагами запросы лежат по правилу
|
||||
`sql/<слой>/<объект>_<роль>.sql`. Слой — слой цели запроса: забор заказов в
|
||||
сырьё живёт в `sql/stg/orders_raw_load.sql`, две ветви разбора — в `sql/ods/`.
|
||||
Даг называет файлы, передаёт параметры и задаёт порядок выполнения, но не
|
||||
хранит текст преобразований. Общий фрагмент начинается с подчёркивания: файл
|
||||
`sql/ods/_order_wire_contract.sql` сам не исполняется, его включают обе ветви
|
||||
разбора.
|
||||
сырьё живёт в `sql/stg/orders_raw_load.sql`, разбор — в
|
||||
`sql/ods/order_parse.sql`. Даг называет файлы, передаёт параметры и задаёт
|
||||
порядок выполнения, но не хранит текст преобразований. Два выражения разбора
|
||||
делят локальный макрос Jinja в том же файле; подключаемого SQL-фрагмента между
|
||||
ними нет.
|
||||
|
||||
Airflow читает и собирает эти файлы штатным шаблонизатором при исполнении
|
||||
задачи. Каталог `sql/` смонтирован во все его службы только для чтения. Поэтому
|
||||
обработчик дагов не зависит от наличия SQL на машине при разборе Python, а
|
||||
планировщик видит те же файлы при исполнении задачи.
|
||||
SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. При разборе Python-кода
|
||||
обработчик DAG читает список DDL-файлов, загружает содержимое каждого
|
||||
названного SQL-файла и кладёт его в сериализованный граф. При исполнении Airflow
|
||||
разворачивает в этом тексте Jinja; параметры ClickHouse передаются отдельно и
|
||||
не подставляются строкой. Каталог `sql/` смонтирован во все службы Airflow
|
||||
только для чтения.
|
||||
|
||||
### DDL
|
||||
|
||||
@@ -489,8 +491,10 @@ Airflow читает и собирает эти файлы штатным шаб
|
||||
отладке.
|
||||
|
||||
Файлы применяет даг `world_initialize` с ноды 1 отдельной учётной записью
|
||||
`lifecycle`: всё `ON CLUSTER`, кроме чтеца заказов — он объявлен только на этой
|
||||
ноде. Перед DDL даг создаёт `hits` с двумя партициями и `orders` с одной
|
||||
`lifecycle`: один файл — одна последовательная задача
|
||||
`SQLExecuteQueryOperator`, а выражения внутри разделяет сам оператор. Всё
|
||||
`ON CLUSTER`, кроме чтеца заказов — он объявлен только на этой ноде. Перед DDL
|
||||
даг создаёт `hits` с двумя партициями и `orders` с одной
|
||||
([ADR 0008](../adr/0008-order-ingestion.md)). Этот порядок не даёт `hits`
|
||||
автоматически создаться с одной партицией. Брокер автосоздание разрешает, а
|
||||
потребитель librdkafka внутри ClickHouse его не просит. Оба конца измерены;
|
||||
@@ -532,8 +536,8 @@ Airflow читает и собирает эти файлы штатным шаб
|
||||
| `dic` | `dic.products` | словарь товаров поверх подложки |
|
||||
|
||||
Матвью разбора у заказов нет: срез сырья раскладывают по этим двум целям два
|
||||
`INSERT SELECT` шага `parse_batch` из файлов `sql/ods/order_load.sql` и
|
||||
`sql/ods/order_errors_load.sql`.
|
||||
`INSERT SELECT` одной задачи `parse_batch` из файла
|
||||
`sql/ods/order_parse.sql`.
|
||||
|
||||
Слой DDS и слой DM появляются на следующих этапах; их состав задан разделом
|
||||
7 мастер-спеки и переносится сюда по мере постройки.
|
||||
|
||||
@@ -235,6 +235,21 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
|
||||
|
||||
## Что проверено
|
||||
|
||||
**Перезамер 21 августа 2026 года при исполнении #124.** Образ Airflow собран с
|
||||
`apache-airflow-providers-clickhousedb==1.0.0`,
|
||||
`apache-airflow-providers-common-sql==2.0.1` и
|
||||
`clickhouse-connect==1.6.0`; `pip check` не нашёл несовместимых зависимостей.
|
||||
Адресная пересборка мира через `make rebuild-storage` заняла 3 м 5 с: все шесть
|
||||
задач DDL, `pull_batch` и `parse_batch` завершились успешно. После неё прошли
|
||||
смоук 21/21 за 17 с, проверка ClickHouse 10/10 и межсервисная проверка 7/7 за
|
||||
100 с.
|
||||
|
||||
Первый `make up` после замены образа попал в короткий рассинхрон запуска
|
||||
Airflow: API уже сообщил о здоровье и отдал `world_initialize` по GET, но POST
|
||||
нового запуска ещё ответил `Dag with dag_id not found`. Ошибок импорта не было;
|
||||
через несколько секунд повторный `make up` завершился успешно и не менял
|
||||
готовый мир.
|
||||
|
||||
**Перезамер 21 августа 2026 года при исполнении #114.** После добавления
|
||||
`kafka-exporter` и источника ClickHouse смоук прошёл 21/21 за 16–17 секунд,
|
||||
`check-services` — 7/7 за 93 секунды. При остановленном экспортёре смоук дал
|
||||
|
||||
@@ -51,8 +51,8 @@ Airflow держит прикладные топики Kafka, каноничес
|
||||
([ADR 0007](../adr/0007-clickhouse-access.md)). Ей разрешено администрировать
|
||||
прикладные базы — заводить и удалять их и объекты внутри, — читать их и
|
||||
выполнять кластерный DDL. Ей разрешено чтение `system.settings`: без него
|
||||
`clickhouse-connect` не открывает соединение. Административные запросы к
|
||||
системным базам ей запрещены.
|
||||
драйвер `clickhouse-connect` под официальным провайдером Airflow не открывает
|
||||
соединение. Административные запросы к системным базам ей запрещены.
|
||||
|
||||
`etl` при этом не расширяется. Право удаления баз нужно редкой операции
|
||||
восстановления и остаётся у `lifecycle`. Граница проверяема: при лишнем запросе
|
||||
@@ -69,9 +69,10 @@ Airflow держит прикладные топики Kafka, каноничес
|
||||
Меняющиеся API сверены через Context7 и затем с установленными пакетами
|
||||
стенда. Выбраны публичный REST API v2 и ждущий `TriggerDagRunOperator` Airflow,
|
||||
сборка образа через `DockerHook.get_conn().build()`, futures `AdminClient` для
|
||||
Kafka и по одному `clickhouse-connect.command()` на выражение DDL. Последний
|
||||
выбор требует явного разбора файлов: клиент сам многозапросный файл не делит,
|
||||
а в каноническом SQL конец выражения уже задан точкой с запятой в конце строки.
|
||||
Kafka, `SQLExecuteQueryOperator` для исполняемых SQL-файлов ClickHouse и
|
||||
`ClickHouseHook` для запросов, чей результат обрабатывает Python. Оператор сам
|
||||
шаблонизирует файл и при `split_statements=True` исполняет выражения по порядку;
|
||||
собственный разбор SQL дагу больше не нужен.
|
||||
|
||||
## Контракты дагов
|
||||
|
||||
@@ -90,8 +91,11 @@ Kafka и по одному `clickhouse-connect.command()` на выражени
|
||||
матвью, и топик, созданный брокером сам, приехал бы с одним разделом
|
||||
([ADR 0008](../adr/0008-order-ingestion.md)).
|
||||
3. Применяет канонический DDL из `sql/ddl/` по порядку имён файлов, с ноды 1,
|
||||
учётной записью `lifecycle`. Правило «матвью приёма создаётся последней»
|
||||
остаётся за раскладкой файлов и не переезжает в даг.
|
||||
учётной записью `lifecycle`. Каждый файл — отдельная последовательная
|
||||
задача `SQLExecuteQueryOperator`; выражения внутри неё исполняются по
|
||||
порядку с `split_statements=True`. Красная задача называет файл и не пускает
|
||||
последующие. Правило «матвью приёма создаётся последней» остаётся за
|
||||
раскладкой файлов и не переезжает в даг.
|
||||
4. Собирает канонический образ генератора. Иначе мир мог бы приехать из старого
|
||||
кода: сборкой образа при подъёме больше никто не занимается.
|
||||
5. Играет стартовые дни одним прогоном `batch` того же образа.
|
||||
|
||||
Reference in new issue
Block a user