Files
clickstream-data-platform/docs/architecture/dds/session.md
T
ddadminandClaude Opus 5 176cf5520d feat(dds): добавлена модель сессии и нарезка визитов окнами
Зачем:
- Дать слою расчётную сущность, которой в потоке нет: стенд режет визиты сам,
  а VisitID генератора остаётся эталоном самопроверки.
- Отделить прожитый день от живого — иначе визит, начатый в 23:50, замёрз бы
  обрезанным навсегда: пересобирать замёрзший день слой не станет.

Что:
- Добавлены dds.session_rep/_dist, двойник-донор dds.session_stage_rep/_dist и
  публичный договор dds.session_v.
- Нарезка стоит на двух окнах: первое ставит шов там, где пауза длиннее
  тридцати минут, второе считает нарастающий итог швов — номер визита. Границу
  суток держит разбиение окна, отдельной проверки ей не нужно.
- Порядок в окнах полный, по метке времени и event_id: сортировок две, и при
  равных метках они разошлись бы, разорвав сессию надвое.
- Объём прогона считается от первой дыры в слое по последний прожитый день;
  прожитый день выводится из позиции мира и первого дня событий. Собирать
  нечего — группа задач пропускается, а не зеленеет впустую.
- В dds_transform появилась группа session рядом с заказом; зависимости по
  данным между ними нет.
- Потолок max_active_tasks: размноженная замена без него выносит планировщика
  за отведённые ему 640 МиБ.
- parallel_distributed_insert_select у сессий не назначается вслух — ключ
  шардирования у источника и цели один по контракту ко-локации.
- Записано, что позиция отвечает за «прожит», а не за «доехал»: сторож хвоста
  потока приезжает отдельным тикетом.

Проверка:
- make lint
- make smoke (21 проверка), make check-clickhouse (10), make check-services
- Стартовый мир, пересобранный с нуля: 79367 сессий за восемь дней;
  расхождение с uniqExact(VisitID) по паре «кука × день» — ноль; все 401185
  событий ровно в одной сессии; session_id уникальны.
- Ни одной паузы длиннее тридцати минут внутри сессии и ни одной пары соседних
  визитов куки, разделённой меньшим промежутком.
- Повтор после DROP PARTITION ALL: те же session_id и то же содержимое строк.
- Снесённая руками партиция 2026-06-04: следующий прогон взял хвост от неё,
  дни до дыры не тронуты.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:56:44 -04:00

14 KiB
Raw Blame History

Модель сессии в DDS

Учебный результат: менти строит расчётную сущность, которой нет в потоке, — режет события на сессии по правилу визита, чеканит идентификатор и сверяет свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый VisitID прямо в хитах. Поэтому стенд считает сессии сам, а VisitID генератора использует только как эталон самопроверки — иначе сборка сессий выродилась бы в GROUP BY по чужому полю и центральный урок слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем считать то, что уже дано колонкой».

Зерно

Одна строка — один визит (CONTEXT.md): подряд идущие события одной куки без пауз длиннее 30 минут, не пересекающие границу суток. В нарезку входят все три типа событий — торговые садятся на свои страницы и держат сессию наравне с просмотрами. События внутри сессии упорядочиваются по метке времени события (UTCEventTime — опорная колонка приёма).

Граница суток проходит по модельному дню счётчика, и он у события уже есть: EventDate — свойство содержимого. Раз сессия сутки не пересекает, у всех её событий EventDate один; он и становится днём сессии — колонкой партиции, без пересчёта.

Идентификатор

Идентификатор сессии в природе не существует: сессия — расчётная сущность, и id ей чеканит тот, кто сессионизирует. VisitID — такой же суррогат, отчеканенный генератором в роли Метрики; наш — session_id, детерминированный хеш от пары «кука + начало сессии» (cityHash64). Детерминированность обязательна: партиция дня пересобирается заменой, и повторный прогон обязан отчеканить те же id — счётчик или UUID это правило ломают.

session_id — не ключ сортировки и не ключ шардирования, а колонка-ручка: витрины сессий читают dds.session_v, их зерно совпадает с зерном модели, и в соединениях id не участвует — события его не несут, мост к заказам живёт на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со стендовой и назвать расхождение одним числом) и разбор DQ-расхождений.

Состав строки

Боевой образец — таблица визитов Метрики, парная к нашему формату хита; по ней состав и собран:

  • ключи и времена: кука, session_id, начало и конец сессии, длительность, день сессии. Времена — в поясе счётчика по правилу слоя;
  • счётчики: событий всего, просмотров, корзин, покупок;
  • страницы: входная и выходная — как StartURL/EndURL визита; полного списка страниц нет и у Метрики, топы считаются по событиям;
  • атрибуция: UTM-метки первого события сессии — так визит атрибутирует Метрика. В v1 витрина брала последнее касание — заменено осознанно;
  • паспорт куки: категория устройства (расшифрованная, язык бизнес-модели) и город — у куки они постоянны, берутся с первого события;
  • служебные: _load_id и _load_ts собравшего запуска — правило слоя (модель заказа, «Служебные колонки»).

Осознанные расхождения с образцом: денег в сессии нет — визит Метрики несёт клиентскую выручку, у нас деньги считает только бэкенд (спека, раздел 7), а клиентская сумма живёт в сверке; целей нет — их место заняли счётчики торговых событий.

Движок и наполнение

dds.session_rep — ReplicatedMergeTree без колонки версии, по образцу заказа: партиция — день сессии, ключ сортировки — кука и начало сессии. dds.session_dist шардирует по cityHash64(client_id), тем же выражением, что события, — это ко-локация с dds.identity_map_rep (дока хранилища, «Раскладка по шардам»). Совпадение ключа с событиями не случайно: все события куки лежат на одном шарде, поэтому сессионизация локальна по природе — капкан смены ключа при вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.

Сборка — дневной батч: хранилище выше ODS по смыслу батчевое, раз второй источник приходит раз в сутки. Собирается день, который мир уже прожил, и собирается один раз и целиком; какой день прожит, слой спрашивает у позиции мира (дневной конвейер, «Объём прогона»). Идемпотентность держит замена партиции. В отличие от заказов, где батч пересобирает до K «дышащих» дней, у сессии дышащего окна нет: прожитый день самодостаточен (правило спеки, раздел 6). Второй раз он соберётся только вынужденно — когда упавшая замена оставила позади дыру и хвост от неё накрыл соседние дни. Дней в одном прогоне при этом бывает несколько — столько, сколько мир успел прожить без слоя. Внутри живого дня модель сессии про сегодня честно молчит — «сегодня» смотрят через dds.event_v; сессия и по смыслу не финальна, пока день не прожит.

Публичная поверхность — dds.session_v: обычное представление с явным списком деловых и собственных служебных колонок над dds.session_dist. Роли объектов и доступ следуют общей конвенции, «Имена объектов».

Источник сборки — dds.event_v (контракт события): сессии нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки. Механика запуска — где собирается партиция и как подменяется на кластере — не входит: это устройство дневного конвейера.

Сверка с эталоном

Счётная DQ-проверка с зерном «кука × день»: число сессий нашей нарезки равно uniqExact(VisitID) по тем же событиям. Счёт по Distributed ловит и склеенную, и разрезанную сессию — мимо него проходит лишь взаимная компенсация ошибок внутри одной куки за день; эталон в модель не тащим — строка сессии остаётся нашей, а не генераторной. Поштучное сопоставление границ — материал лабы, где менти разбирает найденное расхождение руками. Дом проверки — контур DQ этапа 4. Сверке нужен VisitID, а сборке сессий — нет; контракт dds.event_v его скрывает, и за эталоном проверка идёт в ods.event_v (контракт события).

Читатели

  • Витрина обзора сессий session_overview_v: длительности, глубина, вход/выход; читает dds.session_v, зерно совпадает (#135).
  • DQ-сверка с эталоном (см. выше).
  • Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда, потом сравнивает — с эталоном и со стендовой.

Не входит

  • Механика запуска трансформации и замены партиций на кластере — дневной конвейер.
  • Контур проверок качества (даг DQ, dm.dq_summary) — отдельный разговор этапа 4.
  • Форма витрин над сессиями — #135.

Отклонённые варианты

  • Готовый VisitID вместо своей нарезки. Сборка выродилась бы в GROUP BY по чужому полю: центральный урок слоя съеден, сверка с эталоном осталась бы без предмета. Боевой довод «Метрика теперь отдаёт VisitID в хитах» честно рассказывается в лабе — после того как менти собрал сам.
  • Гибрид: сборка по VisitID, DQ пересчитывает нарезку. Сторож на нарезку, которой сборка не делает; та же работа, что в варианте «считать сам», но урок спрятан в проверку.
  • Одно динамическое представление вместо физической таблицы. Оконная сессионизация на каждое чтение; урок «слой — контракт, а не копия» уже закреплён за dds.event_v — доводы модели заказа, здесь они только сильнее. Публичное dds.session_v поверх готовой физической модели ничего заново не считает.
  • Счётчик или UUID вместо детерминированного хеша. Пересборка партиции выдала бы другие id — замена перестала бы быть идемпотентной.
  • VisitID колонкой в строке сессии. Чужой чекан в своей модели: наша сессия обязана уметь расходиться с эталоном, на этом стоит сверка; поштучное сопоставление — лаба, не таблица.
  • Массив посещённых страниц. Вес без читателя: вход и выход — как у визита Метрики, топы страниц считает витрина по событиям.
  • UTM последнего касания (как в v1). Визит атрибутируется своим первым хитом; последнее касание — атрибуция другого уровня, не сессионного.
  • Деньги в сессии. Выручка — только по бэкенду; клиентская сумма живёт в сверке (спека, разделы 4 и 7).

Связанные решения

  • Модель заказа — прецедент замены партиций в DDS и правило служебных колонок слоя.
  • Дока хранилища — раскладка по шардам, конвенции имён, часовые пояса.
  • Мастер-спека, разделы 1.2 и 7 — VisitID как эталон самопроверки (принятое отступление приёмки), карта таблиц.
  • CONTEXT.md — «Визит», «Граница суток», «День активности».