Зачем: - Дать слою первую физическую сущность и показать идемпотентность, устроенную иначе, чем в приёме: заменой партиции, а не колонкой версии. - Родить механику дневной пересборки на кластере — образец для сессий и витрин. Что: - Добавлены dds.order_rep/_dist, двойник-донор dds.order_stage_rep/_dist и публичный договор dds.order_v с разбором позиций в три массива. - Добавлены запросы объёма прогона, сборки в двойник и замены партиции по дню. - Добавлен даг dds_transform: группа задач order, замена — размноженной задачей. - Окно изменяемости K пришло на сторону стенда якорем compose; владелец числа остаётся у генератора. - Объём считается от позднейшего дня заказа и первого недостающего дня: ярлыки доставки ODS для этого непригодны, а максимум собранного дня прячет дыры. - Записано, чего конвейер о себе не знает, и адресовано контуру DQ. Проверка: - make lint - make smoke, make check-clickhouse, make check-services - Прогон по стартовому миру: 1917 заказов, счёт равен ods.order_v, каждый заказ на шарде своего cityHash64(order_id), сумма позиций равна items_total. - Повтор без нового слепка: содержимое то же, _load_id новый; после слепка пересобрано окно, день за окном не тронут. - Снесённая руками партиция 2026-06-01 восстановлена побайтово. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
13 KiB
Дневной конвейер трансформаций
Как принятые источники превращаются в модель DDS и витрины DM: кто запускает трансформации, когда и в каком объёме. Правила самих сущностей — зерно, движки, что именно пересобирается — живут в доках слоя (карта модели DDS); здесь — только конвейер.
Спека обещала etl_pipeline этапом 5 — он понадобился раньше. Конвейер
рождается этапом 4, и временного в нём ничего нет: дневная сборка вперёд
останется и после. Этапу 5 достаётся его законный предмет — партиционная
переобработка, умение переиграть произвольный прошлый день по команде.
Форма
Дневной конвейер на стенде один — даг etl_pipeline. Своей работы
у него нет: он дёргает слойные даги последовательно и ждёт каждый —
dds_transform, затем dm_transform. Контур DQ этапа 4 встаёт его же
звеном после DM; состав звена — в доке слоя DM.
Слойные даги устроены по образцу orders_ingest: без расписания, один
активный прогон, запускаются триггером конвейера или рукой при разборе
происшествий. Витрины внутри dm_transform — отдельные задачи.
Три пути запуска
- Живой день.
world_live_dayдёргает конвейер с ожиданием — последним шагом, после установки позиции. Порядок несёт смысл: день сыгран — факт, упавшая трансформация его не отменяет, поэтому позиция ставится раньше; но зеленеть, не узнав, разложился ли день, работник не должен (ADR 0003). Такой отказ чинится перезапускомetl_pipelineрукой — перезапуску ничего не нужно помнить, см. «Объём прогона». - Разгон пачкой.
world_next_dayконвейер не дёргает: пульт управляет генератором и только им. После разгона менти запускает конвейер сам — и видит, как слои догоняют уехавший вперёд мир. - Рождение мира.
world_initializeдёргает конвейер последним шагом:make upотдаёт стенд с собранными слоями, а не пустые витрины. Уроки ручного запуска начинаются со следующих дней.
Объём прогона
Параметров у конвейера нет: объём он выводит сам из world_position и
состояния слоёв. Сессии догоняют хвост — от последнего собранного дня до
последнего прожитого; заказы пересобирают дышащее окно K и тот же хвост; карта
идентичностей пересчитывается целиком; правила витрин — у
слоя DM.
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
после падения, а разгону на семь дней хватает одного прогона.
Окно заказов отсчитывается от последнего дня, о котором рассказал сам источник, — от позднейшего дня заказа в ODS. Рядом напрашиваются ещё два способа, и оба отвергнуты по одной причине: они отвечают не на тот вопрос.
Позиция мира говорит, какой день прожит, а не какой доехал: прогон дня D везёт слепок дня D − 1, да и позиция ставится раньше трансформации, так что после неудачного приёма она обгоняет хранилище. Слой обязан считать объём по тому, что у него есть, а не по тому, что мир о себе заявляет.
Ярлыки доставки — snapshot_date, _load_id, _load_ts — не отвечают тоже, и
это свойство ODS, а не недосмотр: слой схлопывает доставки, сохраняя деловые
версии, поэтому у заказа, который не менялся, ярлык выжившей строки достаётся
жребием (приём заказов, «Роль ODS»). Замер на стартовом
мире: последний слепок нёс все 1662 заказа, а его ярлык сохранили 1188 —
отбор «дни, у которых snapshot_date равен последнему» потерял бы четверть
заказов и молча пропустил бы дальний край окна, где заказы давно замёрзли.
К времени создания это не относится: у всех версий заказа оно одно.
Готовность источника
Первый шаг dds_transform — дождаться, пока хвост топика hits дожёван:
отставание его потребителей нулевое (system.kafka_consumers). Зелёный
приём заказов о событиях не говорит ничего — топики разные и потребители
разные. В живом режиме зазор ничтожен, но в пакетном генератор выстреливает
дни в топик быстрее, чем чтец их прожёвывает, и сессии собрались бы по
недожёванному дню. DQ-сверка такой день подсветит, но починить его до
этапа 5 нечем: профилактика дешевле ремонта, которого ещё нет.
Идемпотентность
Партиция не заменяется простой вставкой. День собирается в таблицу-двойник —
вставка через Distributed раскладывает строки по шардам, — затем
REPLACE PARTITION на кластере атомарно подменяет кусок каждого шарда. Эта
механика — часть самих трансформаций и строится этапом 4: без неё дневной
прогон не повторяем. Сущности, пересчитываемые целиком, идемпотентны и без
замены: полная перезапись в ReplacingMergeTree — так живут карта
идентичностей и витрина daily_traffic (слой DM).
Чего конвейер о себе не знает
Трансформация собирает слой по правилу, а не сверяет его с источником. Отсюда четыре вещи, которых она не заметит, и у всех четырёх один адрес — контур DQ.
- Окно K разъехалось с источником. Число живёт двумя копиями: у генератора
в
world.pyи у стенда вcompose.yaml. Разойдутся — слой начнёт недобирать край окна или переписывать замёрзший день, и оба прогона будут зелёными. - Пустой свежий день сдвигает край окна. Если в позднейший день источника
не родилось ни одного заказа, окно съезжает на день назад
(
sql/dds/order_scope.sql). - Замена партиции прошла на одном шарде из двух. День в слое есть — значит, объём следующего прогона его не тронет, а слой останется собранным наполовину из старого, наполовину из нового.
- Брак внутри
itemsстановится нулями. Строгий приём внутрь позиций не смотрит, разбор по типу молчит, и цена без числа доедет до витрины выручки нулём (sql/dds/order_rebuild.sql).
Общее у них одно, и оно же подсказывает лекарство: ответить может только
сверка слоя с источником по дню — счёт заказов и позднейший updated_at с
обеих сторон. Трансформации такую сверку не поручают намеренно: она собирает,
а проверяет другой. Так две роли остаются различимы, и ни одна не
притворяется другой.
Не входит
- Партиционная переобработка — переиграть произвольный прошлый день или
диапазон, включая форму необязательных параметров того же дага, — этап 5
(#7); туда же стык #111 про дату модельного мира и
logical_date. - Инкрементальный ETL вместо пересборки — техдолг #2.
- Состав витрин
dm_transformи звена DQ — дока слоя DM; имена задач — нарезка этапа 4.
Отклонённые варианты
- Задачи трансформаций прямо в работниках пульта. Один зелёный прогон — весь день, но пульт — про ось времени: работников два, шаги дублировались бы, а этапу 5 пришлось бы выдирать их обратно.
- Make-цель или SQL рукой при приёмке. Второй оркестратор рядом с Airflow — против ADR 0013, — а в живом режиме слои копили бы отставание молча.
- Временный даг с нейтральным именем до этапа 5. Дневная сборка вперёд никуда не денется, переезд был бы переименованием ради выдуманной границы.
- Диапазон дней параметрами триггера. Ручному запуску менти параметры передавать некому, а перезапуск после падения требовал бы помнить conf упавшего прогона.
- Принять гонку с хвостом
hits. Дыра всплыла бы в DQ-сверке, а чиниться — не раньше этапа 5.
Связанные решения
- ADR 0003 — зелёный конец графа не переживает отказ выше: отсюда ожидания в триггерах.
- ADR 0009 — пульт мира: его работник живого дня и дёргает конвейер.
- ADR 0013 — жизненный цикл мира: почему
прикладным процессом владеет Airflow и почему
world_initializeкончается собранными слоями. - Резолюция — тикет #134 карты #127; что пересобирает каждая сущность — заказ, сессия, карта идентичностей.