diff --git a/docs/architecture/dds/README.md b/docs/architecture/dds/README.md index 1da3e8f..8d37a5e 100644 --- a/docs/architecture/dds/README.md +++ b/docs/architecture/dds/README.md @@ -21,7 +21,9 @@ - **Заказ** (`dds.order`) — физическая сущность: одна строка — заказ в текущем состоянии, позиции массивами в строке, наполняется заменой партиций — [модель заказа](order.md). -- **Сессия** (`dds.session`) — зерно и правила сборки решает развилка #131. +- **Сессия** (`dds.session`) — расчётная сущность: одна строка — визит, + нарезку по правилу визита стенд делает сам, `VisitID` генератора — эталон + сверки; наполнение заменой дневной партиции — [модель сессии](session.md). - **Событие** (`dds.event_v`) — контракт над ODS, а не копия: язык бизнес-модели событию даёт развилка #132. - **Карта идентичностей** (`dds.identity_map`) — кука ↔ пользователь @@ -47,8 +49,8 @@ ## Открытые решения -- Сессии, контракт `dds.event_v`, сборка `dds.identity_map` — развилки - #131–#133 карты #127. +- Контракт `dds.event_v`, сборка `dds.identity_map` — развилки #132–#133 + карты #127. - Запуск трансформаций и механика замены партиций на кластере — #134 и этап 5. - Витрины DM — #135. diff --git a/docs/architecture/dds/session.md b/docs/architecture/dds/session.md new file mode 100644 index 0000000..dde6acb --- /dev/null +++ b/docs/architecture/dds/session.md @@ -0,0 +1,151 @@ +# Модель сессии в DDS + +Учебный результат: менти строит расчётную сущность, которой нет в потоке, — +режет события на сессии по правилу визита, чеканит идентификатор и сверяет +свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её +играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый +`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID` +генератора использует только как эталон самопроверки — иначе сборка +`dds.session` выродилась бы в `GROUP BY` по чужому полю и центральный урок +слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем +считать то, что уже дано колонкой». + +## Зерно + +Одна строка — один визит ([CONTEXT.md](../../../CONTEXT.md)): подряд идущие +события одной куки без пауз длиннее 30 минут, не пересекающие границу суток. +В нарезку входят все три типа событий — торговые садятся на свои страницы +и держат сессию наравне с просмотрами. События внутри сессии +упорядочиваются по метке времени события (`UTCEventTime` — опорная колонка +приёма). + +Граница суток проходит по модельному дню счётчика, и он у события уже есть: +`EventDate` — свойство содержимого. Раз сессия сутки не пересекает, у всех +её событий `EventDate` один; он и становится **днём сессии** — колонкой +партиции, без пересчёта. + +## Идентификатор + +Идентификатор сессии в природе не существует: сессия — расчётная сущность, +и id ей чеканит тот, кто сессионизирует. `VisitID` — такой же суррогат, +отчеканенный генератором в роли Метрики; наш — `session_id`, +детерминированный хеш от пары «кука + начало сессии» (`cityHash64`). +Детерминированность обязательна: партиция дня пересобирается заменой, и +повторный прогон обязан отчеканить те же id — счётчик или UUID это правило +ломают. + +`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка: +витрины сессий читают таблицу напрямую, их зерно совпадает с зерном модели, +и в соединениях id не участвует — события его не несут, мост к заказам живёт +на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со +стендовой и назвать расхождение одним числом) и разбор DQ-расхождений. + +## Состав строки + +Боевой образец — таблица визитов Метрики, парная к нашему формату хита; по +ней состав и собран: + +- **ключи и времена**: кука, `session_id`, начало и конец сессии, + длительность, день сессии. Времена — в поясе счётчика по правилу слоя; +- **счётчики**: событий всего, просмотров, корзин, покупок; +- **страницы**: входная и выходная — как `StartURL`/`EndURL` визита; полного + списка страниц нет и у Метрики, топы считаются по событиям; +- **атрибуция**: UTM-метки первого события сессии — так визит атрибутирует + Метрика. В v1 витрина брала последнее касание — заменено осознанно; +- **паспорт куки**: категория устройства (расшифрованная, язык бизнес-модели) + и город — у куки они постоянны, берутся с первого события; +- **служебные**: `_load_id` и `_load_ts` собравшего запуска — правило слоя + ([модель заказа](order.md), «Служебные колонки»). + +Осознанные расхождения с образцом: **денег в сессии нет** — визит Метрики +несёт клиентскую выручку, у нас деньги считает только бэкенд (спека, раздел +7), а клиентская сумма живёт в сверке; **целей нет** — их место заняли +счётчики торговых событий. + +## Движок и наполнение + +`MergeTree` без колонки версии, по образцу заказа: партиция — день сессии, +ключ сортировки — кука и начало сессии, шардирование распределённой таблицы — +`cityHash64(ClientID)`, тем же выражением, что у событий, — ко-локация с +`dds.identity_map` ([дока хранилища](../storage.md), «Раскладка по шардам»). +Совпадение ключа с событиями не случайно: все события куки лежат на одном +шарде, поэтому сессионизация локальна по природе — капкан смены ключа при +вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет. + +Сборка — дневной батч после прихода слепка заказов: хранилище выше ODS по +смыслу батчевое, раз второй источник приходит раз в сутки. К этому моменту +события дня полны, и сессии только что прожитого дня собираются один раз и +целиком; идемпотентность держит замена партиции. В отличие от заказов, где +батч пересобирает до K «дышащих» дней, у сессий пересобирается только одна +партиция: день самодостаточен и замерзает сразу (правило спеки, раздел 6). +Внутри живого дня `dds.session` про сегодня честно молчит — «сегодня» +смотрят через `dds.event_v`; сессия и по смыслу не финальна, пока день не +прожит. + +Источник сборки — `dds.event_v`; его контракт решает развилка #132, сессии +нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки. +Механика запуска — где собирается партиция и как подменяется на кластере — +не входит: это устройство трансформаций (#134, этап 5). + +## Сверка с эталоном + +Счётная DQ-проверка с зерном «кука × день»: число сессий нашей нарезки +равно `uniqExact(VisitID)` по тем же событиям. Счёт по Distributed ловит и +склеенную, и разрезанную сессию — мимо него проходит лишь взаимная +компенсация ошибок внутри одной куки за день; эталон в модель не тащим — +строка сессии остаётся нашей, а не генераторной. Поштучное сопоставление +границ — материал лабы, где менти разбирает найденное расхождение руками. Дом проверки — +контур DQ этапа 4. Сверке нужен `VisitID`, а сборке сессий — нет; останется +ли он в контракте `dds.event_v` или проверка пойдёт за ним в ODS — решает +развилка #132. + +## Читатели + +- Витрина обзора сессий `session_overview_v`: длительности, глубина, + вход/выход; читает таблицу напрямую, зерно совпадает (#135). +- DQ-сверка с эталоном (см. выше). +- Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда, + потом сравнивает — с эталоном и со стендовой. + +## Не входит + +- Механика запуска трансформации и замены партиций на кластере — #134 и + этап 5. +- Контур проверок качества (даг DQ, `dm.dq_summary`) — отдельный разговор + этапа 4. +- Форма витрин над сессиями — #135. + +## Отклонённые варианты + +- **Готовый `VisitID` вместо своей нарезки.** Сборка выродилась бы в + `GROUP BY` по чужому полю: центральный урок слоя съеден, сверка с эталоном + осталась бы без предмета. Боевой довод «Метрика теперь отдаёт `VisitID` в + хитах» честно рассказывается в лабе — после того как менти собрал сам. +- **Гибрид: сборка по `VisitID`, DQ пересчитывает нарезку.** Сторож на + нарезку, которой сборка не делает; та же работа, что в варианте «считать + сам», но урок спрятан в проверку. +- **Представление вместо таблицы.** Оконная сессионизация на каждое чтение; + урок «слой — контракт, а не копия» уже закреплён за `dds.event_v` — доводы + модели заказа, здесь они только сильнее. +- **Счётчик или UUID вместо детерминированного хеша.** Пересборка партиции + выдала бы другие id — замена перестала бы быть идемпотентной. +- **`VisitID` колонкой в строке сессии.** Чужой чекан в своей модели: наша + сессия обязана уметь расходиться с эталоном, на этом стоит сверка; + поштучное сопоставление — лаба, не таблица. +- **Массив посещённых страниц.** Вес без читателя: вход и выход — как у + визита Метрики, топы страниц считает витрина по событиям. +- **UTM последнего касания (как в v1).** Визит атрибутируется своим первым + хитом; последнее касание — атрибуция другого уровня, не сессионного. +- **Деньги в сессии.** Выручка — только по бэкенду; клиентская сумма живёт + в сверке (спека, разделы 4 и 7). + +## Связанные решения + +- [Модель заказа](order.md) — прецедент замены партиций в DDS и правило + служебных колонок слоя. +- [Дока хранилища](../storage.md) — раскладка по шардам, конвенции имён, + часовые пояса. +- Мастер-спека, разделы 1.2 и 7 — `VisitID` как эталон самопроверки + (принятое отступление приёмки), карта таблиц. +- [CONTEXT.md](../../../CONTEXT.md) — «Визит», «Граница суток», «День + активности».