docs(adr): решён приём заказов — пакетный забор слепка

- Зачем:
  - развилка этапа 3 стояла нерешённой прямо в разделе 7 мастер-спеки: нужен
    ли слепку слой сырья и как заказы попадают из топика в хранилище (#70).
- Что:
  - заведён ADR 0008 — байтовый чтец без матвью, слой сырья у заказов
    остаётся, в ods.order_snapshot пишет шаг Airflow заменой партиции; топик
    orders в одну партицию, чтец на clickhouse-01 без ON CLUSTER.
  - мастер-спека приведена в соответствие, разделы 6, 7, 9, 11, 12: сравнение
    двух приёмов переписано на «поток против слепка», сенсор дневного батча
    снят, первый даг переехал с этапа 5 на этап 3.
  - в CONTEXT.md заведены «слепок», «окно изменяемости», «пакетный забор».
- Проверка:
  - решение сверено по документации ClickHouse через MCP Context7 12 августа
    2026 года; что осталось замерить на стенде — списком в конце ADR 0008.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 21:57:37 +03:00
co-authored by Claude Opus 5
parent a3d852cad9
commit 1fb219f836
3 changed files with 212 additions and 29 deletions
+42 -29
View File
@@ -307,12 +307,17 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
- Роли нод: нода 1 — инициатор DDL и подключение Airflow; **Superset — на
ноду 2**. Это осознанная ловушка правильных ошибок: забытый ON CLUSTER или
VIEW поверх локальной таблицы проявляются в дашборде сами.
- **Приём Kafka**: Kafka-таблицы и MV на обеих нодах, одна consumer group,
2 партиции на топик; MV пишут в Distributed-цели. Раскладку решает ключ:
- **Приём Kafka**: у событий — Kafka-таблицы и MV на обеих нодах, одна
consumer group, 2 партиции на топик `hits`; MV пишут в Distributed-цели.
У заказов приём другого рода — пакетный забор без MV, чтец на одной ноде,
топик `orders` в одну партицию ([ADR 0008](../adr/0008-order-ingestion.md)).
Раскладку в обоих случаях решает ключ:
события — по `cityHash64(ClientID)` (см. 1.3), заказы —
`cityHash64(order_id)`, сырьё STG — `cityHash64(сырой строки)`; полный
список и доводы — в [доке хранилища](../architecture/storage.md). Урок:
«какая нода читала топик — меняется между прогонами, куда легли данные — нет».
список и доводы — в [доке хранилища](../architecture/storage.md). Урок
«какая нода читала топик — меняется между прогонами, куда легли данные —
нет» живёт на событиях: при пакетном заборе читает та нода, которую
спросили.
- **Приём строгий**: пять опорных колонок — `WatchID`, `VisitID`, `ClientID`,
`EventDate`, `UTCEventTime` — разбираются как `Nullable`, а набор ключей
сообщения сверяется с контрактным; строка с NULL среди опорных колонок
@@ -382,8 +387,9 @@ README.
| Слой | Объект | Что это |
|---|---|---|
| Kafka | `hits`, `orders` | два топика, по 2 партиции |
| STG | `stg.hits_raw_kafka`, `stg.hits_raw` + MV; для orders — развилка этапа 3, не решена (ниже) | сырые строки, Kafka Engine на обеих нодах |
| Kafka | `hits`, `orders` | два топика: `hits` — 2 партиции, `orders` — одна |
| STG | `stg.hits_raw_kafka`, `stg.hits_raw` + MV | сырые строки событий, Kafka Engine на обеих нодах |
| STG | `stg.orders_raw_kafka`, `stg.orders_raw`, без MV | сырые строки слепка; чтец на ноде 1, забирает пакетный шаг |
| ODS | `ods.event` (+`_errors`) | типизированное широкое событие, ReplacingMergeTree |
| ODS | `ods.order_snapshot` (+`_errors`) | слепки заказов как приехали, партиция по `snapshot_date`, без дедупа |
| DDS | `dds.session` | сборка сессий из событий (наследник `dds.click`) |
@@ -397,14 +403,16 @@ README.
суффиксов; она же задаёт служебные колонки, нарезку и срок хранения сырья —
см. [доку хранилища](../architecture/storage.md).
Как принимаются заказы — развилка этапа 3, и она не решена. Событиям выбран
приём сырья байтами с разбором функциями ([ADR 0005](../adr/0005-event-ingestion.md));
заказам этот же способ идёт только вместе с ответом на вопрос, нужен ли им слой
сырья вообще — у них слепок, а не поток. Нужен — и типизированный чтец даст двух
чтецов на один топик, а такую схему ADR 0005 отверг; не нужен — и слои
перестают быть единообразными. Разбирать грилингом, когда дойдём до заказов;
как учебное сравнение двух способов приёма это записано и в опорных точках
раздела 12.
Заказы принимаются **пакетным забором** ([ADR
0008](../adr/0008-order-ingestion.md)): чтец топика байтовый, как у событий, но
матвью к нему не привязана, и сырьё забирает шаг, которым управляет Airflow —
он же вставляет прочитанное в `stg.orders_raw`, разбирает в типизированный
слепок и заменяет партицию дня в `ods.order_snapshot`. Тот же даг проигрывает
модельный день генератором, поэтому переливается ровно то, что он положил в
топик. Слой сырья у заказов остаётся: без него `ods.order_snapshot` повторил бы
его роль, а обещание идемпотентности повисло бы — матвью партиций не заменяет.
Стенд получает от этого два режима приёма рядом, поток и слепок, и сравнение
из опорных точек раздела 12 переформулировано под них.
Состав служебных колонок задаёт дока хранилища. Спеке важны два следствия:
`ods.event` и `ods.order_snapshot` получают метку загрузки `_load_ts`, и у
@@ -495,7 +503,7 @@ v2 стартует пустым, поэтому объём ниже — это
| Генератор | с нуля: модель v1 не переносится (другая модель данных, плюс известные проблемы производительности v1); широкое событие, таксономия, анонимность, N:1, заказы слепками, расхождения A–D, каталог; масштаб — ~4–5 тыс. строк с тестами | L |
| Инфраструктура | compose: 2 ноды CH + keeper + остальной стенд; конфиги кластера, макросы; make/скрипты | M — ~10–12 файлов |
| SQL | DDL по слоям и ролям (ON CLUSTER, Replicated*, Distributed; раскладка файлов — в доке хранилища) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность |
| Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets), world_init/next_day, helpers | M — ~5–6 файлов |
| Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка), world_init/next_day, helpers; первый даг — переливка слепка — приходит раньше, этапом 3 | M — ~5–6 файлов |
| Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла |
| Эталонный мир | пересборка снимка на месте, счётчики описи, чек-скрипты | M–L |
| Мониторинг | дашборды Grafana «данные», «кластер», «запросы»; ClickHouse источником данных, панели на SQL; Prometheus тонким полом (ADR 0002) | M — конфиги и дашборды |
@@ -537,10 +545,11 @@ v2 стартует пустым, поэтому объём ниже — это
целиком). В конце этапа фиксируется маленький стартовый мир для
стабильных приёмок следующих этапов (полная пересборка эталонного мира —
отдельный этап 7).
3. Заказы и каталог: генератор слепков, STG/ODS/DDS заказа, словарь.
3. Заказы и каталог: генератор слепков, STG/ODS/DDS заказа, словарь и первый
настоящий даг — проигрыш модельного дня плюс переливка слепка ([ADR
0008](../adr/0008-order-ingestion.md)).
4. Трансформации и витрины: сессии, identity_map, выручка, сверка A+C.
5. Airflow: `etl_pipeline` (партиционная переобработка, ожидание дневного
батча заказов — сенсор/Datasets).
5. Airflow: `etl_pipeline` (партиционная переобработка).
6. Расхождения B+D и опоздания; счётчики описи.
7. Эталонный мир: опись и пересборка снимка, чек-скрипты; CI-генерация
на amd64 и arm64.
@@ -603,9 +612,13 @@ Kafka Engine на двух нодах снят с этого списка при
- Размер артефакта эталонного мира после пересборки.
- Спорные API (Airflow Datasets/сенсоры, ClickHouse DDL) — перед кодом
сверять через MCP Context7 (правило AGENTS.md).
- Airflow 3.x: версия фиксируется на этапе 1 (каркас); DAG'и этапа 5 пишутся
под API третьей версии (Datasets → Assets) — актуальные операторы и сенсоры
проверить через Context7.
- Airflow 3.x: версия фиксируется на этапе 1 (каркас); DAG'и пишутся под API
третьей версии (Datasets → Assets) — актуальные операторы проверить через
Context7. Первый даг приходит этапом 3, а не 5 ([ADR
0008](../adr/0008-order-ingestion.md)).
- Пакетный забор слепка из Kafka: коммит офсетов прямым чтением, хватает ли
одного чтения на слепок дня, доступны ли при нём виртуальные колонки
доставки — список и ответы в [ADR 0008](../adr/0008-order-ingestion.md).
- Генератор: рабочее решение — Python с производительной архитектурой
(батчевая генерация вместо посточной, быстрая JSON-сериализация,
распараллеливание по модельным дням). Читаемость генератора для менти —
@@ -653,14 +666,14 @@ v2, этап 0).
синтетическая постановка — осознанный приём;
- лекция «`Sign` и CollapsingMergeTree»: почему на стенде `sum(Sign)` =
`count()`, а в бою — нет; частый вопрос на собеседованиях;
- два способа принять топик, рядом на одном стенде: сырьё байтами с разбором
функциями (`hits`, [ADR 0005](../adr/0005-event-ingestion.md)) против
типизированного чтеца с `kafka_handle_error_mode` — сравнение цены и
наблюдаемости как задание. **Развилка этапа 3, не решена**: типизированный
чтец идёт заказам только вместе с ответом на вопрос, нужен ли им слой сырья.
Нужен — и чтецов на один топик станет два, а эту схему ADR 0005 отверг; не
нужен — и слои перестают быть единообразными. Разбирать грилингом, когда
дойдём до заказов;
- два способа принять топик, рядом на одном стенде: **поток против слепка,
push против pull**. События тянет матвью на лету ([ADR
0005](../adr/0005-event-ingestion.md)), слепок заказов забирает пакетный шаг
по команде дага ([ADR 0008](../adr/0008-order-ingestion.md)); чтец в обоих
случаях байтовый, так что различает их ровно режим. Сравнение цены,
наблюдаемости и того, чем платит каждый режим, — как задание. Третий способ,
типизированный чтец с `kafka_handle_error_mode`, на стенде не живёт: он
отвергнут обоими ADR, и остаётся материалом для рассказа;
- матвью как рабочий механизм, а не диковина: их видно на приёме и на сборке
ODS, а пакетная работа начинается выше. Отдельным заданием — как читать из ODS
последние версии, через `FINAL` или оконной функцией: что нагляднее, решаем на