docs(warehouse): упрощены объяснения витрин, сессий и пересчета
Зачем: читателю ступеней 1–4 нужно проследить выбор дней, расчет строки и публикацию результата без расшифровки метафор. Что: согласованы справочники DM, сессий, ETL и два раздела хранилища с комментариями шести SQL-примеров. Уточнены ограничения повторного запуска и выбора атрибутов при равном времени событий. Проверка: make lint, git diff --check, локальные Markdown-ссылки и якоря. SQL без комментариев совпадает; AST Python без строк документации совпадает. Исторические разделы и материалы вне границ задачи сохранены. Refs: #249
This commit is contained in:
1 parent
84dc718f62
commit
5f317ba8d2
11 files changed
+440
-490
No files matched your search
+90
-82
@@ -1,28 +1,21 @@
|
||||
# Дневной конвейер трансформаций
|
||||
|
||||
Как принятые источники превращаются в модель DDS и витрины DM: кто запускает
|
||||
трансформации, когда и в каком объёме. Правила самих сущностей — зерно,
|
||||
движки, что именно пересобирается — живут в доках слоя
|
||||
([карта модели DDS](dds/README.md)); здесь — только конвейер.
|
||||
|
||||
Дневная сборка вперёд появляется на этапе 4 и остаётся основным режимом
|
||||
конвейера. На этапе 5 к ней добавляется партиционная переобработка: запуск
|
||||
произвольного прошлого дня по команде.
|
||||
Даг `etl_pipeline` собирает модель DDS, обновляет витрины DM и проверяет
|
||||
результат. Здесь описаны запуск, выбор дней и публикация данных.
|
||||
Состав моделей — в [справочниках DDS](dds/README.md) и [DM](dm.md).
|
||||
|
||||
## Форма
|
||||
|
||||
Дневной конвейер на стенде один — даг `etl_pipeline`. Своей работы
|
||||
у него нет: он дёргает слойные даги последовательно и ждёт каждый —
|
||||
`dds_transform`, затем `dm_transform` и `dq_check`. Состав проверок описан в
|
||||
[доке слоя DM](dm.md).
|
||||
`etl_pipeline` последовательно запускает `dds_transform`, `dm_transform`
|
||||
и `dq_check`. Каждый следующий даг начинается после успеха предыдущего.
|
||||
Для этого `TriggerDagRunOperator` ждет завершения созданного им запуска;
|
||||
его ошибка завершает ошибкой и соответствующую задачу конвейера.
|
||||
Поведение проверено через Context7 по документации Airflow 3.x (3.1.6).
|
||||
|
||||
Ждущие вызовы сделаны `TriggerDagRunOperator`. Проверка через Context7 по
|
||||
ближайшей доступной документации Airflow 3.x (версия 3.1.6) подтвердила, что
|
||||
оператор опрашивает точный созданный им прогон и краснеет вслед за потомком.
|
||||
|
||||
Слойные даги устроены по образцу `orders_ingest`: без расписания, один
|
||||
активный прогон, запускаются триггером конвейера или рукой при разборе
|
||||
происшествий. Витрины внутри `dm_transform` — отдельные задачи.
|
||||
У этих дагов нет расписания. Их вызывает конвейер; для отдельного разбора
|
||||
можно запустить даг слоя вручную. У каждого разрешен один активный запуск:
|
||||
два запуска одного дага не должны одновременно заполнять общий донор.
|
||||
Внутри дага модели собраны в отдельные задачи и группы задач.
|
||||
|
||||
## Три пути запуска
|
||||
|
||||
@@ -34,83 +27,98 @@ world_live_day ───┼─> etl_pipeline ─> dds_transform ─> dm_transfor
|
||||
world_next_day ─────> источники без ETL
|
||||
```
|
||||
|
||||
- **Живой день.** `world_live_day` дёргает конвейер с ожиданием — последним
|
||||
шагом, после установки позиции. Порядок несёт смысл: день сыгран — факт,
|
||||
упавшая трансформация его не отменяет, поэтому позиция ставится раньше; но
|
||||
зеленеть, не узнав, разложился ли день, шаговый даг не должен ([ADR
|
||||
0003](../adr/0003-dag-run-state.md)). Такой отказ чинится перезапуском
|
||||
`etl_pipeline` рукой — перезапуску ничего не нужно помнить, см. «Объём
|
||||
прогона».
|
||||
- **Разгон пачкой.** `world_next_day` конвейер не дёргает: даги модельного
|
||||
времени управляют генератором и только им. После разгона менти запускает
|
||||
конвейер сам — и видит, как слои догоняют уехавший вперёд мир.
|
||||
- **Рождение мира.** `world_initialize` дёргает конвейер последним шагом:
|
||||
`make up` отдаёт стенд с собранными слоями, а не пустые витрины. Уроки
|
||||
ручного запуска начинаются со следующих дней.
|
||||
- `world_live_day` запускает конвейер после успешной генерации, приема
|
||||
заказов и обновления `world_position`. Если трансформация упала, день
|
||||
уже прожит и позиция не откатывается. После исправления причины
|
||||
запустите `etl_pipeline` вручную.
|
||||
- `world_next_day` отправляет дни пачкой и ждет приема заказов, но конвейер
|
||||
не запускает. После него запустите `etl_pipeline` сами.
|
||||
- `world_initialize` запускает конвейер последним шагом: при успешном
|
||||
первом `make up` начальные данные уже собраны в DDS и DM.
|
||||
|
||||
## Объём прогона
|
||||
|
||||
Параметров у конвейера нет: объём он выводит сам из `world_position` и
|
||||
состояния слоёв. Сессии догоняют хвост — от первого несобранного дня по
|
||||
последний прожитый; заказы пересобирают дышащее окно и тот же хвост; карта
|
||||
идентичностей пересчитывается целиком; правила витрин — у
|
||||
[слоя DM](dm.md).
|
||||
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
|
||||
после падения, а разгону на семь дней хватает одного прогона.
|
||||
Объем прогона — дни, которые обрабатывает один запуск. Даты передавать
|
||||
не нужно: даги определяют их по состоянию таблиц и `world_position`.
|
||||
Например, после генерации семи дней без ETL один запуск соберет все семь.
|
||||
|
||||
Окно заказов задаёт последний слепок, наблюдаемый в ODS. У каждого заказа
|
||||
`snapshot_date` равен последнему слепку, в котором источник его показал.
|
||||
Трансформация выбирает строки с максимальной `snapshot_date` и берёт среди них
|
||||
минимальный и максимальный `order_date`. Это границы дней, состояние которых
|
||||
источник ещё может изменить
|
||||
([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Кроме живого окна,
|
||||
трансформация закрывает пропуски: находит самый ранний день, представленный в
|
||||
ODS и ещё не собранный в DDS, затем при необходимости расширяет до него левую
|
||||
границу.
|
||||
Сессии собираются от первого отсутствующего дня до последнего прожитого.
|
||||
`world_position` хранит номер следующего дня для генерации, поэтому
|
||||
последний прожитый день равен `min(EventDate) + world_position - 1`.
|
||||
Начало календаря берется из ODS; события ранних дней там не удаляются по TTL.
|
||||
Один `max(EventDate)` не годится: события за день есть и во время его
|
||||
генерации, когда последние визиты еще не закончились.
|
||||
Точный запрос — [`session_scope.sql`](../../sql/dds/session_scope.sql).
|
||||
|
||||
Позиция мира заказам не отвечает: она говорит, какой день прожит, а не какой
|
||||
доехал. Прогон дня D везёт слепок дня D − 1, да и ставится позиция раньше
|
||||
трансформации, так что после неудачного приёма обгоняет хранилище. Слой обязан
|
||||
считать объём по тому, что у него есть, а не по тому, что мир о себе заявляет.
|
||||
Для заказов диапазон задает последний принятый слепок ODS. Среди строк
|
||||
с максимальной `snapshot_date` выбираются самая ранняя и самая поздняя
|
||||
даты оформления заказа. Эти дни еще могут измениться. Если в DDS
|
||||
отсутствует более ранний день, который есть в ODS, левая граница
|
||||
расширяется до него. Все дни отрезка пересобираются.
|
||||
|
||||
Для сессий границу задаёт `world_position`: она отличает полностью прожитый
|
||||
день от ещё идущего. Сами события этой границы не показывают: и посреди дня D,
|
||||
и сразу после его конца их позднейший день равен D. Если собрать сессии посреди
|
||||
дня, визит, начатый в 23:50, останется обрезанным: завершённый день слой
|
||||
повторно не собирает. Поэтому последний прожитый день равен первому дню
|
||||
событий плюс позиция минус один
|
||||
([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)).
|
||||
Оговорка про «доехал» остаётся в силе и здесь: за хвост потока отвечает не
|
||||
позиция, а «Готовность источника».
|
||||
Позиция мира для выбора дней заказов не подходит: с событиями дня D
|
||||
приходит слепок за D−1. Объем рассчитывается по принятым строкам ODS;
|
||||
их наличие еще не доказывает полноту слепка.
|
||||
|
||||
Карта идентичностей пересчитывается целиком. Витрины выбирают дни
|
||||
по своим источникам — см. [ритм пересборки DM](dm.md#ритм-пересборки).
|
||||
Поиск отсутствующих дней проверяет только наличие даты: частично
|
||||
заполненный день требует отдельной проверки.
|
||||
|
||||
## Готовность источника
|
||||
|
||||
Первый шаг `dds_transform` проходит два барьера за общий срок в 300 секунд.
|
||||
Сначала клиент `confluent-kafka` сверяет подтвержденные смещения группы
|
||||
`clickstream_hits` с верхними смещениями обоих разделов `hits`. Группа, оба
|
||||
раздела и все смещения должны существовать. Суммарное отставание должно стать
|
||||
нулевым. Каждый опрос пишет в журнал оба смещения и отставание по разделу.
|
||||
Перед сборкой DDS задача `wait_for_source` последовательно ждет двух
|
||||
условий:
|
||||
|
||||
Только после этого шаг проверяет обе ноды ClickHouse через
|
||||
`clusterAllReplicas`: у `ods.event_dist` в `system.distribution_queue` не
|
||||
должно остаться файлов. Каждый опрос пишет в журнал ноды, число файлов и
|
||||
`last_exception` очереди. Оба барьера опрашиваются раз в три секунды и делят
|
||||
один крайний срок. После Kafka срок не отсчитывается заново.
|
||||
1. Группа `clickstream_hits` дочитала оба раздела топика `hits`.
|
||||
Подтвержденные смещения сравниваются с верхними смещениями Kafka.
|
||||
Группа и смещения должны существовать, суммарное отставание — равняться нулю.
|
||||
2. В `system.distribution_queue` у `ods.event_dist` не осталось файлов
|
||||
доставки на шарды. Запрос через `clusterAllReplicas` читает обе ноды.
|
||||
|
||||
Зелёный приём заказов о событиях не говорит ничего — топики разные и
|
||||
потребители разные. Одного нулевого отставания Kafka тоже мало: чтец мог
|
||||
подтвердить сообщение раньше, чем Distributed-таблица доставила файл на шард.
|
||||
Два барьера закрывают обе части пути до того, как сессии выберут свой объем.
|
||||
Опросы идут с паузой в три секунды; на обе стадии задан общий срок
|
||||
300 секунд. Журнал показывает смещения и отставание Kafka, затем число
|
||||
файлов и `last_exception` очереди на каждой ноде. Если готовность не
|
||||
подтверждена за срок ожидания, задача завершается ошибкой.
|
||||
|
||||
Одного нулевого отставания Kafka мало: сообщение может быть подтверждено,
|
||||
пока Distributed-таблица еще доставляет его на шард. Успех `orders_ingest`
|
||||
тоже не заменяет эти проверки: у заказов другой топик и потребитель.
|
||||
|
||||
Эти условия проверяют доставку на момент опроса. Они не останавливают
|
||||
поступление новых событий и не сверяют содержимое таблиц. Поэтому
|
||||
`world_position` отдельно ограничивает сборку сессий прожитыми днями,
|
||||
а качество результата проверяет `dq_check`.
|
||||
|
||||
## Идемпотентность
|
||||
|
||||
Партиция не заменяется простой вставкой. День собирается в таблицу-двойник —
|
||||
вставка через Distributed раскладывает строки по шардам, — затем
|
||||
`REPLACE PARTITION` на кластере атомарно подменяет кусок каждого шарда. Эта
|
||||
механика — часть самих трансформаций и строится этапом 4: без неё дневной
|
||||
прогон не повторяем. Сущности, пересчитываемые целиком, идемпотентны и без
|
||||
замены: полная перезапись в `ReplacingMergeTree` — так живут карта
|
||||
идентичностей и витрина `daily_traffic` ([слой DM](dm.md)).
|
||||
Идемпотентность означает, что повторная обработка тех же исходных данных
|
||||
не добавляет копии деловых строк. Служебные `_load_id` и `_load_ts`
|
||||
при новой сборке меняются.
|
||||
|
||||
Для дневных таблиц порядок такой: `scope` выбирает даты, `rebuild` очищает
|
||||
донор и собирает в него весь диапазон, затем отдельные задачи `replace`
|
||||
публикуют дни через `REPLACE PARTITION`. Донор — служебная таблица с той же
|
||||
структурой и раскладкой по шардам, что у цели. Запись в `_stage_dist`
|
||||
с `distributed_foreground_insert = 1` дожидается доставки строк перед заменой.
|
||||
|
||||
Замена выполняется на локальных `_rep` с `ON CLUSTER`. Она атомарна
|
||||
на каждом шарде: читатель видит старую или новую партицию. Единой
|
||||
транзакции для двух шардов и всех дней нет. Если замена прошла только
|
||||
на одном шарде, другой может сохранить прежние данные. Повтор задачи
|
||||
`replace` с тем же готовым донором повторит замену без добавления копий.
|
||||
|
||||
Новый запуск всего конвейера заново выбирает дни. Он не обязан повторить
|
||||
все дни неудачного запуска: если дата уже есть в целевой таблице, поиск
|
||||
пропусков ее не выберет. В частности, изменение SQL сессий само по себе
|
||||
не заставляет пересчитать заполненные дни. Повторная сборка выбранного
|
||||
дня разобрана в [задании ступени 4](../labs/4-1-session-pipeline.md).
|
||||
|
||||
Карта идентичностей и `daily_traffic` обновляются иначе: каждый запуск
|
||||
добавляет полный результат новой версией в `ReplacingMergeTree(_load_ts)`.
|
||||
Публичные представления читают текущие строки через `FINAL`. Старые версии
|
||||
могут физически оставаться до фонового слияния; повторяемость относится
|
||||
к результату чтения, а не к числу строк на диске.
|
||||
|
||||
## Чего конвейер о себе не знает
|
||||
|
||||
|
||||
Reference in new issue
Block a user