docs(glossary): четыре термина переименованы, world.md и stand.md вычитаны
- Зачем:
- справочники читает человек со стороны, а «пульт мира», «опись мира»,
«работник» и «сырьё» звучали шуткой или калькой времён разработки
(тикет #214, выбор владельца).
- Что:
- CONTEXT.md: статьи «Паспорт мира», «Даги модельного времени»,
«Шаговый даг», «Сырые данные», старые слова в строках «Избегать»;
пометки в ADR 0005, 0008, 0009 без переписывания решений.
- замены по README, справочникам, путеводителю, лабам, спекам,
комментариям дагов и SQL, тегу дага и панели Grafana; имена в коде
не тронуты.
- world.md и stand.md: 35 находок вычитки внесены, регистр выровнен.
- Проверка:
- make config-test; make lint в корне и в generator/; make test в
generator/ (437 passed); скрипт ссылок из #212 пуст; grep старых
слов вне ADR, research и CONTEXT.md пуст.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QBCtU38qSHg5DvgtnKXcW
This commit is contained in:
1 parent
86ad8afaf2
commit
59d8c83b17
54 files changed
+794
-776
No files matched your search
+159
-161
@@ -7,20 +7,19 @@
|
||||
|
||||
**Что здесь описано и чего ещё нет.** Собран этап 1: кластер из двух шардов,
|
||||
keeper, Kafka, каркас сервисов. Этап 2 собран: в `sql/ddl/` лежит вся цепочка
|
||||
`Kafka → STG → ODS` — чтец топика `hits`, таблицы сырья, типизированное
|
||||
событие с таблицей ошибок, поверхность актуального состояния и три матвью.
|
||||
Этап 3 добавил вход второго источника и довёл его до ODS: топик `orders`, свой
|
||||
чтец, своё сырьё, версии заказов с таблицей ошибок и поверхность текущего
|
||||
состояния. Наполняет всю цепочку даг `orders_ingest` двумя шагами, а не матвью.
|
||||
Тот же этап принёс первый справочник — словарь товаров из общего с генератором
|
||||
CSV-файла. Живёт он вне цепочки слоёв, в зоне `dic`, и читает не файл, а
|
||||
подложку над ним ([ADR 0012](../adr/0012-dictionary-home.md)). Этап 4 начат
|
||||
публичным договором события `dds.event_v` над точным состоянием ODS, а следом
|
||||
получил две физические модели слоя — заказ и сессию: их собирает заменой
|
||||
партиций даг `dds_transform`. Сессия среди них особая: в потоке такой сущности
|
||||
нет, стенд режет её из событий сам.
|
||||
Дальше по тексту устройство описано так, как оно проектируется; построенное от
|
||||
заложенного отличает карта таблиц в конце.
|
||||
`Kafka → STG → ODS` — чтец топика `hits`, таблицы STG, типизированное событие с
|
||||
таблицей ошибок, поверхность актуального состояния и три матвью. Этап 3 добавил
|
||||
вход второго источника и довёл его до ODS: топик `orders`, свой чтец, свой сырой
|
||||
слой, версии заказов с таблицей ошибок и поверхность текущего состояния.
|
||||
Наполняет всю цепочку даг `orders_ingest` двумя шагами, а не матвью. Тот же этап
|
||||
принёс первый справочник — словарь товаров из общего с генератором CSV-файла.
|
||||
Живёт он вне цепочки слоёв, в зоне `dic`, и читает не файл, а подложку над ним
|
||||
([ADR 0012](../adr/0012-dictionary-home.md)). Этап 4 начат публичным договором
|
||||
события `dds.event_v` над точным состоянием ODS, а следом получил две физические
|
||||
модели слоя — заказ и сессию: их собирает заменой партиций даг `dds_transform`.
|
||||
Сессия среди них особая: в потоке такой сущности нет, стенд режет её из событий
|
||||
сам. Дальше по тексту устройство описано так, как оно проектируется; построенное
|
||||
от заложенного отличает карта таблиц в конце.
|
||||
|
||||
Зона ответственности у документа одна — хранилище. Генератор описан отдельно:
|
||||
его замысел — в [спеке генератора](../specs/2026-08-01-generator.md), формат
|
||||
@@ -97,7 +96,7 @@ ClickHouse и отклонённые варианты записаны в
|
||||
Ключи шардирования: `cityHash64(ClientID)` у событий, `cityHash64(order_id)` у
|
||||
заказов, `cityHash64` сырой строки у STG и у таблицы ошибок. У первых двух хеш
|
||||
выбран против перекоса: структурированный числовой идентификатор распределяется
|
||||
по остатку от деления неравномерно. У сырья выбора нет — строку иначе не
|
||||
по остатку от деления неравномерно. У сырого слоя выбора нет — строку иначе не
|
||||
разложишь; там хеш даёт другое свойство, одинаковые сообщения ложатся на один
|
||||
шард.
|
||||
|
||||
@@ -157,10 +156,10 @@ FROM источник_dist`. Заводское значение `parallel_distr
|
||||
даёт две: `_timestamp` — `Nullable(DateTime)`, то есть секунды, и
|
||||
`_timestamp_ms` — `Nullable(DateTime64(3))`, миллисекунды. Колонка объявлена
|
||||
`Nullable(DateTime64(3, 'UTC'))` и заполняется из `_timestamp_ms`: у брокера
|
||||
метка миллисекундная, соседняя `_load_ts` тоже миллисекундная, а слой сырья
|
||||
метка миллисекундная, соседняя `_load_ts` тоже миллисекундная, а сырой слой
|
||||
хранит приехавшее, и округлять ему нечего. Обнуляемость нужна отдельно от
|
||||
разрядности: брокер метку заполняет не всегда, а необнуляемый тип значил бы
|
||||
либо падение приёма на первом сообщении, либо тихие нули за 1970 год.
|
||||
разрядности: брокер метку заполняет не всегда, а необнуляемый тип значил бы либо
|
||||
падение приёма на первом сообщении, либо тихие нули за 1970 год.
|
||||
|
||||
Заполняются все они выражением в `SELECT` матвью приёма, а не `DEFAULT` в
|
||||
таблице. Для `consumer_host` это обязательно: `DEFAULT hostName()` вычисляется
|
||||
@@ -173,10 +172,10 @@ FROM источник_dist`. Заводское значение `parallel_distr
|
||||
пакетный шаг ([ADR 0008](../adr/0008-order-ingestion.md), [ADR
|
||||
0010](../adr/0010-order-versions-in-ods.md)).
|
||||
|
||||
Движок таблицы сырья — обычный `ReplicatedMergeTree`, `ORDER BY (kafka_partition,
|
||||
kafka_offset)`: разбор полётов идёт от «какое сообщение», другого ключа у сырья и
|
||||
нет. Замену версий сюда ставить нельзя — она отменила бы свойство слоя, ради
|
||||
которого он заведён: повтор доставки в сырье обязан быть виден.
|
||||
Движок таблицы STG — обычный `ReplicatedMergeTree`, `ORDER BY (kafka_partition,
|
||||
kafka_offset)`: разбор полётов идёт от «какое сообщение», другого ключа у
|
||||
таблицы STG и нет. Замену версий сюда ставить нельзя — она отменила бы свойство
|
||||
слоя, ради которого он заведён: повтор доставки в STG обязан быть виден.
|
||||
|
||||
Метка времени загрузки зовётся `_load_ts`, тип `DateTime64(3, 'UTC')`. Ставится
|
||||
она один раз при записи в STG: для событий — матвью приёма, для дневного слепка
|
||||
@@ -192,7 +191,7 @@ kafka_offset)`: разбор полётов идёт от «какое сооб
|
||||
`_load_ts`. Совпадение слов «техническое время» не делает эти часы одной осью.
|
||||
|
||||
Пакетной переобработки у событий в ODS нет: слой наполняют матвью, а не задание
|
||||
Airflow. Переделать разобранное руками можно вставкой из сырья с фильтром по
|
||||
Airflow. Переделать разобранное руками можно вставкой из STG с фильтром по
|
||||
`_load_ts` в пределах трёхсуточного окна; ничья по версии разрешается в пользу
|
||||
вставленного позже. Заказы разбирает пакетный шаг из неизменного среза STG по
|
||||
`_load_id`; дневные партиции ODS он не заменяет.
|
||||
@@ -223,10 +222,10 @@ ODS и в таблицу ошибок. Нужен ли он выше ODS, реш
|
||||
|
||||
Типы ODS обычно повторяют контракт выгрузки. Денежный `purchaseRevenue` —
|
||||
исключение: источник объявляет `Array(Float64)`, а материализованное
|
||||
представление ODS разбирает числа сырого JSON сразу в
|
||||
`Array(Decimal(18, 2))`. DDS наследует этот тип без нового преобразования.
|
||||
Прямой разбор подтвержден документацией ClickHouse через Context7 и запросом
|
||||
к ClickHouse 26.3; сравнение с сырьем подтвердило: копейки всех покупок целы.
|
||||
представление ODS разбирает числа сырого JSON сразу в `Array(Decimal(18, 2))`.
|
||||
DDS наследует этот тип без нового преобразования. Прямой разбор подтвержден
|
||||
документацией ClickHouse через Context7 и запросом к ClickHouse 26.3; сравнение
|
||||
с сырыми данными подтвердило: копейки всех покупок целы.
|
||||
|
||||
## Версии заказов
|
||||
|
||||
@@ -257,16 +256,16 @@ DDS. `snapshot_date` в нём одновременно дата наблюде
|
||||
|
||||
**Линза называется явно — в типе колонки либо в вызове функции.** Третий
|
||||
источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому там,
|
||||
где линза что-то решает — в объявлении хранимой колонки и в выражении,
|
||||
считающем дату, — его не остаётся. Прописать этот пояс своей рукой —
|
||||
`<timezone>` в конфигурации ноды или `TZ` контейнеру — было бы той же болезнью
|
||||
с другим умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах
|
||||
и в разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться,
|
||||
поменяв его. Правило стоит на источнике пояса, а не на функции:
|
||||
`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и
|
||||
работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок. Имя
|
||||
пишется тогда, когда нужна другая линза, чем у колонки:
|
||||
`toDate(UTCEventTime, 'Europe/Samara')` — это «день по часам счётчика».
|
||||
где линза что-то решает — в объявлении хранимой колонки и в выражении, считающем
|
||||
дату, — его не остаётся. Прописать этот пояс своей рукой — `<timezone>` в
|
||||
конфигурации ноды или `TZ` контейнеру — было бы той же болезнью с другим
|
||||
умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах и в
|
||||
разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться,
|
||||
поменяв его. Правило стоит на источнике пояса, а не на функции: `toDate` по
|
||||
колонке, чей тип пояс несёт, законен и имени не требует — так и работают ключи
|
||||
партиций `toDate(_load_ts)` у таблицы STG и у таблицы ошибок. Имя пишется тогда,
|
||||
когда нужна другая линза, чем у колонки: `toDate(UTCEventTime, 'Europe/Samara')`
|
||||
— это «день по часам счётчика».
|
||||
|
||||
**Какая линза, решает слой — по тому, кого он обслуживает.** ODS хранит снимок
|
||||
выгрузки и говорит на языке выгрузки: у Метрики `UTCEventTime` абсолютна,
|
||||
@@ -314,8 +313,8 @@ UTC+4), и пересчёт идёт один раз при наполнении
|
||||
0008](../adr/0008-order-ingestion.md) и [спецификация приёма
|
||||
заказов](orders/ingestion.md).
|
||||
|
||||
Цепочка одна: чтец топика → матвью → сырьё STG → матвью разбора → событие и
|
||||
таблица ошибок ODS.
|
||||
Цепочка одна: чтец топика → матвью → STG → матвью разбора → событие и таблица
|
||||
ошибок ODS.
|
||||
|
||||
Чтец стоит на обеих нодах и читает одной группой потребителей — имя группы
|
||||
`clickstream_hits`, и оно одинаково на обеих нодах по построению: DDL идёт
|
||||
@@ -344,15 +343,15 @@ Airflow она стоит — там это обычный `SETTINGS` у зап
|
||||
пользователя в конфигурации ноды. Синхронный режим связывает шарды: пока второй
|
||||
недоступен, вставка падает, офсеты не коммитятся, и приём встаёт целиком — тогда
|
||||
как при фоновом первая нода продолжает принимать и копит спул для соседа.
|
||||
Платится при этом не одно ожидание на блок, а три распределённые вставки — сырьё,
|
||||
Платится при этом не одно ожидание на блок, а три распределённые вставки — STG,
|
||||
событие, ошибки, — и все внутри потока-потребителя, что само по себе повод для
|
||||
ребаланса по таймауту сессии. Против всего этого — окно в сотню миллисекунд на
|
||||
ноутбуке, где мир пересобирается одной командой. Размен не в пользу настройки, а
|
||||
компромисс полезнее показать, чем спрятать за галочкой.
|
||||
|
||||
Ещё одно место, где сырьё хранит не всё приехавшее: запись с пустым значением и
|
||||
запись-надгробие проходят молча, не оставляя строки. Свойство измерено и принято
|
||||
осознанно — подробности в разделе «Что проверено».
|
||||
Ещё одно место, где сырой слой хранит не всё приехавшее: запись с пустым
|
||||
значением и запись-надгробие проходят молча, не оставляя строки. Свойство
|
||||
измерено и принято осознанно — подробности в разделе «Что проверено».
|
||||
|
||||
**Гарантии нет ни в одну сторону — есть два узких окна.** Окно потери описано
|
||||
выше: нода умерла между коммитом офсетов и сбросом спула. Окно дубля
|
||||
@@ -361,14 +360,14 @@ Airflow она стоит — там это обычный `SETTINGS` у зап
|
||||
один раз» нельзя — это обещало бы, что потерь не бывает, а они возможны.
|
||||
|
||||
Дубль ниже по течению ведёт себя по-разному. В ODS его схлопнет
|
||||
`ReplacingMergeTree`, а сырьё дедупа не имеет вовсе: перезаливка модельного дня
|
||||
честно удваивает `count()` в STG, и живёт эта пара до истечения срока хранения.
|
||||
Это свойство слоя, а не поломка, — но обещание идемпотентности конвейера к
|
||||
сырому слою не относится.
|
||||
`ReplacingMergeTree`, а таблицы STG вовсе не устраняют дубли: перезаливка
|
||||
модельного дня честно удваивает `count()` в STG, и живёт эта пара до истечения
|
||||
срока хранения. Это свойство слоя, а не поломка, — но обещание идемпотентности
|
||||
конвейера к сырому слою не относится.
|
||||
|
||||
Оговорка к последнему: у семейства `Replicated*` есть своя дедупликация — блок с
|
||||
тем же хешем, вставленный повторно, отбрасывается (`insert_deduplicate`).
|
||||
Удвоение сырья проходит мимо неё только потому, что при повторном чтении
|
||||
Удвоение строк STG проходит мимо неё только потому, что при повторном чтении
|
||||
`_load_ts` новый и хеш блока другой. Свойство слоя держится на этом, а не на
|
||||
отсутствии механизма.
|
||||
|
||||
@@ -384,53 +383,54 @@ Airflow она стоит — там это обычный `SETTINGS` у зап
|
||||
роняет вставку и останавливает потребление до починки
|
||||
([ADR 0005](../adr/0005-event-ingestion.md)).
|
||||
|
||||
Постоянной сверки **слоёв между собой** при этом нет и не должно быть. У сырья
|
||||
срок жизни трое суток, а ODS хранит всё, поэтому равенство «сырьё = события +
|
||||
ошибки» разъедется само: сырьё истечёт раньше, а перезаливка модельного дня
|
||||
задвоит его, тогда как в ODS тот же повтор схлопнется. Правило, красное в норме, учит не
|
||||
смотреть на оповещения
|
||||
([ADR 0002](../adr/0002-monitoring-scope.md)). Равенство проверено разовым
|
||||
опытом при исполнении #43, на управляемой пачке: отправили N сообщений —
|
||||
получили N строк сырья и N в сумме событий и ошибок. Постоянной целью такой
|
||||
опыт не становится, и почему — в [карте
|
||||
проверок](testing.md), раздел «Интеграционная проверка постоянной целью не
|
||||
становится». Счёт актуальных событий идёт через `ods.event_v`, которое скрывает
|
||||
`FINAL`: голый `count()` по `ReplacingMergeTree` зависит от того, сколько
|
||||
мержей успело пройти, и спека это прямо запрещает (раздел 6).
|
||||
Постоянной сверки **слоёв между собой** при этом нет и не должно быть. У сырого
|
||||
слоя срок жизни трое суток, а ODS хранит всё, поэтому равенство «строки STG =
|
||||
события + ошибки» разъедется само: строки STG истекут раньше, а перезаливка
|
||||
модельного дня задвоит их, тогда как в ODS тот же повтор схлопнется. Правило,
|
||||
красное в норме, учит не смотреть на оповещения ([ADR
|
||||
0002](../adr/0002-monitoring-scope.md)). Равенство проверено разовым опытом при
|
||||
исполнении #43, на управляемой пачке: отправили N сообщений — получили N строк
|
||||
STG и N в сумме событий и ошибок. Постоянной целью такой опыт не становится, и
|
||||
почему — в [карте проверок](testing.md), раздел «Интеграционная проверка
|
||||
постоянной целью не становится». Счёт актуальных событий идёт через
|
||||
`ods.event_v`, которое скрывает `FINAL`: голый `count()` по `ReplacingMergeTree`
|
||||
зависит от того, сколько мержей успело пройти, и спека это прямо запрещает
|
||||
(раздел 6).
|
||||
|
||||
**Постоянная сверка одна, и она другого рода** — не слой против слоя, а ODS
|
||||
против [описи мира](../../data/world-inventory.json), лежащей в git
|
||||
(`make check-clickhouse`, пришла с #42). Разъехаться сама она не может, и в
|
||||
этом вся разница: опись описывает восемь дней стартового мира, счёт обрамлён
|
||||
их датами, повтор заливки схлопывается в `ods.event_v`, а срок хранения ей не
|
||||
помеха — ODS хранит всё. Обе стороны равенства зафиксированы: одна кодом
|
||||
генератора, другая файлом в git. У межслойной сверки такой опоры нет ни с
|
||||
одной стороны.
|
||||
против [паспорта мира](../../data/world-inventory.json), лежащего в git (`make
|
||||
check-clickhouse`, пришла с #42). Разъехаться сама она не может, и в этом вся
|
||||
разница: паспорт описывает восемь дней стартового мира, счёт обрамлён их датами,
|
||||
повтор заливки схлопывается в `ods.event_v`, а срок хранения ей не помеха — ODS
|
||||
хранит всё. Обе стороны равенства зафиксированы: одна кодом генератора, другая
|
||||
файлом в git. У межслойной сверки такой опоры нет ни с одной стороны.
|
||||
|
||||
## Срок жизни сырья
|
||||
## Срок жизни сырых данных
|
||||
|
||||
Сырьё в STG живёт трое суток реального времени и уходит само. Трое — это окно
|
||||
отладки: столько сырьё лежит на ноутбуке, чтобы менти успел разобрать полёты,
|
||||
после чего перестаёт занимать место. Нарезка — по дню загрузки, срок — по той же
|
||||
колонке `_load_ts`, снятие — целыми кусками (`ttl_only_drop_parts`). В DDL
|
||||
значение проставлено явно, чтобы поведение не зависело от умолчания версии.
|
||||
Сырые данные в STG живут трое суток реального времени и уходят сами. Трое — это
|
||||
окно отладки: столько сырые данные хранятся на ноутбуке, чтобы менти успел
|
||||
разобрать полёты, после чего перестаёт занимать место. Нарезка — по дню
|
||||
загрузки, срок — по той же колонке `_load_ts`, снятие — целыми кусками
|
||||
(`ttl_only_drop_parts`). В DDL значение проставлено явно, чтобы поведение не
|
||||
зависело от умолчания версии.
|
||||
|
||||
Нарезать сырьё по модельному дню события было бы соблазнительно — он единица
|
||||
переобработки и он же ключ партиции в ODS, — но чистку это ломает. Кусок
|
||||
снимается целиком, только когда в нём истекли все строки, а в партиции модельного
|
||||
дня лежит приехавшее в разное реальное время: мерж склеит куски разного возраста,
|
||||
самая свежая строка удержит весь кусок, и данные переживут срок неограниченно.
|
||||
Нарезать сырые данные по модельному дню события было бы соблазнительно — он
|
||||
единица переобработки и он же ключ партиции в ODS, — но чистку это ломает. Кусок
|
||||
снимается целиком, только когда в нём истекли все строки, а в партиции
|
||||
модельного дня лежит приехавшее в разное реальное время: мерж склеит куски
|
||||
разного возраста, самая свежая строка удержит весь кусок, и данные переживут
|
||||
срок неограниченно.
|
||||
|
||||
В партиции дня загрузки склейка идёт точно так же, и строки в ней тоже разного
|
||||
возраста — но не более чем на сутки, потому что партицию закрывает календарный
|
||||
день. Отсюда и оценка: сырьё живёт трое суток плюс хвост до суток, а не ровно
|
||||
трое. Оговорка про сроки: TTL исполняется на мержах, а не по будильнику, так что
|
||||
«уходит само» здесь обещано, а «уходит вовремя» — нет.
|
||||
день. Отсюда и оценка: сырые данные живут трое суток плюс хвост до суток, а не
|
||||
ровно трое. Оговорка про сроки: TTL исполняется на мержах, а не по будильнику,
|
||||
так что «уходит само» здесь обещано, а «уходит вовремя» — нет.
|
||||
|
||||
Модельного дня среди колонок сырья нет вовсе. Он свойство содержимого, а
|
||||
содержимое разбирает ODS — там `EventDate` и живёт, ключом партиции. Сырьё
|
||||
режется своими координатами: и разбор полётов, и переобработка фильтруют по
|
||||
`_load_ts`, попадая в ключ партиции, а не идя сплошным проходом. Фильтр по
|
||||
Модельного дня среди колонок STG нет вовсе. Он свойство содержимого, а
|
||||
содержимое разбирает ODS — там `EventDate` и живёт, ключом партиции. Сырые
|
||||
данные режутся по своим координатам: и разбор полётов, и переобработка фильтруют
|
||||
по `_load_ts`, попадая в ключ партиции, а не идя сплошным проходом. Фильтр по
|
||||
модельному дню вдобавок пропускал бы битые строки — у них дата не извлекается.
|
||||
|
||||
Две оси времени тут не совпадают намеренно. Ось модельного времени начинается в
|
||||
@@ -440,21 +440,21 @@ D0 и к реальному календарю не привязана; паке
|
||||
дате был бы просто сломан: он отсчитывает срок от реального «сейчас» и удалял бы
|
||||
эталонные дни прямо на входе.
|
||||
|
||||
В бою слой сырья иногда собирают на движке `Null` — тогда он не хранится вовсе.
|
||||
Такой вариант отвергнут: на стенде сырьё нужно для отладки, поэтому окно, а не
|
||||
ноль.
|
||||
В бою сырой слой иногда собирают на движке `Null` — тогда он не хранится вовсе.
|
||||
Такой вариант отвергнут: на стенде сырые данные нужны для отладки, поэтому окно,
|
||||
а не ноль.
|
||||
|
||||
## Таблица ошибок
|
||||
|
||||
`ods.event_errors` держит строки, не прошедшие строгий приём, вместе с их сырым
|
||||
текстом, метаданными доставки и классом брака. Ключи её собственные, потому что у
|
||||
брака нет разобранных полей: шардируется `cityHash64` сырой строки — `ClientID` у
|
||||
строки, которая не разобралась, взять неоткуда; нарезается по дню загрузки, как и
|
||||
сырьё; живёт месяц. Дольше сырья — намеренно: если брак истекает вместе с ним,
|
||||
разбираться к моменту разбирательства будет уже нечем. Снятие — целыми кусками
|
||||
(`ttl_only_drop_parts`), как у сырья и по той же причине: строки в партиции дня
|
||||
загрузки разного возраста не более чем на сутки, и куску незачем переживать
|
||||
срок из-за самой свежей строки.
|
||||
текстом, метаданными доставки и классом брака. Ключи её собственные, потому что
|
||||
у брака нет разобранных полей: шардируется `cityHash64` сырой строки —
|
||||
`ClientID` у строки, которая не разобралась, взять неоткуда; нарезается по дню
|
||||
загрузки, как и сырые данные; живёт месяц. Дольше сырых данных — намеренно: если
|
||||
брак истекает вместе с ними, разбираться к моменту разбирательства будет уже
|
||||
нечем. Снятие — целыми кусками (`ttl_only_drop_parts`), как у STG и по той же
|
||||
причине: строки в партиции дня загрузки разного возраста не более чем на сутки,
|
||||
и куску незачем переживать срок из-за самой свежей строки.
|
||||
|
||||
Класс брака лежит в колонке `error_class` типа `LowCardinality(String)`. Без неё
|
||||
в таблице копятся строки «что-то не так» без ответа на «что именно», а витрине
|
||||
@@ -500,13 +500,12 @@ kafka_offset)`: смотрят такую таблицу от класса, а
|
||||
|
||||
## Раскладка SQL
|
||||
|
||||
Исполняемые дагами запросы лежат по правилу
|
||||
`sql/<слой>/<объект>_<роль>.sql`. Слой — слой цели запроса: забор заказов в
|
||||
сырьё живёт в `sql/stg/orders_raw_load.sql`, разбор — в
|
||||
`sql/ods/order_parse.sql`. Даг называет файлы, передаёт параметры и задаёт
|
||||
порядок выполнения, но не хранит текст преобразований. Два выражения разбора
|
||||
делят локальный макрос Jinja в том же файле; подключаемого SQL-фрагмента между
|
||||
ними нет.
|
||||
Исполняемые дагами запросы лежат по правилу `sql/<слой>/<объект>_<роль>.sql`.
|
||||
Слой — слой цели запроса: забор заказов в STG живёт в
|
||||
`sql/stg/orders_raw_load.sql`, разбор — в `sql/ods/order_parse.sql`. Даг
|
||||
называет файлы, передаёт параметры и задаёт порядок выполнения, но не хранит
|
||||
текст преобразований. Два выражения разбора делят локальный макрос Jinja в том
|
||||
же файле; подключаемого SQL-фрагмента между ними нет.
|
||||
|
||||
SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. При разборе Python-кода
|
||||
обработчик DAG читает список DDL-файлов, загружает содержимое каждого
|
||||
@@ -524,10 +523,10 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
|---|---|
|
||||
| `00-databases.sql` | базы слоёв и зона справочников |
|
||||
| `05-dic-catalog.sql` | подложка над CSV-каталогом и словарь товаров поверх неё |
|
||||
| `10-stg-tables.sql` | чтецы топиков `hits` и `orders`, локальные и распределённые таблицы сырья обоих источников |
|
||||
| `10-stg-tables.sql` | чтецы топиков `hits` и `orders`, локальные и распределённые таблицы STG обоих источников |
|
||||
| `20-ods-tables.sql` | типизированное событие, версии заказа и обе таблицы ошибок |
|
||||
| `30-ods-views.sql` | актуальные события, текущие заказы и матвью разбора в ODS |
|
||||
| `40-stg-views.sql` | матвью приёма: чтец в сырьё |
|
||||
| `40-stg-views.sql` | матвью приёма: чтец в STG |
|
||||
| `45-dds-tables.sql` | физические модели заказа, сессии и карты идентичностей, доноры партиций |
|
||||
| `50-dds-views.sql` | публичные договоры сущностей DDS |
|
||||
| `55-dm-tables.sql` | физические витрины выручки, сверки и трафика, доноры дневных партиций |
|
||||
@@ -537,14 +536,14 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
правила порядка слоёв к ней не применяются.
|
||||
|
||||
Порядок остальных задают два правила. Первое: матвью принадлежит слою своей
|
||||
цели, а не источника, — разбор из STG в ODS лежит среди файлов ODS, потому
|
||||
что наполняет ODS. Второе: последней среди матвью цепочки Kafka создаётся
|
||||
матвью приёма, и потому она нарушает нумерацию слоёв. Kafka-движок начинает
|
||||
читать топик, когда к нему привязывают первую матвью; создай её раньше
|
||||
разбора — и всё, что доедет в зазоре, ляжет в сырьё и не попадёт в ODS
|
||||
никуда, ни в событие, ни в ошибки. На пустом топике зазор безвреден,
|
||||
поэтому первый прогон о нём не скажет. Проснётся он, когда тома ClickHouse
|
||||
снесены, а данные Kafka целы, — то есть на обычной отладке.
|
||||
цели, а не источника, — разбор из STG в ODS лежит среди файлов ODS, потому что
|
||||
наполняет ODS. Второе: последней среди матвью цепочки Kafka создаётся матвью
|
||||
приёма, и потому она нарушает нумерацию слоёв. Kafka-движок начинает читать
|
||||
топик, когда к нему привязывают первую матвью; создай её раньше разбора — и всё,
|
||||
что доедет в зазоре, ляжет в STG и не попадёт в ODS никуда, ни в событие, ни в
|
||||
ошибки. На пустом топике зазор безвреден, поэтому первый прогон о нём не скажет.
|
||||
Проснётся он, когда тома ClickHouse снесены, а данные Kafka целы, — то есть на
|
||||
обычной отладке.
|
||||
|
||||
Файлы применяет даг `world_initialize` с ноды 1 отдельной учётной записью
|
||||
`lifecycle`: один файл — одна последовательная задача
|
||||
@@ -579,13 +578,13 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
|---|---|---|
|
||||
| `stg` | `stg.hits_raw_kafka` | чтец топика `hits`, формат `RawBLOB` |
|
||||
| `stg` | `stg.hits_raw_rep` / `_dist` | сырая строка сообщения плюс метаданные доставки |
|
||||
| `stg` | `stg.hits_raw_mv` | наполняет сырьё из чтеца |
|
||||
| `stg` | `stg.hits_raw_mv` | наполняет STG из чтеца |
|
||||
| `stg` | `stg.orders_raw_kafka` | чтец топика `orders`, формат `RawBLOB`, только на ноде 1 и без матвью |
|
||||
| `stg` | `stg.orders_raw_rep` / `_dist` | сырое сообщение слепка, метаданные доставки и `_load_id` |
|
||||
| `ods` | `ods.event_rep` / `_dist` | типизированное широкое событие |
|
||||
| `ods` | `ods.event_v` | актуальная версия события с полями источника |
|
||||
| `ods` | `ods.event_errors_rep` / `_dist` | строки, не прошедшие строгий приём |
|
||||
| `ods` | `ods.event_mv`, `ods.event_errors_mv` | разбор сырья в событие и в ошибки |
|
||||
| `ods` | `ods.event_mv`, `ods.event_errors_mv` | разбор STG в событие и в ошибки |
|
||||
| `ods` | `ods.order_rep` / `_dist` | типизированные версии заказа |
|
||||
| `ods` | `ods.order_v` | текущая версия заказа на языке источника |
|
||||
| `ods` | `ods.order_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
|
||||
@@ -612,9 +611,8 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
|
||||
| `dic` | `dic.products` | словарь товаров поверх подложки |
|
||||
|
||||
Матвью разбора у заказов нет: срез сырья раскладывают по этим двум целям два
|
||||
`INSERT SELECT` одной задачи `parse_batch` из файла
|
||||
`sql/ods/order_parse.sql`.
|
||||
Матвью разбора у заказов нет: срез STG раскладывают по этим двум целям два
|
||||
`INSERT SELECT` одной задачи `parse_batch` из файла `sql/ods/order_parse.sql`.
|
||||
|
||||
В слое DDS построены событие, заказ, сессия и карта идентичностей. Событие
|
||||
остаётся представлением над ODS; заказ и сессия гасят повтор заменой партиции;
|
||||
@@ -655,11 +653,11 @@ DDL-словарь с файловым источником внутри `user_f
|
||||
**Проверка ко-локации заказов 22 августа 2026 года (#137).** Context7 и
|
||||
`system.settings` закреплённого ClickHouse 26.3 назвали одну и ту же тройку
|
||||
значений `parallel_distributed_insert_select`: `0` — выключено, `1` — `SELECT`
|
||||
на каждом шарде, `2` — `SELECT` и `INSERT` на каждом шарде; заводское — `2`.
|
||||
На живых данных видны и дефект, и починка: до правки 1189 из 1662 заказов
|
||||
лежали на обоих шардах, после явного нуля и повторного разбора того же
|
||||
`_load_id` — ни одного. Вершина версий уцелела: деловые поля `ods.order_v`
|
||||
сошлись с эталоном, посчитанным прямо из сырья.
|
||||
на каждом шарде, `2` — `SELECT` и `INSERT` на каждом шарде; заводское — `2`. На
|
||||
живых данных видны и дефект, и починка: до правки 1189 из 1662 заказов лежали на
|
||||
обоих шардах, после явного нуля и повторного разбора того же `_load_id` — ни
|
||||
одного. Вершина версий уцелела: деловые поля `ods.order_v` сошлись с эталоном,
|
||||
посчитанным прямо из STG.
|
||||
|
||||
**Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил,
|
||||
что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов
|
||||
@@ -683,19 +681,19 @@ DDL-словарь с файловым источником внутри `user_f
|
||||
потолком разбор отдаёт как `NULL`. Это ловит опыт, который берёт «заведомо
|
||||
далёкий» год: 2999-й не разбирается вовсе.
|
||||
|
||||
Про сам приём заказов снято следующее. Предикат формы провода разложил все
|
||||
13 661 строку сырья, приехавшую при исполнении #93, без исключений и без
|
||||
`NULL`: 13 653 годных и 8 брака, сумма сошлась с общим счётом по каждому
|
||||
`_load_id`, пересечение ветвей — ноль. Все восемь строк брака — версии одного
|
||||
заказа с отрицательным `total`; это дефект генератора, разобранный в
|
||||
[спецификации приёма](orders/ingestion.md), «Что проверено». На управляемой
|
||||
порции из пяти строк (две версии одного заказа плюс по одной строке каждого
|
||||
класса брака) годные ушли в `ods.order`, брак — в `ods.order_errors` с
|
||||
ожидаемыми классами. Обе версии легли в одну партицию и на один шард; `FINAL`
|
||||
через распределённую таблицу вернул одну строку — ту, что несла позднейшую
|
||||
версию, — тогда как физический счёт показывал две. Повтор задачи с тем же
|
||||
`_load_id` строку в `ods.order_v` и её `_load_ts` не изменил, а таблица ошибок
|
||||
записала тот же брак второй раз, как и обещано спецификацией приёма.
|
||||
Про сам приём заказов снято следующее. Предикат формы провода разложил все 13
|
||||
661 строку STG, приехавшую при исполнении #93, без исключений и без `NULL`: 13
|
||||
653 годных и 8 брака, сумма сошлась с общим счётом по каждому `_load_id`,
|
||||
пересечение ветвей — ноль. Все восемь строк брака — версии одного заказа с
|
||||
отрицательным `total`; это дефект генератора, разобранный в [спецификации
|
||||
приёма](orders/ingestion.md), «Что проверено». На управляемой порции из пяти
|
||||
строк (две версии одного заказа плюс по одной строке каждого класса брака)
|
||||
годные ушли в `ods.order`, брак — в `ods.order_errors` с ожидаемыми классами.
|
||||
Обе версии легли в одну партицию и на один шард; `FINAL` через распределённую
|
||||
таблицу вернул одну строку — ту, что несла позднейшую версию, — тогда как
|
||||
физический счёт показывал две. Повтор задачи с тем же `_load_id` строку в
|
||||
`ods.order_v` и её `_load_ts` не изменил, а таблица ошибок записала тот же брак
|
||||
второй раз, как и обещано спецификацией приёма.
|
||||
|
||||
Отдельно измерено, что **сводка вставки не годится в счётчики строк**: у
|
||||
запроса с `WHERE` `read_rows` считает прочитанное с диска, а не подошедшее, —
|
||||
@@ -758,27 +756,26 @@ DDL-словарь с файловым источником внутри `user_f
|
||||
вовсе, до 7 августа утверждение держалось на опыте владельца.
|
||||
- Упавшая матвью роняет вставку и останавливает потребление до починки.
|
||||
Проверено сносом цели — распределённой `ods.event_dist` — при живом чтеце: за
|
||||
двадцать секунд (сброс блока идёт за 7,5) в сырьё не приехало ничего, а
|
||||
офсет группы застыл с отставанием в одно сообщение. Цель вернули — сообщение
|
||||
доехало само, без повторной отправки, отставание ушло в ноль, событие
|
||||
разобралось. Сносить надо именно распределённую таблицу: вставка в
|
||||
`Distributed` кладёт блок в спул и сразу возвращает управление, так что на
|
||||
сносе локальной ошибка всплыла бы фоном и утверждение показалось бы
|
||||
опровергнутым.
|
||||
- `_load_ts` в `ods.event` — это метка исходной строки сырья, а не время
|
||||
двадцать секунд (сброс блока идёт за 7,5) в STG не приехало ничего, а офсет
|
||||
группы застыл с отставанием в одно сообщение. Цель вернули — сообщение доехало
|
||||
само, без повторной отправки, отставание ушло в ноль, событие разобралось.
|
||||
Сносить надо именно распределённую таблицу: вставка в `Distributed` кладёт
|
||||
блок в спул и сразу возвращает управление, так что на сносе локальной ошибка
|
||||
всплыла бы фоном и утверждение показалось бы опровергнутым.
|
||||
- `_load_ts` в `ods.event` — это метка исходной строки STG, а не время
|
||||
разбора. Сверено по `WatchID` на двух тысячах событий модельного дня,
|
||||
залитого дважды: у каждого события метка совпала с меткой одной из двух его
|
||||
доставок, а после `FINAL` — с меткой поздней. Случаев «метки нет среди
|
||||
доставок» ноль, то есть `now64()` в матвью разбора нет.
|
||||
- Пересозданная матвью пропускает ближайшие сообщения. Снятые и заново
|
||||
созданные матвью разбора при живом чтеце: сообщение, отправленное сразу
|
||||
после, легло в сырьё и не попало в ODS никуда — ни в событие, ни в ошибки;
|
||||
то же сообщение через минуту разобралось штатно. Воспроизведено дважды
|
||||
7 августа 2026 года. Это тот же зазор, о котором предупреждает нумерация
|
||||
файлов DDL, только приходит он с другой стороны — не при первом создании, а
|
||||
при замене матвью на работающем стенде. Практический вывод один: правишь
|
||||
матвью — не верь ближайшей отправке, повтори её. Чем именно держится
|
||||
задержка, не измерено; наблюдение записано как наблюдение.
|
||||
созданные матвью разбора при живом чтеце: сообщение, отправленное сразу после,
|
||||
легло в STG и не попало в ODS никуда — ни в событие, ни в ошибки; то же
|
||||
сообщение через минуту разобралось штатно. Воспроизведено дважды 7 августа
|
||||
2026 года. Это тот же зазор, о котором предупреждает нумерация файлов DDL,
|
||||
только приходит он с другой стороны — не при первом создании, а при замене
|
||||
матвью на работающем стенде. Практический вывод один: правишь матвью — не верь
|
||||
ближайшей отправке, повтори её. Чем именно держится задержка, не измерено;
|
||||
наблюдение записано как наблюдение.
|
||||
- Форма ключа `ods.event` принимается такой, как её задумала спека: выражение
|
||||
`intHash32(ClientID)` стоит в ключе сортировки `ReplacingMergeTree`, а
|
||||
`SAMPLE BY` — по тому же выражению. Вопрос стоял открытым в разделе 11
|
||||
@@ -811,18 +808,19 @@ DDL-словарь с файловым источником внутри `user_f
|
||||
|
||||
Оговорка к первому опыту, и она измеренная: граница проходит по пустоте. Запись
|
||||
Kafka с пустым значением (ноль байт) и запись-надгробие (значение `null`)
|
||||
читаются, двигают офсет потребителя и не дают строки вовсе — ни в сырьё, ни в
|
||||
таблицу ошибок; приём при этом не останавливается. Проверено 5 августа
|
||||
2026 года: в топик ушли четыре записи — пустая, обычная, надгробие, обычная, —
|
||||
в сырьё приехали две, а офсет группы сдвинулся на все четыре. Формат тут ни при
|
||||
чём: `LineAsString`, запасной по [ADR 0005](../adr/0005-event-ingestion.md), на
|
||||
том же наборе даёт ровно те же две строки и тот же офсет.
|
||||
читаются, двигают офсет потребителя и не дают строки вовсе — ни в STG, ни в
|
||||
таблицу ошибок; приём при этом не останавливается. Проверено 5 августа 2026
|
||||
года: в топик ушли четыре записи — пустая, обычная, надгробие, обычная, — в STG
|
||||
приехали две, а офсет группы сдвинулся на все четыре. Формат тут ни при чём:
|
||||
`LineAsString`, запасной по [ADR 0005](../adr/0005-event-ingestion.md), на том
|
||||
же наборе даёт ровно те же две строки и тот же офсет.
|
||||
|
||||
Свойство принято осознанно и переделкой не закрывается. Генератор стенда пустых
|
||||
сообщений не шлёт, приём от них не встаёт, а менять решённый формат из-за
|
||||
случая, которого стенд не производит, — размен не в ту сторону. Знать о нём
|
||||
стоит ровно затем, чтобы не искать пропавшую строку глазами: пропуск виден в
|
||||
самом сырье, `kafka_offset` лежит там колонкой, и дырка в офсетах — это он.
|
||||
самой таблице STG, `kafka_offset` лежит там колонкой, и дырка в офсетах — это
|
||||
он.
|
||||
|
||||
Оговорка к третьему опыту, и она сама непроверенная: `CREATE ... Distributed AS
|
||||
<локальная>` копирует умолчания колонок, а значит, умолчание, оказавшееся заодно
|
||||
@@ -865,7 +863,7 @@ Kafka с пустым значением (ноль байт) и запись-н
|
||||
- **`JSONExtract` читает `Decimal` и из числа JSON.** Массив
|
||||
`purchaseRevenue`, объявленный источником как `Array(Float64)`, разбирается
|
||||
сразу в `Array(Decimal(18, 2))`. Синтаксис подтвержден через Context7 и на
|
||||
ClickHouse 26.3; все суммы после ODS совпали с сырьем до копейки.
|
||||
ClickHouse 26.3; все суммы после ODS совпали с сырыми данными до копейки.
|
||||
|
||||
**Сказано по памяти, проверки нет.** Группа пуста: оба утверждения, ждавшие
|
||||
матвью разбора, закрыты опытами при исполнении #43 и переехали выше.
|
||||
Reference in new issue
Block a user