fix(course): правки по итогам слепого ревью лаб 07/08 (#22)
Зачем: два слепых ревью (Codex CODE-1..9, Claude TASK-1..2) и перепроверки нашли дефекты в лабах и метадоках; финал лабы 08 переделан по решению пользователя. Что: - канонический сброс в README курса теперь снимает DAG'и с паузы (все создаются на паузе); - имя дашборда исправлено на «E-commerce Analytics Dashboard»; - финал лабы 08: вместо мифа «users == sessions на статике» — вернувшийся пользователь через границу заморозки и измеренное обещание свежести; §2 велит записать границы и время (§6 их требует); - CONTEXT.md: эталонный мир — замороженный живой, 4 056 пользователей / 26 083 визита, возвраты уже есть; - уроки 5–6 ведут в обязательный маршрут, живое упражнение урока 5 требует канонический сброс; - контрактные тесты: убран якорь на удалённую команду; добавленные на триаже тесты-цитаты срезаны до устойчивых инвариантов (прозаический легаси-жанр — issue #24); - handoff обновлён до текущего состояния. Проверка: make test (219 generator + 31 contract) и make lint зелёные; SQL вернувшегося пользователя проверен на эфемерном ClickHouse 25.1 (на полном наборе — в #23). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -274,6 +274,10 @@ make generator-continue
|
||||
make generator-down
|
||||
```
|
||||
|
||||
Остановка генератора не удаляет уже приехавшие данные и сохранённое состояние.
|
||||
После этого необязательного опыта верни эталонный мир по
|
||||
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
|
||||
|
||||
---
|
||||
|
||||
## 4. Управляемая правка: остановим scheduler и увидим алерт
|
||||
@@ -381,7 +385,6 @@ make recover-monitoring
|
||||
|
||||
## Мост к следующему шагу
|
||||
|
||||
Теперь стенд закрывает полный учебный маршрут: Kafka принимает поток, ClickHouse раскладывает
|
||||
слои, Airflow управляет порядком, а Prometheus и Grafana показывают состояние системы. Дальше
|
||||
этот же стенд можно использовать не как разовый набор уроков, а как тренажёр: менять данные,
|
||||
ломать отдельные места, смотреть, где появляется сигнал, и объяснять по метрикам, что произошло.
|
||||
Теперь ты видишь состояние пайплайна со стороны: где идут данные, где растёт
|
||||
отставание и где сработал алерт. В уроке 6 у готовых витрин появится
|
||||
потребитель — дашборд в Superset.
|
||||
|
||||
@@ -425,9 +425,8 @@ metadata Superset.
|
||||
|
||||
---
|
||||
|
||||
## Мост после курса
|
||||
## Мост к следующему шагу
|
||||
|
||||
Теперь у тебя есть сквозная цепочка: Kafka → ClickHouse STG → ODS → DDS → DM → мониторинг →
|
||||
Superset. Следующий честный вопрос уже не про этот стенд, а про продакшен: какие витрины стоит
|
||||
материализовать, какие права дать BI-пользователям и как не превратить dashboard в единственный
|
||||
источник правды вместо версионированного SQL в репозитории.
|
||||
Теперь у тебя есть сквозная цепочка от Kafka до дашборда в Superset. В лабе 07
|
||||
ты добавишь в этот мир следующий модельный день и проверишь границу двух
|
||||
суточных порций.
|
||||
|
||||
@@ -142,10 +142,11 @@ ORDER BY visit_start;
|
||||
|
||||
### Смотрим день-к-дню
|
||||
|
||||
Открой в Superset дашборд `Clickstream Analytics`. Поставь фильтр
|
||||
Открой в Superset дашборд `E-commerce Analytics Dashboard`. Поставь фильтр
|
||||
**Date Range → No filter** и найди график `Events over Time`. После обновления
|
||||
на нём должен появиться день 4. Точную высоту точки не угадывай: она должна
|
||||
сойтись с данными ClickHouse и manifest.
|
||||
на нём должны появиться непустые пятиминутные интервалы дня 4 после дня 3.
|
||||
Высоту отдельной точки не сравнивай с накопительными числами manifest:
|
||||
manifest уже сверили отдельно в начале секции.
|
||||
|
||||
---
|
||||
|
||||
@@ -274,10 +275,12 @@ Airflow проигрывать все пропущенные интервалы
|
||||
|
||||
После лабы сохрани:
|
||||
|
||||
- скрин зелёного ручного запуска `world_next_day`;
|
||||
- скрин зелёного ручного запуска `world_next_day`, который добавил день 4;
|
||||
- скрин зелёного запланированного запуска `world_next_day`, который добавил
|
||||
день 5;
|
||||
- вывод `make generated-history-chain-check` без ошибки;
|
||||
- результат SQL-запроса с переходящим через полночь `click_id`;
|
||||
- скрин дневного графика с добавленным днём;
|
||||
- скрин дневного графика с днём 5 после дня 4;
|
||||
- короткое объяснение своими словами: чем пакетный инкремент отличается от
|
||||
полного пересчёта и зачем отдельно проверять границу порций.
|
||||
|
||||
|
||||
@@ -104,17 +104,18 @@ make generator-continue
|
||||
|
||||
Подожди несколько минут и снова выполни запрос по слоям. Правая граница STG
|
||||
уйдёт вперёд. ODS, DDS и DM останутся на границе последнего
|
||||
`etl_pipeline`.
|
||||
`etl_pipeline`. Запиши время по обычным часам и границы STG и DM. Границу STG
|
||||
считай контрольной.
|
||||
|
||||
### Догоняем пакетные слои
|
||||
|
||||
Открой Airflow и запусти `etl_pipeline` через **Trigger DAG** с пустой формой.
|
||||
По умолчанию это полный пересчёт.
|
||||
По умолчанию это полный пересчёт. При запуске включи секундомер.
|
||||
|
||||
После зелёного прогона повтори запрос. ODS, DDS и DM догонят данные, которые
|
||||
успели попасть в STG к началу обработки. Генератор всё ещё работает, поэтому
|
||||
STG вскоре снова может оказаться чуть свежее. Это ожидаемое расслоение, а не
|
||||
потеря строк.
|
||||
После зелёного прогона останови секундомер и повтори запрос. Запиши новые
|
||||
границы STG и DM: DM должна достичь контрольной границы STG. Генератор всё ещё
|
||||
работает, поэтому STG вскоре снова может оказаться чуть свежее. Это ожидаемое
|
||||
расслоение, а не потеря строк.
|
||||
|
||||
---
|
||||
|
||||
@@ -216,24 +217,29 @@ make generator-continue
|
||||
Запусти `etl_pipeline` с пустой формой. После зелёного прогона выполни:
|
||||
|
||||
```sql
|
||||
WITH parseDateTime64BestEffort('<model_t_end>', 6, 'UTC') AS boundary
|
||||
SELECT
|
||||
uniqExact(user_domain_id) AS users,
|
||||
uniqExact(click_id) AS sessions
|
||||
FROM dm.v_events_enriched
|
||||
WHERE user_domain_id IS NOT NULL;
|
||||
user_domain_id,
|
||||
countIf(visit_start < boundary) AS visits_before,
|
||||
countIf(visit_start >= boundary) AS visits_after,
|
||||
min(visit_start) AS first_visit_ts, max(visit_start) AS last_visit_ts
|
||||
FROM (
|
||||
SELECT user_domain_id, click_id, min(event_ts) AS visit_start
|
||||
FROM dm.v_events_enriched
|
||||
WHERE user_domain_id IS NOT NULL
|
||||
GROUP BY user_domain_id, click_id
|
||||
)
|
||||
GROUP BY user_domain_id
|
||||
HAVING min(visit_start) < boundary AND max(visit_start) >= boundary
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
В живом мире один пользователь может вернуться и открыть новый визит. Поэтому
|
||||
после достаточного продолжения ожидаем `users < sessions`. На эталонной
|
||||
статике было `users == sessions`.
|
||||
|
||||
Точные числа здесь не печатаем: длительность живого запуска у каждого своя.
|
||||
`users = <значение>`, `sessions = <значение>`.
|
||||
<!-- сверить-на-стенде -->
|
||||
|
||||
Если равенство ещё сохранилось, дай генератору поработать несколько минут,
|
||||
снова запусти `etl_pipeline` и повтори запрос. Важно увидеть сам возврат
|
||||
пользователя, а не угадать конкретное число.
|
||||
Подставь вместо `<model_t_end>` границу из манифеста. Успех — хотя бы одна
|
||||
строка. Иначе подожди несколько минут, перезапусти `etl_pipeline` и повтори
|
||||
запрос. Это симметрия лаб: в лабе 07 визит пересекал полночь, здесь
|
||||
пользователь с разными визитами пересекает границу замороженного мира.
|
||||
Мир расходится: итоги превышают эталонные и различаются у менти. Это правильно.
|
||||
|
||||
### Верни как было
|
||||
|
||||
@@ -256,22 +262,18 @@ make generator-down
|
||||
| Действие | Где смотреть | Что ожидать |
|
||||
|----------|--------------|-------------|
|
||||
| выполнить `make generator-continue` | Prometheus Targets | `generator` переходит в `UP` |
|
||||
| посмотреть живой генератор | `Generator Overview` | `Total Events/min (all 4 topics)` становится ненулевым |
|
||||
| сравнить свежесть до ETL | SQL-запрос по слоям | STG свежее ODS, DDS и DM <!-- сверить-на-стенде --> |
|
||||
| запустить `etl_pipeline` | Airflow и SQL-запрос | пакетные слои догоняют снимок STG |
|
||||
| измерить свежесть до и после ETL | запрос из секции 2 и часы | записаны модельное отставание и настенное ожидание <!-- сверить-на-стенде --> |
|
||||
| выполнить `make generator-down` | Grafana и Kafka | события перестают поступать, отставание читателей стекает к нулю |
|
||||
| снова выполнить `make generator-continue` | Grafana, Kafka и STG | поток продолжается, offset-ы и правая граница снова растут |
|
||||
| пересчитать аналитику | запрос `users` и `sessions` | после возвратов пользователей выполняется `users < sessions` <!-- сверить-на-стенде --> |
|
||||
| остановить поток и пройти сброс | Grafana и SQL | `generator` выключен, слои снова совпадают с эталонным миром |
|
||||
| найти вернувшегося пользователя | запрос по границе `model_t_end` | есть пользователь с визитами до и после границы |
|
||||
|
||||
Ответь своими словами:
|
||||
|
||||
- что означает свежесть данных и кто задаёт требование к ней;
|
||||
- почему новые строки уже есть в STG, но их ещё нет в DDS;
|
||||
- почему запуск `etl_pipeline` догоняет поток лишь до очередного снимка;
|
||||
- что сохраняет режим `continue`;
|
||||
- почему после живого продолжения числа разных менти расходятся;
|
||||
- откуда берётся `users < sessions`.
|
||||
- почему STG опережает DM, а `etl_pipeline` догоняет лишь очередной снимок;
|
||||
- что сохраняет `continue` и почему итоги менти расходятся;
|
||||
- какую свежесть можно честно обещать при ручном запуске и при расписании
|
||||
каждые 30 минут;
|
||||
- почему минутная свежесть лежит за пределами этого стенда.
|
||||
|
||||
---
|
||||
|
||||
@@ -279,13 +281,14 @@ make generator-down
|
||||
|
||||
После лабы сохрани:
|
||||
|
||||
- скрин Prometheus Targets с `generator` в `UP`;
|
||||
- скрин живого `Total Events/min (all 4 topics)`;
|
||||
- два результата запроса свежести: до и после `etl_pipeline`;
|
||||
- наблюдение остановки и продолжения по offset-ам или правой границе STG;
|
||||
- результат запроса с `users < sessions`;
|
||||
- короткое объяснение своими словами: почему потоковый приём не гарантирует
|
||||
потоковую обработку до витрины.
|
||||
- скрин `generator` в `DOWN` после `make generator-down` и скрин `generator`
|
||||
в `UP` после повторного `make generator-continue`;
|
||||
- границы STG до остановки и после продолжения;
|
||||
- строку пользователя с визитами до и после `model_t_end`;
|
||||
- замеры до и после `etl_pipeline`: разницу `STG − DM` в модельном времени и
|
||||
настенное ожидание до появления зафиксированной границы STG в DM;
|
||||
- вывод своими словами: что можно обещать при ручном запуске, что — при
|
||||
расписании каждые 30 минут и почему этот стенд не обещает минутную свежесть.
|
||||
|
||||
В конце `generator` должен быть остановлен, а стенд — возвращён к эталонному
|
||||
миру.
|
||||
@@ -294,6 +297,6 @@ make generator-down
|
||||
|
||||
## Мост после курса
|
||||
|
||||
Теперь у тебя есть два способа растить один мир: повторяемая суточная порция и
|
||||
живое продолжение. Следующий практический вопрос уже зависит от продукта:
|
||||
какую свежесть обещать потребителю и какую цену платить за пересчёт.
|
||||
Замер отвечает на вопрос из секции 1: ручной запуск не ограничивает ожидание,
|
||||
а расписание раз в 30 минут дало бы около 30 минут плюс время прогона. Для
|
||||
минутной свежести нужна другая обработка.
|
||||
|
||||
Reference in New Issue
Block a user