- Зачем:
- менти собирал мир генерацией (минуты и десятки минут CPU); теперь
готовый трёхдневный мир загружается импортом за ~2 минуты, и числа
у всех менти совпадают число-в-число (issue #3).
- Что:
- артефакт data/startup_history/reference-world.json.xz в git:
3 модельных дня daily-wave, ~850 МБ JSON → 32 МБ xz;
- чтение и запись артефакта понимают .xz потоково (lzma); пустое поле
artifact_path в пульте и make startup-history-import читают эталон;
- длительность профиля daily-wave стала 3d — в тон эталонному миру;
- предпроверка чистого стенда ставит зависимости генератора через uv;
экспорт и импорт разведены отдельными переменными Makefile;
- доки и runbook обновлены; новые контрактные тесты: xz round-trip
и дефолтные пути артефакта.
- Проверка:
- make test (210 + 31) и make lint зелёные; импорт на чистом стенде
за 2м03с, manifest совпал (280437 событий), Superset-проверка
зелёная; независимое ревью — APPROVED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
210 lines
12 KiB
Markdown
210 lines
12 KiB
Markdown
# Runbook: стартовая история стенда
|
||
|
||
Этот runbook нужен, чтобы один раз создать стартовую историю генератора, сохранить
|
||
её в файл и быстро восстановить на чистом стенде.
|
||
|
||
За устройством генератора см. [`generator/README.md`](../../generator/README.md)
|
||
и [спеку модельного времени](../specs/2026-06-14-generator-model-time-and-startup-history.md).
|
||
|
||
## Что дёшево
|
||
|
||
- Перезапуск без очистки volumes: Kafka хранит `generator_state` и manifest.
|
||
- Live-продолжение после импорта: генератор стартует с `T_end`, если настройки
|
||
совпадают.
|
||
- Восстановление чистого стенда из готового файла: события повторно пишутся в
|
||
Kafka, ClickHouse наполняется штатным путём.
|
||
|
||
## Что требует нового артефакта
|
||
|
||
- Другая длительность истории или другой профиль запуска.
|
||
- Другой `GEN_SEED`, `GEN_MODEL_T0`, часовой пояс, скорость или настройки
|
||
генерации.
|
||
- Осознанный новый мир после несовместимого state: сначала сбросьте state через
|
||
`GEN_STATE_RESET=true` или чистые volumes.
|
||
|
||
## Профили запуска
|
||
|
||
Основной способ — глагол плюс профиль:
|
||
|
||
| Профиль | Для чего | Длительность | Live-ход |
|
||
|---------|----------|--------------|----------|
|
||
| `ci` | Быстрая проверка и CI | `6h` | ×1, тик 60 с |
|
||
| `daily-wave` | История с видимой суточной волной | `3d` | ×60, тик 1 с |
|
||
|
||
Длительность можно переопределить через `GEN_HISTORY_DURATION`, например `2d`.
|
||
Команда сама считает `GEN_MODEL_T_END` от `GEN_MODEL_T0`.
|
||
|
||
## Эталонный мир
|
||
|
||
В репозитории хранится готовый мир за три модельных дня:
|
||
`data/startup_history/reference-world.json.xz`. Это обычный JSON-артефакт,
|
||
сжатый xz. Его добавляют прямо в Git, без Git LFS.
|
||
|
||
Сопровождающий проекта собирает файл одним запуском из корня репозитория:
|
||
|
||
```bash
|
||
ARTIFACT="$PWD/data/startup_history/reference-world.json.xz" \
|
||
make startup-history-export
|
||
```
|
||
|
||
Команда выполняет backfill с профилем `daily-wave` и сразу пишет xz-файл.
|
||
Несжатый JSON занимает около 850 МБ, сжатый файл — около 32 МБ. После проверки
|
||
файл можно добавить обычной командой:
|
||
|
||
```bash
|
||
git add data/startup_history/reference-world.json.xz
|
||
```
|
||
|
||
При импорте DAG дважды читает и распаковывает артефакт: во время предпроверки и
|
||
перед записью в Kafka. Это увеличивает время импорта, но не меняет результат.
|
||
|
||
Менти в форме `generator_control` выбирает `import` и оставляет
|
||
`artifact_path` пустым. Тогда читается эталонный мир из репозитория. Из консоли
|
||
тот же импорт запускается без указания пути:
|
||
|
||
```bash
|
||
make startup-history-import
|
||
```
|
||
|
||
## Пульт в Airflow
|
||
|
||
Основной ручной путь — DAG `generator_control` в Airflow UI:
|
||
|
||
1. Поднимите стенд: `make up`.
|
||
2. Если DDL ещё не применён, запустите `ddl_init`.
|
||
3. Снимите паузу с `etl_pipeline`, если он ещё paused:
|
||
`docker compose exec -T airflow-webserver airflow dags unpause etl_pipeline`.
|
||
4. Откройте `generator_control` и выберите `operation`.
|
||
|
||
Операции:
|
||
|
||
- `backfill` — создать стартовую историю. После записи в Kafka DAG сам запускает
|
||
`etl_pipeline`, ждёт завершения и выполняет `check`.
|
||
- `import` — прочитать артефакт из `artifact_path`. Несовместимый артефакт
|
||
отклоняется до записи в Kafka.
|
||
- `check` — сверить ClickHouse с manifest из Kafka.
|
||
|
||
Поля формы:
|
||
|
||
- `profile` берётся из профилей генератора.
|
||
- `duration` можно оставить пустым, тогда берётся длительность профиля.
|
||
- `seed` и `model_time_speed` — необязательные переопределения мира.
|
||
- `artifact_path`: для `backfill` — куда сохранить файл; пусто — не сохранять.
|
||
Для `import` — что читать; пусто — эталонный мир из репозитория:
|
||
`/opt/airflow/data/startup_history/reference-world.json.xz`.
|
||
|
||
Backfill и import работают только на чистом стенде. Если Kafka-топики данных или
|
||
STG уже непустые, DAG упадёт до записи и подскажет `make clean`. Консольные
|
||
команды `make generator-backfill` и `make startup-history-import` делают такую же
|
||
предпроверку с хоста. Это защита от смешивания разных миров.
|
||
|
||
Границы пульта:
|
||
|
||
- `make up`, `make clean` и live-продолжение остаются в консоли.
|
||
- Операции `continue` в DAG нет намеренно: live — долгоживущий сервис, а пульт
|
||
управляет разовыми пакетными операциями.
|
||
- Airflow не получает доступ к жизненному циклу контейнеров; таски выполняют обычный
|
||
Python-код генератора.
|
||
|
||
Если backfill сохраняет файл в `./data`, он создаётся пользователем Airflow
|
||
внутри контейнера. Чтение работает из Airflow и консольных команд, но перезапись
|
||
чужого файла может потребовать удалить старый файл вручную.
|
||
|
||
## Экспорт
|
||
|
||
По умолчанию создаётся трёхсуточный артефакт `daily-wave` с суточной волной:
|
||
|
||
```bash
|
||
make startup-history-export
|
||
```
|
||
|
||
Файл по умолчанию: `/tmp/clickstream-startup-history.json`. В live-продолжении
|
||
профиль `daily-wave` проживает модельные сутки примерно за 24 настенные минуты.
|
||
Для быстрой автоматической проверки явно задайте служебный профиль `ci`:
|
||
|
||
```bash
|
||
PROFILE=ci ARTIFACT=/tmp/clickstream-startup-history-ci.json \
|
||
make startup-history-export
|
||
```
|
||
|
||
Команда делает чистый backfill и пишет в файл один связный набор: события Kafka,
|
||
state и manifest.
|
||
|
||
## Импорт на чистый стенд
|
||
|
||
```bash
|
||
make clean
|
||
docker compose up -d clickhouse kafka
|
||
make ddl
|
||
|
||
make startup-history-import
|
||
|
||
sleep 10
|
||
make transform
|
||
ARTIFACT=data/startup_history/reference-world.json.xz make startup-history-check
|
||
CHECK_LIVE_SEAM=0 make generated-history-check
|
||
```
|
||
|
||
Это путь только импорта: live-строк после `T_end` ещё нет. Стык backfill/live
|
||
проверяйте через `make generated-history-runtime-check` или после
|
||
`make generator-continue` и повторного `make transform`.
|
||
|
||
Импорт не пишет напрямую в ClickHouse. Он воспроизводит события и служебные
|
||
compact-топики в Kafka. ClickHouse читает данные через свои Kafka-таблицы и
|
||
Materialized View, затем batch строит ODS, DDS и DM.
|
||
|
||
Импорт запускайте с тем же профилем, на котором создан артефакт. Для служебного
|
||
артефакта `ci` профиль нужно задать явно:
|
||
|
||
```bash
|
||
PROFILE=ci ARTIFACT=/tmp/clickstream-startup-history-ci.json \
|
||
make startup-history-import
|
||
```
|
||
|
||
`make startup-history-check` сверяет контрольные числа DM-витрины с manifest
|
||
артефакта: события, визиты, пользователей и диапазон `event_timestamp`. Если
|
||
data-топики Kafka уже непустые, импорт остановится до публикации событий.
|
||
|
||
В текущем стеке `kafka-python` не даёт транзакционный producer для нескольких
|
||
топиков. Поэтому импорт остаётся clean-stand операцией: при ошибке записи он
|
||
удаляет import-топики Kafka, чтобы повторный импорт не дописал дубли. Если
|
||
ClickHouse уже успел прочитать частичные сообщения, очистите стенд через
|
||
`make clean` и повторите импорт.
|
||
|
||
## Live-продолжение
|
||
|
||
После импорта запускайте live с тем же профилем, что был в артефакте. Для
|
||
обычного артефакта `daily-wave` достаточно команды:
|
||
|
||
```bash
|
||
make generator-continue
|
||
```
|
||
|
||
У `daily-wave` скорость ×60. Долгий простой стенда создаёт большую дыру в
|
||
модельном времени: ночь простоя может стать десятками модельных суток без
|
||
событий. Для чистой демонстрации лучше очистите стенд, затем запустите
|
||
`make generator-reset` или повторите импорт стартовой истории через
|
||
`make startup-history-import`.
|
||
`make generator-reset` начнёт новый live-мир только на чистом стенде; если в
|
||
Kafka data-топиках или STG уже есть строки, команда попросит `make clean`.
|
||
|
||
Если читаемый state есть, но настройки не совпадают, генератор падает с перечнем
|
||
полей. Это защита от смешения разных миров. Для намеренного нового мира
|
||
сначала очистите стенд через `make clean`, затем запускайте нужный сценарий.
|
||
|
||
После обновления кода старый state может оказаться в старом формате. При
|
||
`GEN_STATE_RESET=false` это теперь громкий отказ, а не тихий старт с нуля поверх
|
||
старой истории. Оператору нужно выбрать одно из двух: очистить стенд через
|
||
`make clean` и заново создать стартовую историю, либо осознанно начать новый
|
||
live-мир через `make generator-reset` на чистом стенде.
|
||
|
||
После нестандартного мира `make generator-continue` нужно запускать с теми же
|
||
настройками, что были у backfill/import. При расхождении генератор громко
|
||
покажет поля, которые не совпали.
|
||
Старые артефакты `daily-wave`, созданные до перехода на ×60 и тик 1 с, с новым
|
||
профилем несовместимы. Это ожидаемо: защита от смешения миров должна остановить
|
||
такой запуск.
|
||
|
||
Старые переменные `GEN_RUN_MODE`, `GEN_STATE_RESET` и `GEN_MODEL_T_END` остаются
|
||
низкоуровневым способом для отладки и прямого `docker compose run`.
|