- Зачем: - корневые цели смешивали два уровня: пять из шестнадцати начинались с cd generator. - цель, названная общерепозиторной, охватывала 31 файл Python из 34: даги и Superset не видел ни линт, ни типы. - Что: - lint, typecheck, test, docs и inventory переехали в новый generator/Makefile. - корневой lint заведён по коду стенда — dags и infra/superset — с явными путями и закреплённой версией ruff. - заведён корневой ruff.toml: тот же список правил, target-version по младшему Python в образах стенда. - цели корня сгруппированы по использованию, осталось двенадцать. - два файла дагов переформатированы под новую проверку. - карта проверок, оба README, спека генератора и AGENTS.md приведены к двум дверям. - Проверка: - make lint; make config-test — зелёные. - make -C generator lint; typecheck; test — зелёные, 407 тестов. - ruff check --show-files: из корня ровно три файла стенда, из generator/ — только его. - цена корневого lint замерена (0,4 с) и вписана в карту проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
332 lines
26 KiB
Markdown
332 lines
26 KiB
Markdown
# Учебная дата-платформа кликстрима
|
||
|
||
Стенд для работы с кликстримом. Преемник учебного стенда
|
||
[clickstream-ch-kafka-superset-demo](https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo).
|
||
|
||
## Статус
|
||
|
||
Репозиторий строится по спеке
|
||
[«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md).
|
||
Сейчас работают кластер ClickHouse из двух шардов, отдельный
|
||
clickhouse-keeper, односерверная Kafka в режиме KRaft, Airflow 3.3, Superset,
|
||
Prometheus, Grafana и общая база Postgres для метаданных. Генератор
|
||
кликстрима в [`generator/`](generator/) собран целиком со стороны клиента: у
|
||
него есть контракт схемы события, из которого собрано
|
||
[описание выгрузки](docs/formats/clickstream-event.md), модельный мир и
|
||
проигрыватель, отправляющий дни в Kafka или в файл. События доезжают до
|
||
типизированного `ods.event`, а `make up` наполняет стенд стартовым миром —
|
||
первой неделей модельного времени.
|
||
|
||
## Быстрый старт
|
||
|
||
Нужны Docker с Compose и около 8 ГБ памяти, доступной Docker. Это не объём
|
||
ноутбука, а то, что отдано самому Docker: в Docker Desktop и WSL2 он живёт
|
||
внутри виртуальной машины и получает лишь часть памяти хозяина. Сколько выдано
|
||
сейчас, в байтах, покажет `docker info --format '{{.MemTotal}}'`. В WSL2 это
|
||
поднимается параметром `memory` в файле `.wslconfig` домашнего каталога
|
||
пользователя Windows; после правки нужен `wsl --shutdown`. Если своей машины не
|
||
хватает, стенд одинаково хорошо живёт на недорогом VPS.
|
||
|
||
Проверкам на поднятом стенде также нужны `curl`, `jq`, `awk`,
|
||
`grep`, `sed`, `tail`, `sleep` и `timeout`. `jq` нужен и самому `make up`: им
|
||
читается опись мира, по которой он ждёт заливки. По умолчанию должны быть свободны
|
||
порты `23000`, `28080`, `28088`, `28123`, `28124`, `29000`, `29001`, `29090`
|
||
и `29092`. Проверкам без стенда — `make config-test` и `make lint` в корне,
|
||
всем целям генератора в `generator/` — нужен `uv`.
|
||
|
||
Сначала скопируйте настройки стенда:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
make up
|
||
make smoke
|
||
make check-clickhouse
|
||
make check-services
|
||
```
|
||
|
||
Первому знакомству нужны все три проверки на стенде: `make smoke` говорит, что
|
||
стенд собран, `make check-clickhouse` — что кластер работает кластером, а
|
||
`make check-services` — что Airflow запускает DAG, а Superset ходит в базу.
|
||
Дальше, в рабочей петле, обычно хватает `make smoke`.
|
||
|
||
`.env.example` — образец обязательной локальной настройки. В нём живут имя
|
||
экземпляра, внешние порты, учебные учётные данные и ключи. `compose.yaml` не
|
||
дублирует их умолчаниями: если значения нет в `.env`, Compose сразу предложит
|
||
скопировать образец. Внутренние адреса, имена топиков и версии образов описывают
|
||
сам стенд, поэтому записаны литералами в `compose.yaml` и Dockerfile.
|
||
|
||
Учётные данные Postgres и Grafana применяются при создании их томов.
|
||
После первого запуска меняйте их только вместе с `make clean`: команда удалит
|
||
все локальные данные стенда, а следующий `make up` создаст их с новыми
|
||
значениями.
|
||
|
||
`make up` собирает локальные образы Airflow и Superset, поднимает весь стенд и
|
||
ждёт здорового состояния долгоживущих контейнеров. В образ Airflow добавлены
|
||
закреплённые клиенты ClickHouse и Kafka. Одноразовые `airflow-init` и
|
||
`superset-init` завершаются с кодом 0. Первый обновляет схему Airflow,
|
||
подготавливает администратора и подключение к `clickhouse-01`. Второй обновляет
|
||
Superset, создаёт администратора и импортирует подключение к `clickhouse-02`.
|
||
С нуля подъём занимает около трёх минут, на живом стенде — около минуты.
|
||
|
||
`make smoke` за секунды спрашивает, собран ли стенд: зависимости машины,
|
||
здоровье контейнеров, устройство keeper, ответ Kafka с машины через отображённый
|
||
порт, три цели Prometheus, источник Grafana, компоненты Airflow и подготовленное
|
||
подключение к `clickhouse-01`. В конце проверка спрашивает у Docker, не убивало
|
||
ли ядро что-нибудь в долгоживущих контейнерах за нехватку памяти и не включалась
|
||
ли политика перезапуска: убитый контейнер Docker поднимает сам, и проверка
|
||
состояния об этом промолчит.
|
||
|
||
Одиннадцать проверок здоровья сразу после `make up --wait` повторяют то, чего
|
||
Compose уже дождался: у каждой долгоживущей службы есть своя `healthcheck`.
|
||
Оставлены они потому, что первый вопрос к стенду всё равно «всё ли живо», а
|
||
ответ на него стоит меньше секунды. Устройство keeper — другое дело: он должен
|
||
работать от пользователя `clickhouse`, с пределом в 262144 открытых файла и со
|
||
своим томом под данные. Всё это объявлено в `compose.yaml`, но здоровым keeper
|
||
выглядит и без этого, поэтому смоук спрашивает у живого контейнера, дошли ли
|
||
объявленные настройки до процесса.
|
||
|
||
Kafka по той же причине спрашивают снаружи. Её `healthcheck` обращается к
|
||
брокеру изнутри контейнера и по внутреннему слушателю, поэтому здоровой Kafka
|
||
остаётся и тогда, когда объявленный наружу адрес ведёт не туда. Клиент с машины
|
||
в этом случае подключается, получает метаданные и молча виснет на адресе,
|
||
которого с его стороны не существует. Смоук идёт тем же путём, что и будущий
|
||
генератор: стучится в отображённый порт и просит список топиков.
|
||
|
||
`make check-services` проверяет то, ради чего приходится ждать службу: работу с
|
||
топиком Kafka с машины — создан, найден, удалён, — ручной запуск пробников
|
||
`test_clickhouse` и `test_kafka`, вход в Superset, его метаданные и подключение
|
||
к `clickhouse-02`. Первый пробник создаёт таблицы на обеих нодах и читает через
|
||
`Distributed` на ноде 2 строку из локальной таблицы ноды 1. Второй пишет в Kafka
|
||
и читает свой маркер. Временный топик проверки с машины и запуски DAG удаляются;
|
||
постоянный топик пробника сохраняется, а старые записи чистит Kafka.
|
||
|
||
`make check-clickhouse` запускает отдельную глубокую проверку ClickHouse:
|
||
описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`,
|
||
`Distributed`, очередь распределённых DDL и очистку временных таблиц. Последняя
|
||
из девяти проверок — единственная на настоящих данных: она подневно сверяет
|
||
события стартового мира с описью и при расхождении говорит, где искать —
|
||
в событиях или в браке.
|
||
|
||
`make config-test` проверяет Compose, синтаксис файлов DAG и пробельные ошибки
|
||
в diff без запуска стенда.
|
||
|
||
Обычный рабочий цикл — `make config-test` и `make smoke`; остальные цели гоняют
|
||
тогда, когда правка их касается. Что утверждает каждая проверка, нужен ли ей
|
||
поднятый стенд, сколько стоит прогон и куда класть новую — в
|
||
[карте проверок](docs/architecture/testing.md).
|
||
|
||
Остановить контейнеры без удаления данных можно командой `make down`. Для
|
||
полного сброса с удалением всех именованных томов используйте `make clean`.
|
||
Повторный `make up` безопасен: одноразовая подготовка приложений идемпотентна.
|
||
|
||
## Стартовый мир
|
||
|
||
Стенд поднимается не пустым: разовая служба `world-init` играет в топик `hits`
|
||
первые восемь дней модельного времени — понедельник по понедельник, 401 185
|
||
событий. Дальше их обычным путём разбирает хранилище, и к концу `make up` они
|
||
лежат в `ods.event`. Так у всякой лабы есть данные, и всегда одни и те же.
|
||
|
||
Ждать приходится дольше, чем работает заливка: приём асинхронный, поэтому
|
||
вторым шагом `make up` зовёт `scripts/wait-for-world.sh` — тот опрашивает
|
||
ClickHouse, пока мир не доедет. Повторный `make up` заливает мир заново; это
|
||
не ошибка, а свойство: номера событий те же, и повтор схлопнет
|
||
`ReplacingMergeTree`.
|
||
|
||
Сам мир в git не хранится — он чистая функция зерна, и держать его в
|
||
репозитории значило бы держать там кэш. Вместо него лежит **опись мира**,
|
||
[`data/world-inventory.json`](data/world-inventory.json): зерно, версия
|
||
генератора, хеш каталога товаров и по строке на каждый день — дата, число
|
||
событий и хеш его байтов. Опись отвечает на единственный вопрос: тот ли это
|
||
мир, что был вчера.
|
||
|
||
Спрашивают её двое. `make test` в `generator/` сверяет опись с тем, что
|
||
собирается из кода сегодня: правка генератора меняет мир, и опись надо
|
||
пересобрать — `make inventory` там же. `make check-clickhouse` в корне
|
||
сверяет с описью то, что доехало до
|
||
`ods.event`, подневно. Хеш дня можно пересчитать и руками — это обычный
|
||
`sha256sum` файла, который пишет файловый приёмник:
|
||
|
||
```bash
|
||
uv run --project generator python -m clickstream_generator batch \
|
||
--day 0 --file tmp/day0.jsonl
|
||
sha256sum tmp/day0.jsonl
|
||
```
|
||
|
||
## Как позвать генератор
|
||
|
||
События производит генератор из [`generator/`](generator/). Модельный день —
|
||
функция зерна и номера дня, поэтому один и тот же день всегда даёт те же
|
||
события: обрыв лечится повтором. Позицию на оси генератор не помнит — какой
|
||
день играть, решает зовущий.
|
||
|
||
Режима два, и различаются они темпом. **Пакетный** гонит день подряд, без пауз:
|
||
так заливается мир и так переигрывается день после обрыва. **Живой** держит темп
|
||
модельного времени — по умолчанию ×60: модельные сутки за 24 реальные минуты,
|
||
суточная волна разворачивается на глазах, отставание видно в логе.
|
||
|
||
На стенде генератор ходит своим образом — разовой службой, которую поднимают
|
||
и убирают на один прогон. На каждый режим по цели:
|
||
|
||
```bash
|
||
# день D0 целиком в топик hits
|
||
make generate-batch
|
||
# первая сотня событий дня D3
|
||
make generate-batch GENERATOR_DAY=3 GENERATOR_LIMIT=100
|
||
# день D3 живьём, ускорение ×1000
|
||
make generate-live GENERATOR_DAY=3 GENERATOR_SPEED=1000
|
||
```
|
||
|
||
| Переменная | Что задаёт | Умолчание |
|
||
| --- | --- | --- |
|
||
| `GENERATOR_DAY` | номер дня на оси мира (D0 — первый) | `0`, задано в `Makefile` |
|
||
| `GENERATOR_LIMIT` | потолок событий на прогон; только `generate-batch` | нет: день целиком |
|
||
| `GENERATOR_SPEED` | ускорение модельного времени; только `generate-live` | ×60, задано в генераторе |
|
||
|
||
Куда отправлять, обеим целям задаёт сам стенд: внутри сети Compose это брокер
|
||
`kafka:9092` и топик `hits`.
|
||
|
||
Образ цели не пересобирают: нет образа — Compose соберёт его сам, есть —
|
||
возьмёт как есть. Пересобрать намеренно, после правки `generator/Dockerfile`
|
||
или зависимостей, — отдельной командой:
|
||
|
||
```bash
|
||
docker compose --profile generator build generator
|
||
```
|
||
|
||
Тот же образ несёт заливка стартового мира, поэтому свежим его держит и
|
||
обычный `make up`: он собирает образы всего стенда, и генератор теперь среди
|
||
них.
|
||
|
||
Разведено это нарочно: собрать образ и запустить контейнер — разные действия, и
|
||
цель запуска, молча пересобирающая образ, стирает между ними границу. Что образ
|
||
устарел, видно по собственному прогону — это обратная связь, а не ловушка.
|
||
|
||
Посмотреть на события, не поднимая стенд, помогает файловый приёмник: одно
|
||
событие — одна строка. Флаг `--limit` берёт начало дня вместо целого дня —
|
||
тому, кто смотрит на конвейер, ждать полсотни тысяч событий незачем.
|
||
|
||
```bash
|
||
uv run --project generator python -m clickstream_generator batch \
|
||
--day 0 --limit 100 --file tmp/day0.jsonl
|
||
```
|
||
|
||
Несколько дней подряд играет один запуск: `--days 8`. Всё, что принимается
|
||
флагами, принимается и переменными окружения — так генератор позовёт даг
|
||
этапа 5; полный список у `--help`.
|
||
|
||
**Умолчания есть не у всего, и это нарочно.** Зерно, число дней и темп живого
|
||
дня описывают модель мира — их генератор знает сам. День на оси, приёмник и имя
|
||
топика описывают стенд, на котором его запустили: не назвали — отказ и ненулевой
|
||
код возврата, ещё до первого события. Поэтому умолчание дня и живёт в
|
||
`Makefile`: день называет тот, кто запускает, а не тот, кого запускают.
|
||
|
||
## Состав и доступ
|
||
|
||
- `clickhouse-01` — инициатор DDL и точка подключения Airflow;
|
||
- `clickhouse-02` — точка подключения Superset;
|
||
- `clickhouse-keeper` — координатор кластера;
|
||
- `kafka` — один брокер KRaft;
|
||
- `postgres-metadata` — один Postgres с отдельными базами и пользователями
|
||
`airflow` и `superset`;
|
||
- `airflow-apiserver`, `airflow-scheduler` и `airflow-dag-processor` —
|
||
Airflow 3.3 с LocalExecutor, без triggerer;
|
||
- `superset` — интерфейс и подготовленное подключение ClickHouse;
|
||
- `prometheus` и `grafana` — сбор и просмотр встроенных метрик ClickHouse.
|
||
|
||
С настройками из `.env.example` порты доступны только с локальной машины:
|
||
|
||
- нода 1 — `http://127.0.0.1:28123`, нативный порт `29000`;
|
||
- нода 2 — `http://127.0.0.1:28124`, нативный порт `29001`;
|
||
- Kafka — `127.0.0.1:29092`;
|
||
- Airflow — `http://127.0.0.1:28080`, пользователь `admin`, пароль `airflow`;
|
||
- Superset — `http://127.0.0.1:28088`, пользователь `admin`, пароль `superset`;
|
||
- Prometheus — `http://127.0.0.1:29090`;
|
||
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
|
||
|
||
У локального учебного кластера нет пароля: ноды используют общего пользователя
|
||
`default` для запросов `Distributed`. Порты поэтому привязаны к `127.0.0.1` и
|
||
не открыты во внешнюю сеть.
|
||
|
||
Пароли интерфейсов, пароли Postgres, ключи Airflow и Superset, отсутствие
|
||
пароля ClickHouse и отсутствие проверки доступа у Kafka и Prometheus —
|
||
намеренно простые и явно ненастоящие настройки локального учебного стенда. Это
|
||
не пример настройки защиты: не копируйте значения из `.env.example` в рабочую
|
||
среду. Все опубликованные порты привязаны только к `127.0.0.1`; Postgres наружу
|
||
не опубликован.
|
||
|
||
В бою перед репликами ClickHouse обычно был бы балансировщик. Здесь в каждом
|
||
шарде одна реплика, поэтому балансировать нечего. Балансировщик и топология
|
||
2×2 намеренно не входят в стенд.
|
||
|
||
Конфигурация сверена 30 июля 2026 года с официальной документацией ClickHouse:
|
||
[настройками сервера](https://clickhouse.com/docs/operations/server-configuration-parameters/settings),
|
||
[Keeper](https://clickhouse.com/docs/guides/oss/deployment-and-scaling/keeper/),
|
||
[ReplicatedMergeTree](https://clickhouse.com/docs/engines/table-engines/mergetree-family/replication),
|
||
[ON CLUSTER](https://clickhouse.com/docs/sql-reference/distributed-ddl) и
|
||
[Distributed](https://clickhouse.com/docs/engines/table-engines/special/distributed).
|
||
Описание кластера задаётся через `remote_servers`, макросы — через `macros`,
|
||
подключение к keeper — через `zookeeper`; путь `ReplicatedMergeTree` содержит
|
||
`{shard}` и `{replica}`, а `Distributed` получает имя кластера, базу, локальную
|
||
таблицу и ключ шардирования. Макросы выбраны, чтобы один DDL через `ON CLUSTER`
|
||
создавал отдельный путь каждого шарда без вписанных вручную значений. Для
|
||
образа зафиксирован точный текущий
|
||
[LTS-выпуск 26.3.17.56](https://github.com/ClickHouse/ClickHouse/releases/tag/v26.3.17.56-lts);
|
||
серверы и keeper используют один образ. Настройки Kafka 4.3.1 сверены с
|
||
[примером односерверного KRaft](https://github.com/apache/kafka/blob/4.3.1/docker/examples/docker-compose-files/single-node/plaintext/docker-compose.yml).
|
||
Секция метрик взята из конфигурации закреплённого образа ClickHouse и проверена
|
||
на серверах и keeper. Подготовка источника Grafana сверена с
|
||
[официальным описанием автоматической настройки](https://grafana.com/docs/grafana/latest/administration/provisioning/).
|
||
Prometheus собирает только встроенные метрики двух серверов и keeper; внешних
|
||
сборщиков, панелей и правил оповещения пока нет.
|
||
После изменения `infra/clickhouse/config.d/prometheus.xml` выполните
|
||
`docker compose restart clickhouse-01 clickhouse-02`: обычный `make up` не
|
||
перезапускает уже созданные серверы и они продолжают работать со старой
|
||
конфигурацией.
|
||
|
||
Airflow закреплён на 3.3.0. Состав обязательных процессов, LocalExecutor,
|
||
публичный `airflow.sdk`, API здоровья и SimpleAuthManager сверены с
|
||
[архитектурой Airflow 3.3](https://airflow.apache.org/docs/apache-airflow/3.3.0/core-concepts/overview.html),
|
||
[публичным интерфейсом](https://airflow.apache.org/docs/apache-airflow/3.3.0/public-airflow-interface.html)
|
||
и [описанием здоровья](https://airflow.apache.org/docs/apache-airflow/3.3.0/administration-and-deployment/logging-monitoring/check-health.html).
|
||
Для пробников проверены публичный `Connection.get` из `airflow.sdk` и клиент
|
||
`clickhouse-connect`. Официальный провайдер Kafka сам использует
|
||
`confluent-kafka`; отдельное подключение и обёртки провайдера здесь не нужны,
|
||
поэтому прямой клиент оставляет образ и пример короче.
|
||
Superset закреплён на 6.1.0; драйвер `clickhouse-connect`, форма
|
||
`clickhousedb://` и драйвер Postgres сверены с
|
||
[документацией подключений Superset](https://superset.apache.org/user-docs/6.1.0/databases/)
|
||
и [настройкой базы метаданных](https://superset.apache.org/admin-docs/6.1.0/configuration/configuring-superset/).
|
||
|
||
## Что здесь будет
|
||
|
||
- одно широкое событие кликстрима по образцу выгрузки Яндекс Метрики вместо
|
||
четырёх топиков;
|
||
- второй источник — заказы бэкенда, ежедневным слепком в ту же Kafka;
|
||
- сверка клиентской покупки с заказом бэкенда: деньги считаем по бэкенду,
|
||
поведение и атрибуцию — по трекеру;
|
||
- анонимный кликстрим и склейка кука↔пользователь через покупки;
|
||
- ClickHouse кластером как единственным режимом.
|
||
|
||
## Чем отличается от предшественника
|
||
|
||
Предшественник остаётся стабильным учебным стендом и заморожен для новых фич:
|
||
там событие разрезано на четыре топика, есть только просмотры страниц,
|
||
посетители опознаны по email, ClickHouse — одна нода. Развитие идёт здесь.
|
||
|
||
## Документация
|
||
|
||
- [docs/specs/](docs/specs/) — спеки: источник истины о задуманном.
|
||
- [docs/adr/](docs/adr/) — принятые решения с доводами и отвергнутыми
|
||
вариантами: почему сделано так, а не иначе.
|
||
- [docs/architecture/](docs/architecture/) — рабочие справочники по зонам
|
||
ответственности: [хранилище](docs/architecture/storage.md) — конвенции имён,
|
||
раскладка по шардам, приём событий, карта таблиц;
|
||
[проверки](docs/architecture/testing.md) — что утверждает каждая цель `make`
|
||
и куда класть новую проверку.
|
||
- [docs/research/](docs/research/) — исследования; сейчас это формат
|
||
кликстрима Яндекса, по которому строится модель события.
|
||
- [docs/formats/](docs/formats/) — описания форматов источников: по ним
|
||
пишется сторона хранилища. Собираются из кода командой `make docs` в
|
||
`generator/`, руками не правятся.
|
||
- [AGENTS.md](AGENTS.md) — контракт работы в репозитории.
|