Зачем: два новых режима роста мира не имели уроков, а метадокументы курса не знали о новом маршруте. Что: лаба 07 «Следующий день и границы времени» — инкремент дня, сверка manifest, переходящие визиты запросом в dds.event, включение расписания на один запуск, цена роста full_refresh; лаба 08 «Живой поток и свежесть данных» — расслоение свежести слоёв, стоп/продолжение генератора, users < sessions, врезка про модельное время; LEARNING_PLAN и README курса согласованы с маршрутом 0–8; в CONTEXT.md починена ссылка «урок 7» (теперь ведёт на врезку лабы 08). Числа со стенда помечены маркером «сверить-на-стенде». Проверка: обе лабы по шаблону LESSON_STANDARD (6 секций, явный «верни как было»); внутренние ссылки разрешаются; git diff --check чистый. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 KiB
Лаба 07. Следующий день и границы времени
Формат: практика — добавишь к эталонному миру ещё один модельный день, проверишь стык дней и ненадолго включишь расписание. Пререквизит: пройден урок 6; ты умеешь запускать DAG в Airflow, читать граф задач и смотреть дневную динамику в Superset. Эталонный путь:
airflow/dags/world_next_day_dag.py.Поток данных одной строкой:
эталонный мир (3 дня) → world_next_day → Kafka → STG → etl_pipeline → ODS → DDS → DM (4 дня)О чём лаба простыми словами: ночью в хранилище часто приезжает очередной день данных. Ты добавишь такой день и проверишь место, где две суточные порции соприкасаются.
1. Зачем и где в проде
Пакетный инкремент — это новая порция данных за ограниченный период. Например, ночью в хранилище приезжают события за вчера. Старые дни остаются на месте, а пайплайн добавляет и проверяет новый.
Граница порции требует отдельного внимания. Визит может начаться перед полуночью, а закончиться после неё. Если обрабатывать каждый день как изолированный файл, половины такого визита окажутся в разных запусках. Отсюда появляются проверки стыков, запоздавшие данные и пересчёт хвоста прошлого дня.
В этой лабе world_next_day добавляет ровно 24 модельных часа. Сам генератор
накапливает контрольные числа, а затем DAG запускает полный пересчёт
etl_pipeline. На маленьком стенде такой путь понятен и повторяем.
В проде иначе. Суточную задачу обычно привязывают к интервалу данных: повторный запуск того же интервала должен дать тот же результат. Здесь каждый ручной запуск сдвигает правую границу мира ещё на сутки. Это удобно для тренажёра, но не делает DAG идемпотентным.
2. Руки: добавляем день 4
Подготовь стенд по
канонической инструкции курса.
После успешного world_init в мире есть три модельных дня.
Запускаем world_next_day
Открой Airflow: http://localhost:8080 (логин admin, пароль admin).
Найди DAG world_next_day и запусти его через Trigger DAG с пустой формой.
Параметры ему не нужны.
В графе должны позеленеть задачи:
check_etl_not_paused
→ precheck_next_day
→ run_next_day
→ trigger_etl
→ check_after_etl
run_next_day дописывает день 4 в Kafka. Затем trigger_etl запускает
etl_pipeline с полным пересчётом DDS и DM. Последняя задача сверяет ClickHouse
с manifest — паспортом мира, где лежат границы времени и контрольные числа.
Если check_etl_not_paused красная, сними паузу с etl_pipeline и повтори
запуск. world_next_day не может вызвать зависимый DAG, пока тот стоит на паузе.
Сверяем manifest
После зелёного прогона выполни:
docker compose run --rm --no-deps generator \
python -m clickstream_generator.startup_history_artifact kafka-manifest-summary
Команда печатает одну строку с полями в таком порядке:
events visits users min_event_timestamp max_event_timestamp model_t0 model_t_end profile
Сверь результат с таблицей. Значения будут вписаны после контрольного прогона эталонного мира:
| Поле | Ожидаем после дня 4 |
|---|---|
events |
<события-день-4> |
visits |
<визиты-день-4> |
users |
<пользователи-день-4> |
min_event_timestamp |
<минимальное-время> |
max_event_timestamp |
<максимальное-время> |
model_t0 |
<левая-граница> |
model_t_end |
<правая-граница-дня-4> |
profile |
<профиль> |
Здесь числа накопительные: events, visits и users относятся ко всему миру
от model_t0 до model_t_end, а не только к четвёртому дню.
Теперь проверь все завершённые порции и их стыки:
make generated-history-chain-check
Проверка должна закончиться без ошибки. Она подтверждает, что каждая порция не пуста, контрольные числа STG совпадают с manifest, после правой границы нет хвоста, а переходящие визиты не потеряли связанные записи.
Находим переходящие визиты
Открой SQL-консоль http://localhost:9123/play (пользователь default, пароль
123456) и выполни:
SELECT
click_id,
min(event_ts) AS visit_start,
max(event_ts) AS visit_end
FROM dds.event
WHERE event_ts IS NOT NULL
AND click_id IS NOT NULL
GROUP BY click_id
HAVING toDate(min(event_ts)) <> toDate(max(event_ts))
ORDER BY visit_start;
Запрос должен найти хотя бы один визит, чьи события лежат по разные стороны полуночи. Подойдёт любой стык внутри мира: 1→2, 2→3 или 3→4.
Как слова связаны со схемой. В manifest написано «визит», а в SQL такой визит обозначен
click_id. Отдельной таблицы визитов нет: контекст лежит вdds.click, а события визита — вdds.event.
Почему запрос идёт прямо в dds.event? Представление
dm.v_session_overview группирует строки по event_date и click_id.
Переходящий визит там уже разрезан на две строки: одна до полуночи, другая
после. Чтобы увидеть визит целиком, сначала собираем все его события по
click_id, а потом сравниваем даты самого раннего и самого позднего события.
Смотрим день-к-дню
Открой в Superset дашборд Clickstream Analytics. Поставь фильтр
Date Range → No filter и найди график Events over Time. После обновления
на нём должен появиться день 4. Точную высоту точки не угадывай: она должна
сойтись с данными ClickHouse и manifest.
3. Загляни внутрь
Открой
airflow/dags/world_next_day_dag.py.
Это короткая оркестрация вокруг трёх действий: проверить точку продолжения,
дописать сутки и пересчитать аналитику.
Почему сначала идут проверки
check_etl_not_paused убеждается, что Airflow сможет запустить
etl_pipeline. precheck_next_day проверяет, что:
- живой генератор сейчас не работает;
- состояние генератора согласовано с правой границей manifest;
- накопительные счётчики не повреждены.
Только после этого run_next_day добавляет новую порцию. Так два режима,
next-day и continue, не пишут в один мир одновременно.
Почему ETL запускается с full_refresh
TriggerDagRunOperator передаёт в etl_pipeline:
conf={"full_refresh": True}
Значит, DDS и DM пересобираются из всей накопленной ODS, а не только из нового дня. Это простой путь для учебного стенда, но его цена растёт вместе с миром.
Открой в Grafana дашборд Airflow Overview и панель Task Duration (avg).
Сравни длительности задач двух запусков world_next_day: ручного дня 4 и
запланированного дня 5 из следующей секции.
Генерация одной суточной порции должна занимать примерно одинаковое время: счётчики manifest обновляются только новыми данными. Это исправлено в issue #5. Полный ETL перечитывает растущий мир, поэтому со временем дорожает. Это осознанный долг из issue #8, а не поломка этой лабы.
Что делает расписание
У DAG задано расписание */30 * * * *: запуск каждые 30 минут. При создании
DAG стоит на паузе, поэтому мир сам не растёт. catchup=False запрещает
Airflow проигрывать все пропущенные интервалы с 2024 года. После снятия паузы
будет ждать только ближайший новый интервал.
4. Управляемая правка: включаем один запуск по расписанию
Сейчас день 4 был добавлен вручную. На короткое время разрешим Airflow добавить день 5 автоматически.
Шаг 1. Зафиксируй правую границу
Ещё раз выполни команду kafka-manifest-summary из секции 2 и запиши
model_t_end. После запланированного запуска она должна сдвинуться ровно на
24 модельных часа.
Шаг 2. Сними паузу
В списке DAG-ов Airflow включи переключатель world_next_day. Не нажимай
Trigger DAG: на этот раз нужен именно запуск по расписанию.
Ближайший запуск появится на следующей получасовой границе. Следи за списком запусков и графом DAG.
Шаг 3. Останови расписание после одного успеха
Как только один запланированный запуск станет зелёным, снова поставь
world_next_day на паузу. Сделай это до следующей получасовой границы, иначе
приедет ещё один день.
Проверь три результата:
kafka-manifest-summaryпоказывает новую правую границу и новые накопительные числа;make generated-history-chain-checkзаканчивается без ошибки;- в
Events over Timeпоявился день 5.
Точные числа после дня 5 не фиксируем: это результат учебной правки.
Верни как было
Сначала убедись, что world_next_day снова стоит на паузе. Но одной паузы
недостаточно: мир уже вырос до пяти дней. Верни чистый трёхдневный эталон по
канонической инструкции курса.
5. Проверь себя
| Действие | Где смотреть | Что ожидать |
|---|---|---|
запустить world_next_day с пустой формой |
Airflow graph | все пять задач зелёные, etl_pipeline вызван с full_refresh=true |
| прочитать manifest после дня 4 | kafka-manifest-summary |
числа совпадают с таблицей секции 2 |
| проверить накопленную цепочку | make generated-history-chain-check |
команда завершается без ошибки |
| найти переходящие визиты | запрос в dds.event |
хотя бы один click_id пересекает полночь |
| снять паузу с DAG | Airflow runs | на ближайшей получасовой границе приезжает один запланированный день |
| вернуть паузу и пройти сброс | Airflow и Superset | расписание выключено, снова видны три эталонных дня |
Ответь своими словами:
- почему
dm.v_session_overviewне подходит для поиска визита через полночь; - зачем проверять стык двух суточных порций;
- почему
catchup=Falseне означает «у DAG нет расписания»; - что случится, если дважды вручную запустить
world_next_day; - почему промышленная задача «день D» должна повторно обрабатывать тот же день, а учебный DAG при повторе добавляет день D+1.
Последние два вопроса — проверка идемпотентности. Идемпотентная операция при
повторе с тем же входом оставляет тот же результат. world_next_day устроен
иначе: его входом служит текущая правая граница мира, и после каждого успеха
эта граница меняется.
6. Что должно получиться
После лабы сохрани:
- скрин зелёного ручного запуска
world_next_day; - вывод
make generated-history-chain-checkбез ошибки; - результат SQL-запроса с переходящим через полночь
click_id; - скрин дневного графика с добавленным днём;
- короткое объяснение своими словами: чем пакетный инкремент отличается от полного пересчёта и зачем отдельно проверять границу порций.
Перед переходом дальше стенд должен быть возвращён к трёхдневному эталонному
миру, а world_next_day — стоять на паузе.
Мост к следующему шагу
Здесь новый день приехал как законченная и повторяемая порция. В следующей лабе граница исчезнет: события пойдут непрерывно, а разные слои начнут отставать друг от друга.