- Зачем: - триаж issue #2: ошибка `p.dagRuns is undefined` — баг UI Airflow 2.10.x (в `useGridData.ts` нет проверки формы ответа), а спусковой крючок — потеря сеанса: `/object/grid_data` отдаёт HTML страницы входа. - Что: - в docs/OPERATIONS.md (Troubleshooting) добавлен симптом, причина и что делать. - Проверка: - curl -sL http://localhost:8080/object/grid_data?dag_id=... без cookie отдаёт 200 text/html со страницей входа. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
47 KiB
Operations Runbook
Операционный runbook для локального запуска и проверки пайплайна.
Локальный запуск
Базовые команды:
make up(поднимает базовый стенд без Superset init; пересобирает Airflow-образ)make down(остановить и удалить контейнеры/сети проекта)make clean(полная очистка:down -v --remove-orphans)make generated-history-analytics(штатный чистый прогон: стартовая история генератора -> Kafka/STG -> ODS -> DDS -> DM -> Superset)CHECK_LIVE_SEAM=0 make generated-history-check(повторяемая проверка ClickHouse и Superset после прогона только стартовой истории)make generated-history-runtime-check(короткая проверка стыка backfill/live)make generated-history-chain-check(проверка завершённых стыков между порциями истории)make ddl(применяет SQL изsql/ddl/00_databases.sqlиsql/ddl/*/*.sqlв ClickHouse)make data(архивный путь: заливаетdata/*.jsonlв Kafka; не основной источник аналитики)make transform(запускает batch-процесс ODS -> DDS -> DM)make superset-init(повторная инициализация Superset: подключение к ClickHouse, датасеты, дашборд)make test(быстрый предкоммитный набор: тесты генератора, верхние контрактные тесты и проверка compose-конфигурации)make lint(статические проверки: синтаксис Python и Bash, compose-конфиг, пробелы в diff)docker compose psdocker compose logs -f --tail=200 <service>docker compose down(сохраняет named volumes, включаяclickhouse-data)docker compose down -v(удаляет named volumes, использовать осознанно)
make test и make lint не заменяют стендовые проверки, которые управляют
volumes или live-генератором. Для стыка backfill/live отдельно запускайте
make generated-history-runtime-check.
Порты
Порты задаются в docker-compose.yml:
- ClickHouse native:
localhost:8002(пользовательdefault, пароль123456) - ClickHouse HTTP / play-консоль:
http://localhost:9123/play(default/123456) - Kafka:
localhost:9092 - Kafka UI:
http://localhost:8082 - Airflow:
http://localhost:8080(admin/admin) - Superset:
http://localhost:8088(admin/admin) - Prometheus:
http://localhost:9090 - Grafana:
http://localhost:3000(admin/admin)
Airflow DAGs
Штатный ручной путь начинается с world_init: пустая форма импортирует
эталонный мир, затем этот же DAG запускает ETL и проверку.
kafka_load остаётся для экспериментов и совместимости учебного стенда.
world_init
- Запуск: ручной (
Trigger DAG). - Назначение: импорт или служебная сборка стартового мира.
- Операции:
import— операция по умолчанию: импортировать портативный артефакт, затем запуститьetl_pipelineи дождатьсяsuccess;backfill— создать стартовую историю, затем запуститьetl_pipelineи дождатьсяsuccess;check— сверить ClickHouse с manifest из Kafka.
- Параметры:
operation(import/backfill/check);profile— список берётся изPROFILESгенератора;duration—6h,2dи т.п.; пусто означает длительность профиля;seed,model_time_speed— необязательные переопределения мира;artifact_path— дляbackfillпуть сохранения; дляimportпуть чтения. При пустом поле импортируется эталонный мир из репозитория.
Backfill/import требуют чистый стенд: пустые data-топики Kafka и пустые
stg.*_raw. При отказе очистите стенд через make clean. Операции continue
в DAG нет: live-генератор — долгоживущий сервис, его запускают с консоли через
make generator-continue.
world_next_day
- Запуск: вручную с пустой формой.
- Параметров нет.
- Один запуск восстанавливает мир из state, добавляет 24 модельных часа,
запускает
etl_pipelineс полной пересборкой и сверяет витрины с manifest. - Расписание задано каждые 30 минут, но DAG создаётся на паузе;
catchup=False. max_active_runs=1не даёт двум доливкам выполняться параллельно.
world_next_day работает на непустом стенде и не использует проверку чистоты.
Перед записью он требует manifest, state ровно на его T_end и остановленный
live-генератор. Настройки мира берутся из manifest. Один запуск добавляет
полуоткрытый диапазон [T_end, T_end + 24h) в UTC. Новая граница появляется в
boundaries; старый manifest без поля читается как [T0, T_end].
После двух доливок проверьте завершённые стыки:
make generated-history-chain-check
Проверка проходит по внутренним границам boundaries, ищет непарные строки и
смену browser/referer/utm внутри переходящих визитов. Она не меняет
make generated-history-runtime-check для стыка backfill/live.
Результат запуска world_next_day фиксируется новым manifest. Порядок записи:
data-топики, state, manifest. Автоматического отката нет. Если запуск упал до
публикации manifest,
не повторяйте доливку поверх возможного хвоста. Очистите стенд и переимпортируйте
последний исправный портативный артефакт, затем повторите запуск world_next_day.
next-day не перечитывает старые data-топики Kafka. Основная запись manifest
хранит суммы, катящуюся контрольную сумму и SHA-256-ссылку на цепочку точных
множеств. Сами click_id и user_domain_id разбиты на небольшие неизменяемые
фрагменты в отдельном compact-топике. Новый день записывает только новый
фрагмент, поэтому размер одной записи не растёт с возрастом мира, а загрузка
основного manifest не сканирует всю цепочку.
State ссылается на основную запись manifest, чтобы две точки продолжения нельзя
было случайно смешать. Если локальный state создан старым кодом и раздела в нём
нет, повторно запустите world_init с операцией import на чистом стенде.
Перед запуском etl_pipeline пульт проверяет, что DAG не стоит на паузе. Если
стоит, задача падает сразу с подсказкой снять паузу в UI или командой:
docker compose exec -T airflow-webserver airflow dags unpause etl_pipeline
После обновления репозитория выполните make up, чтобы Airflow получил новые
зависимости и DAG-и через пересборку образа. Superset metadata собирается после
готового DM: через make generated-history-analytics или make superset-init.
ddl_init
- Запуск: ручной (
Trigger DAG) - Параметр:
verify_only(bool, defaultfalse) - Назначение: создаёт БД и таблицы в ClickHouse от
00_databasesдо40_dm
kafka_load
- Архивный путь, не основной источник аналитики.
- Запуск: ручной (
Trigger DAG with config) - Параметры:
limit(int, default0) — количество строк (0= все)reset_topics(bool, defaulttrue) — пересоздать топики
- Примеры:
{}
{"limit": 100}
etl_pipeline
- Запуск: ручной (
Trigger DAG with config) - Параметры:
full_refresh(bool, defaulttrue) — очистить DDS перед загрузкойwait_stg_timeout_sec(int, default600, minimum30) — сколько секунд задачаwait_for_stg_dataждёт появления данных в STG, прежде чем упасть по таймауту
- Зависимость: требует наличия данных в STG. В штатном сценарии STG наполняет
make generated-history-analyticsчерез backfill генератора. - Архивные
kafka_loadиmake dataоставлены только для ручных экспериментов и старых проверок. - Гейт целостности DDS:
check_dds_integrityсчитает события без клика, аassert_dds_integrityроняет DAG приorphan_events > 0. Проверка идёт послеload_ddsи доload_dm_summary, чтобы DM не собирался поверх нарушенной связиdds.event -> dds.click. Дляassert_dds_integrityзаданоretries=0: повтор не чинит уже собранную сироту и только задерживает явный failed-статус.
Генератор событий (автономный стриминг)
Автономный сервис для непрерывной генерации событий в Kafka. Работает независимо
от Airflow DAGs. make up его не запускает: live включается только явной
командой.
Управление
# Продолжить live-поток из state
make generator-continue
# Запустить генератор с текущими env напрямую
make generator-up
# Остановить генератор
make generator-down
# Перезапуск с пересборкой
make generator-restart
# Логи
make generator-logs
Конфигурация (env)
| Переменная | Описание | По умолчанию |
|---|---|---|
GEN_TICK_SECONDS |
Интервал между тиками | 5 |
GEN_LAMBDA_BASE_PER_MIN |
Базовая интенсивность (событий/мин) | 30 |
GEN_JITTER_PCT |
Процент вариативности | 20 |
GEN_MIN_EVENTS_PER_TICK |
Минимальный событийный бюджет тика | 1 |
GEN_MAX_EVENTS_PER_TICK |
Максимальный событийный бюджет тика | 50 |
GEN_MAX_SESSION_EVENTS |
Потолок длины одного визита | 30 |
GEN_MAX_ACTIVE_SESSIONS |
Потолок одновременных активных визитов | 200 |
GEN_POPULATION_MAX |
Потолок активной популяции пользователей | 300 |
GEN_P_NEW_USER |
Доля визитов новых пользователей | 0.15 |
GEN_MIN_RETURN_MINUTES |
Минимальная пауза перед возвратом пользователя | 30 |
GEN_MODEL_T0 |
Стартовая модельная точка, ISO 8601 с часовым поясом | 2026-01-01T00:00:00+00:00 |
GEN_MODEL_T_END |
Правая граница стартовой истории для backfill |
пусто |
GEN_MODEL_TIMEZONE |
Часовой пояс модельных часов для дневного коэффициента | UTC |
GEN_MODEL_TIME_SPEED |
Сколько модельных секунд проходит за одну настенную секунду | 1 |
GEN_RUN_MODE |
Режим: live, backfill или next-day |
live |
GEN_LAUNCH_PROFILE |
Имя профиля запуска для логов | daily-wave |
GEN_STARTUP_HISTORY_ARTIFACT |
JSON-файл для экспорта стартовой истории в режиме backfill |
пусто |
GEN_STATE_ENABLED |
Сохранять state v3 между рестартами | true |
GEN_STATE_RESET |
Сбросить state при старте | false |
В docker-compose.yml через окружение переопределяются демо-параметры модели и
режима, например:
GEN_STATE_RESET=true GEN_LAMBDA_BASE_PER_MIN=60 docker compose up -d generator
Контейнерные KAFKA_BOOTSTRAP_SERVERS и GEN_DATA_DIR в compose оставлены
внутренними значениями kafka:29092 и /data.
Стартовая история через backfill
Штатная команда чистого прогона:
make generated-history-analytics
Она выполняет полный сброс volumes, поднимает ClickHouse и Kafka, применяет DDL,
выполняет глагол backfill, прогоняет batch STG -> ODS -> DDS -> DM,
инициализирует Superset и запускает техническую проверку. Для координатора или
CI короткая повторная проверка такого готового стенда:
CHECK_LIVE_SEAM=0 make generated-history-check
По умолчанию команда требует live-строки после T_end и проверяет стык
backfill/live. Чистый make generated-history-analytics отключает эту часть
явно, потому что внутри него live-продолжение не запускается. Для ручной
проверки только стартовой истории используйте:
CHECK_LIVE_SEAM=0 make generated-history-check
Для проверки другого live-окна:
CHECK_LIVE_SEAM=1 GEN_LIVE_CHECK_MINUTES=10 make generated-history-check
Для commit gate issue 17 есть короткий runtime-путь без полного daily-wave на
3 суток и без Superset UI:
make generated-history-runtime-check
По умолчанию он берёт профиль daily-wave, но сжимает историю до 1h и до
25 секунд ждёт в STG визит с browser-событиями по обе стороны границы. Затем
он делает второй batch и проверяет стык с CHECK_LIVE_SEAM=1.
При остановке generator получает SIGTERM и завершает текущий пакет. Параметр
stop_grace_period: 1m даёт время дописать четыре топика, сбросить буферы и
записать историю пакета до принудительной остановки контейнера.
Если машина медленная, можно увеличить только ожидания:
LIVE_SECONDS=45 WAIT_STG_SECONDS=10 make generated-history-runtime-check
По умолчанию команда использует учебный профиль daily-wave: 3 суток с
суточной волной. В live-продолжении он идёт с ×60 и тикает раз в секунду,
поэтому модельные сутки проходят примерно за 24 настенные минуты. Плоский
профиль ci на 6 часов остаётся служебным для автоматических тестов.
Разовую длительность можно задать без ручного расчёта GEN_MODEL_T_END:
GEN_HISTORY_DURATION=2d make generated-history-analytics
GEN_RUN_MODE=backfill быстро проматывает модельное прошлое от GEN_MODEL_T0
до GEN_MODEL_T_END без сна. В Kafka попадают события только за полуоткрытый
отрезок [T0, T_end). В compact-topic generator_state сохраняется state на
T_end, а в generator_startup_history_manifest — manifest с настройками и
контрольными числами. При live-запуске с теми же настройками генератор видит,
что state совпадает с manifest, и стартует ровно с T_end без настенной дельты.
Новый backfill записывает boundaries=[T0, T_end]. Каждый успешный next-day
добавляет одну границу, сдвигает model_t_end и дополняет накопительные
счётчики только событиями нового дня. Полной перечитки Kafka в этом пути нет.
Если историю нужно сохранить в файл и восстановить на чистом стенде без новой генерации, используйте runbook стартовой истории.
Для чистого повтора пересоздавайте volumes. Это сбрасывает ClickHouse,
Kafka-топики данных, state и manifest генератора. make generated-history-analytics
делает это по умолчанию (CLEAN_START=1).
Ручной backfill без всего аналитического контура:
make generator-backfill
Перед записью команда проверяет с хоста, что Kafka data-топики и STG пустые.
Если там уже есть данные, она останавливается с подсказкой make clean.
Старый способ через GEN_RUN_MODE, GEN_STATE_RESET и GEN_MODEL_T_END
остаётся низкоуровневым путём для отладки прямого docker compose run. В обычной
проверке используйте make generated-history-analytics, чтобы не забыть
Superset и итоговый check.
Manifest можно посмотреть так:
docker compose exec -T kafka /opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server kafka:29092 \
--topic generator_startup_history_manifest \
--from-beginning \
--property print.key=true \
--timeout-ms 5000
Live-продолжение стартует с этого state. Используйте тот же профиль или ту же
длительность, что были у backfill. Команда сама выставит GEN_STATE_RESET=false.
Если state записан старой версией генератора, запуск теперь падает громко:
сначала очистите стенд через make clean и пересоздайте историю. Команда
make generator-reset подходит только для осознанного нового live-мира на
чистом стенде: она тоже проверяет, что Kafka data-топики и STG пустые.
make generator-continue
sleep 5
make generator-down
sleep 10
bash scripts/run_batch.sh
Базовая сверка формы данных после backfill:
WITH
toDateTime64('2026-01-01 00:00:00', 6) AS t0,
toDateTime64('2026-01-01 06:00:00', 6) AS t_end
SELECT
uniqExact(user_domain_id) AS users,
uniqExact(click_id) AS visits,
count() AS events,
users < visits AND visits < events AS pyramid_ok,
min(event_ts) AS min_event_ts,
max(event_ts) AS max_event_ts,
min_event_ts >= t0 AND max_event_ts < t_end AS half_open_ok
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_end;
Повторяемость чистого прогона удобно сверять коротким digest по ключевым полям
ClickHouse. Запускайте запрос после bash scripts/run_batch.sh; при одинаковых
GEN_SEED, T0, T_end и настройках значение должно повторяться.
docker compose exec -T clickhouse clickhouse-client \
--user=default \
--password=123456 \
--query "
WITH
toDateTime64('2026-01-01 00:00:00', 6) AS t0,
toDateTime64('2026-01-01 06:00:00', 6) AS t_end
SELECT hex(sipHash128(groupArray(tuple(
event_id,
click_id,
user_domain_id,
event_ts,
page_url_path
)))) AS digest
FROM (
SELECT
event_id,
click_id,
user_domain_id,
event_ts,
page_url_path
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_end
ORDER BY event_id
)"
Возвраты пользователей:
WITH
toDateTime64('2026-01-01 00:00:00', 6) AS t0,
toDateTime64('2026-01-01 06:00:00', 6) AS t_end,
users AS (
SELECT user_domain_id, uniqExact(click_id) AS visits
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_end
AND user_domain_id IS NOT NULL
GROUP BY user_domain_id
)
SELECT
count() AS users,
countIf(visits > 1) AS returning_users,
returning_users / users AS returning_share
FROM users;
Форма длины визита: проверяем не только среднее, а долю коротких визитов,
медиану и долю визитов, срезанных потолком GEN_MAX_SESSION_EVENTS.
WITH
toDateTime64('2026-01-01 00:00:00', 6) AS t0,
toDateTime64('2026-01-01 06:00:00', 6) AS t_end,
30 AS max_session_events,
sessions AS (
SELECT
click_id,
count() AS events_count,
dateDiff('second', min(event_ts), max(event_ts)) AS duration_sec
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_end
GROUP BY click_id
)
SELECT
count() AS visits,
countIf(events_count <= 2) / visits AS short_visit_share,
quantileExact(0.5)(events_count) AS median_events_per_visit,
avg(events_count) AS avg_events_per_visit,
countIf(events_count = max_session_events) / visits AS capped_visit_share,
quantileExact(0.5)(duration_sec) AS median_duration_sec
FROM sessions;
Воронка должна монотонно убывать, а доля дошедших до /confirmation должна быть
в согласованном коридоре для текущих настроек генератора. Для review gate
confirmation_share сравнивается по калибровочной форме «страница была в
визите». Строгий SQL ниже проверяет отдельное свойство: упорядоченный путь
/home -> товары -> /cart -> /payment -> /confirmation.
WITH
toDateTime64('2026-01-01 00:00:00', 6) AS t0,
toDateTime64('2026-01-01 06:00:00', 6) AS t_end,
sessions AS (
SELECT
click_id,
minIf(event_ts, page_url_path = '/home') AS home_ts,
minIf(event_ts, page_url_path IN ('/product_a', '/product_b')) AS product_ts,
minIf(event_ts, page_url_path = '/cart') AS cart_ts,
minIf(event_ts, page_url_path = '/payment') AS payment_ts,
minIf(event_ts, page_url_path = '/confirmation') AS confirmation_ts
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_end
GROUP BY click_id
)
SELECT
countIf(home_ts IS NOT NULL) AS home,
countIf(home_ts IS NOT NULL AND product_ts > home_ts) AS products,
countIf(home_ts IS NOT NULL AND product_ts > home_ts AND cart_ts > product_ts) AS cart,
countIf(home_ts IS NOT NULL AND product_ts > home_ts AND cart_ts > product_ts AND payment_ts > cart_ts) AS payment,
countIf(home_ts IS NOT NULL AND product_ts > home_ts AND cart_ts > product_ts AND payment_ts > cart_ts AND confirmation_ts > payment_ts) AS confirmation,
products <= home AND cart <= products AND payment <= cart AND confirmation <= payment AS monotonic_ok,
confirmation / home AS confirmation_share
FROM sessions;
Калибровочная форма воронки проверяет, что страница была в визите, без строгого
порядка событий. Именно эту форму используем для сравнения confirmation_share
в review gate.
WITH
toDateTime64('2026-01-01 00:00:00', 6) AS t0,
toDateTime64('2026-01-01 06:00:00', 6) AS t_end,
sessions AS (
SELECT
click_id,
countIf(page_url_path = '/home') > 0 AS has_home,
countIf(page_url_path IN ('/product_a', '/product_b')) > 0 AS has_product,
countIf(page_url_path = '/cart') > 0 AS has_cart,
countIf(page_url_path = '/payment') > 0 AS has_payment,
countIf(page_url_path = '/confirmation') > 0 AS has_confirmation
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_end
GROUP BY click_id
)
SELECT
countIf(has_home) AS home,
countIf(has_home AND has_product) AS products,
countIf(has_home AND has_product AND has_cart) AS cart,
countIf(has_home AND has_product AND has_cart AND has_payment) AS payment,
countIf(has_home AND has_product AND has_cart AND has_payment AND has_confirmation) AS confirmation,
products <= home AND cart <= products AND payment <= cart AND confirmation <= payment AS monotonic_ok,
confirmation / home AS confirmation_share
FROM sessions;
Стык backfill + live проверяется после короткого live-продолжения:
WITH
toDateTime64('2026-01-01 00:00:00', 6) AS t0,
toDateTime64('2026-01-01 06:00:00', 6) AS t_end,
toDateTime64('2026-01-01 06:10:00', 6) AS t_live_end
SELECT
count() AS events,
uniqExact(event_id) AS unique_events,
events - unique_events AS duplicate_events,
countIf(event_ts = t_end) AS boundary_events,
min(event_ts) AS min_event_ts,
max(event_ts) AS max_event_ts
FROM dm.v_events_enriched
WHERE event_ts >= t0 AND event_ts < t_live_end;
Однородность визитов, переходящих через T_end:
WITH
toDateTime64('2026-01-01 06:00:00', 6) AS t_end,
crossing AS (
SELECT
click_id,
min(event_ts) AS first_ts,
max(event_ts) AS last_ts,
groupUniqArray(user_domain_id) AS users,
groupUniqArray(device_type) AS devices,
groupUniqArray(os_name) AS os_names,
groupUniqArray(geo_country) AS countries
FROM dm.v_events_enriched
WHERE event_ts >= t_end - INTERVAL 30 MINUTE
AND event_ts < t_end + INTERVAL 30 MINUTE
GROUP BY click_id
HAVING first_ts < t_end AND last_ts >= t_end
)
SELECT
count() AS crossing_visits,
countIf(
length(users) = 1
AND length(devices) = 1
AND length(os_names) = 1
AND length(countries) = 1
) AS homogeneous_visits,
crossing_visits = homogeneous_visits AS context_ok
FROM crossing;
Проверка модельного времени в ClickHouse
Для повторяемой проверки используйте чистый стенд и явный сброс состояния
генератора. event_timestamp в событиях — модельное время от GEN_MODEL_T0, а
не настенное время запуска процесса. При GEN_MODEL_TIME_SPEED=K один тик
покрывает GEN_TICK_SECONDS * K модельных секунд, и событийный бюджет считается
по этой модельной длительности.
make clean
docker compose up -d clickhouse kafka
make ddl
GEN_SEED=4242 \
GEN_MODEL_T0=2026-01-01T10:00:00+00:00 \
GEN_MODEL_TIMEZONE=UTC \
GEN_STATE_RESET=true \
GEN_TICK_SECONDS=60 \
GEN_LAMBDA_BASE_PER_MIN=60 \
GEN_JITTER_PCT=0 \
docker compose up -d --build generator
# Убедитесь по логам, что Tick 1 завершился, а Tick 2 ещё не стартовал.
sleep 9
docker compose logs --tail=40 generator
docker compose stop generator
sleep 5
bash scripts/run_batch.sh
docker compose exec -T clickhouse clickhouse-client --user=default --password=123456 --query "
SELECT
count() AS events,
min(event_ts) AS min_event_ts,
max(event_ts) AS max_event_ts,
uniqExact(event_id) AS unique_events,
uniqExact(click_id) AS unique_clicks
FROM ods.browser_event
FORMAT Vertical"
Повторите блок с теми же значениями. При чистом стенде и GEN_STATE_RESET=true
контрольные числа должны совпасть, а min_event_ts должен начинаться от
2026-01-01 10:00:00.
Топик истории
Генератор пишет историю батчей в топик generator_batch_history (JSON, ключ batch_id).
Важно: генератор требует работающей Kafka. Без Kafka генератор упадёт при старте или потеряет события.
# Чтение истории из Kafka
docker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server kafka:29092 \
--topic generator_batch_history \
--from-beginning
Метрики
Prometheus метрики доступны на http://localhost:9109/metrics:
generator_events_total— счётчик отправленных событийgenerator_publish_errors_total— ошибки публикацииgenerator_tick_duration_seconds— длительность тикаgenerator_last_success_timestamp— время последнего успешного или частично успешного тика
Мониторинг через Grafana
Dashboard URL: http://localhost:3000/d/generator-overview
Дашборд "Generator Overview" автоматически загружается при старте Grafana и содержит:
| Раздел | Панели | Описание |
|---|---|---|
| Overview | Events/min | Скорость генерации событий в минуту |
| Tick Duration | Медиана и p99 длительности тика | |
| Last Successful Tick | Время последнего успешного тика | |
| Events by Topic | Events per Hour (24h bar chart) | Распределение событий по часам и топикам |
| Events Rate by Topic | График по 4 топикам (browser, location, device, geo) | |
| Total Events by Topic | Суммарные счётчики по каждому топику | |
| Errors | Total Errors | Общее число ошибок публикации |
| Error Rate | Скорость ошибок (err/min) | |
| Errors by Topic | Ошибки разбиты по топикам | |
| Tick Statistics | Tick Duration Distribution | p50, p95, p99 длительности тиков |
| Events per Tick | Среднее число событий на тик | |
| Hour Factor | Текущий временной множитель (0.7/1.0/1.2) | |
| Status | Generator Status | Статус работы (enabled/disabled) |
| Generator Health | Статус активности (heartbeat last tick), не проверяет state save | |
| Time Since Last Tick | Время с последнего тика | |
| Info | Полезные команды и параметры конфигурации |
Troubleshooting генератора
Нет данных на дашборде:
- Проверить, что генератор запущен:
docker compose ps generator - Проверить метрики напрямую:
curl http://localhost:9109/metrics - Проверить target в Prometheus:
http://localhost:9090/targets(job: generator)
Высокий error rate:
- Проверить доступность Kafka:
docker compose ps kafka - Смотреть логи:
make generator-logs - Проверить consumer lag: дашборд Kafka Overview
Длительные тики (p99 > 1s):
- Проверить CPU/ресурсы контейнера
- Возможно, высокая нагрузка на Kafka — проверить дашборд Kafka
Dashboard не загрузился:
# Перезагрузить provisioning Grafana
curl -s -u admin:admin -X POST http://localhost:3000/api/admin/provisioning/dashboards/reload
# Или пересоздать контейнер
docker compose restart grafana
Рекомендуемый сценарий
# Полный чистый путь: генерация -> STG -> ODS -> DDS -> DM -> Superset
make generated-history-analytics
# Повторная проверка данных стартовой истории без пересоздания
CHECK_LIVE_SEAM=0 make generated-history-check
# Проверка стыка backfill/live
make generated-history-runtime-check
Обычный make generated-history-check корректен только после
make generator-continue и повторного make transform, когда в витринах уже
есть строки после T_end.
Быстрые проверки
- Kafka ingest: наличие данных генератора в
stg.*и типизированных строк вods.*. - Airflow UI:
http://localhost:8080показывает лестницуddl_init→world_init→world_next_day, а такжеkafka_loadиetl_pipeline. - BI: витрина
dm.v_events_enrichedотвечает за разумное время при фильтре по дате.
Мониторинг
TL;DR после git pull
# Быстрый вариант (make)
make reload-monitoring
# Если мониторинг "залип" (No data/out of bounds) — жесткое восстановление:
make recover-monitoring
# Или вручную:
docker compose up -d prometheus grafana kafka-exporter statsd-exporter
docker compose restart prometheus statsd-exporter
curl -s -u admin:admin -X POST http://localhost:3000/api/admin/provisioning/datasources/reload
curl -s -u admin:admin -X POST http://localhost:3000/api/admin/provisioning/dashboards/reload
curl -s -u admin:admin -X POST http://localhost:3000/api/admin/provisioning/alerting/reload
Если менялся configs/prometheus_ch.xml: docker compose restart clickhouse.
Prometheus + Grafana для ClickHouse, Kafka и Airflow
Стек мониторинга поднимается вместе с остальной инфраструктурой:
# Проверить статус сервисов мониторинга
docker compose ps prometheus grafana
# Проверить скрейп ClickHouse в Prometheus
curl -s http://localhost:9090/api/v1/targets | grep -o '"health":"[^"]*"'
Конфигурация
- ClickHouse: встроенный Prometheus endpoint (
/metricsна порту9126) - Kafka: через
kafka-exporter(порт9308) - Airflow: через
statsd-exporter(StatsD → Prometheus, порт9102)- Airflow отправляет метрики в StatsD-формате на
statsd-exporter:8125 - Mapping конфигурация:
configs/statsd_mapping.yml
- Airflow отправляет метрики в StatsD-формате на
- Grafana provisioning (
configs/grafana/provisioning/):- Дашборды: ClickHouse Overview, Kafka Overview, Airflow Overview, Generator Overview
- Алерты: ClickHouse, Kafka, Airflow
После git pull: быстрый апдейт мониторинга
Если прилетели изменения в configs/grafana/provisioning/* или configs/prometheus.yml, примените их так:
# Рекомендуемый способ (через make)
make reload-monitoring
# Если метрики пропали/залипли:
make recover-monitoring
# Или вручную:
docker compose rm -sf prometheus statsd-exporter
docker compose up -d prometheus grafana kafka-exporter statsd-exporter
docker compose restart airflow-scheduler airflow-webserver
curl -s -u admin:admin -X POST http://localhost:3000/api/admin/provisioning/datasources/reload
curl -s -u admin:admin -X POST http://localhost:3000/api/admin/provisioning/dashboards/reload
curl -s -u admin:admin -X POST http://localhost:3000/api/admin/provisioning/alerting/reload
Проверка результата:
# Дашборды и алерты
curl -s -u admin:admin http://localhost:3000/api/v1/provisioning/alert-rules | grep -o '"title":"[^"]*"'
# Kafka метрики
curl -s http://localhost:9090/api/v1/targets | grep kafka
curl -s http://localhost:9308/metrics | grep "^kafka_brokers"
Если в пулле изменился configs/prometheus_ch.xml, дополнительно перезапустите ClickHouse:
docker compose restart clickhouse
Если дашборд Kafka не загрузился (ошибка "Dashboard title cannot be empty" в логах), пересоздайте контейнер Grafana:
docker compose stop grafana && docker compose rm -f grafana && docker compose up -d grafana
Дашборд ClickHouse Overview
URL: http://localhost:3000/d/clickhouse-overview/clickhouse-overview
| Раздел | Метрики |
|---|---|
| System Health | CPU Usage, Memory Resident, Memory Code |
| Query Performance | Queries per Second, Active Queries, Failed Queries (total), Total Queries, Inserted Rows/sec |
| MergeTree Storage | Total Parts, Parts by State, Total Merges, Merges per Second |
Принятое решение по метрикам: сверили naming через Context7 (/clickhouse/clickhouse-docs, раздел Prometheus interface) и заменили недоступные в 25.1 серии на фактически экспортируемые (ClickHouseProfileEvents_InsertedRows, ClickHouseAsyncMetrics_TotalPartsOfMergeTreeTables, ClickHouseMetrics_Parts*).
Дашборд Kafka Overview
URL: http://localhost:3000/d/kafka-overview/kafka-overview
| Раздел | Метрики |
|---|---|
| Cluster Health | Brokers Up, Topics, Total Partitions, Consumer Groups |
| Throughput | Messages In / sec by Topic |
| Consumers | Consumer Lag by Group |
| Partitions | Partition Offsets (Current) |
Источник метрик: kafka-exporter (danielqsj/kafka-exporter), формат конфигурации подтверждён через Context7 (/danielqsj/kafka_exporter, /prometheus/docs).
Проверка метрик
# Prometheus собирает метрики ClickHouse
curl -s "http://localhost:9090/api/v1/query?query=ClickHouseAsyncMetrics_MemoryResident"
curl -s "http://localhost:9090/api/v1/query?query=ClickHouseProfileEvents_Query"
# Prometheus собирает метрики Kafka
curl -s "http://localhost:9090/api/v1/query?query=kafka_brokers"
curl -s "http://localhost:9090/api/v1/query?query=kafka_consumergroup_lag"
# Прямая проверка kafka-exporter
curl -s http://localhost:9308/metrics | grep "^kafka_"
# Проверить, что Prometheus собирает метрики
curl -s "http://localhost:9090/api/v1/query?query=ClickHouseAsyncMetrics_MemoryResident"
# Проверить счётчик запросов
curl -s "http://localhost:9090/api/v1/query?query=ClickHouseProfileEvents_Query"
Алерты Grafana
ClickHouse Alerts — provisioning-файл: configs/grafana/provisioning/alerting/clickhouse-alert-rules.yml
Настроены правила:
ClickHouse Failed Queries Rate—rate(ClickHouseProfileEvents_FailedQuery[5m]) > 0в течение2mClickHouse Memory Resident High—MemoryResident / OSMemoryTotal * 100 > 85в течение5mClickHouse Parts Active High—ClickHouseMetrics_PartsActive > 500в течение10m
Kafka Alerts — provisioning-файл: configs/grafana/provisioning/alerting/kafka-alert-rules.yml
Настроены правила:
Kafka Broker Down—kafka_brokers < 1в течение1mKafka Consumer Lag High—kafka_consumergroup_lag > 10000в течение5mKafka No Messages Produced—rate(kafka_topic_partition_current_offset[5m]) < 0.1в течение10mKafka Consumer Group Missingне включён: для демо-стенда даёт шум на стартовых прогонах и не повышает диагностику по сравнению с lag/throughput.
Проверка и reload без рестарта контейнера:
# Список правил unified alerting
curl -s -u admin:admin http://localhost:3000/api/v1/provisioning/alert-rules
# Принудительно перечитать provisioning alerting
curl -s -X POST -u admin:admin http://localhost:3000/api/admin/provisioning/alerting/reload
Troubleshooting мониторинга
Общие проблемы:
- "No data" в Grafana: проверить, что Prometheus видит target (
Status -> Targetsв UI) - Dashboard не загрузился: проверить логи Grafana — provisioning работает при первом старте контейнера
- Prometheus spam
out of boundsи дашборды пустые: выполнитьmake recover-monitoring
ClickHouse:
- Метрики не обновляются: ClickHouse экспортирует метрики на
0.0.0.0:9126внутри сети Docker
Kafka:
connection refusedк Kafka: проверить, что kafka-exporter используетkafka:29092(внутренняя сеть), неlocalhost:9092- Метрики Kafka не появляются: проверить, что kafka-exporter подключился к Kafka —
docker compose logs kafka-exporter - Нет консьюмер-групп: kafka-exporter показывает lag только при наличии активных консьюмеров с закоммиченными offset
Troubleshooting
etl_pipelineпадает с ошибкой схемы: сначала запуститьddl_init.- Airflow и Superset быстро завершают сеансы при одновременной работе:
на
localhostсовпадали имена cookie, потому что порты не разделяют их. В Superset cookie переименована вsuperset_session. - В Airflow всплывает
Auto-refresh Error: can't access property "find", p.dagRuns is undefined: сеанс закончился, и авто-обновление Grid вместо данных получило страницу входа. Ошибка косметическая — достаточно обновить страницу и войти заново. Сам Grid не проверяет форму ответа (баг UI Airflow 2.10.x, в Airflow 3 интерфейс переписан). git pullпадает сPermission deniedнаdata/*илиconfigs/grafana/provisioning/*:- Причина: локально есть файлы/каталоги не вашего пользователя (часто после запуска контейнеров с root-пользователем).
- Диагностика:
ls -ld data configs/grafana/provisioning ls -l data | head -n 20 - Быстрое восстановление:
# Владелец и права для рабочей копии репозитория sudo chown -R "$USER:$USER" . find . -type d -exec chmod u+rwx {} \; find . -type f -exec chmod u+rw {} \; - После восстановления повторить
git pull --ff-only. - Не запускать
gitчерезsudo.
grafanaперезапускается с ошибкойattempt to write a readonly database:- Причина: старый
grafana_libсодержитgrafana.db, созданный root-пользователем. - Простой recovery (сбросить только volume Grafana):
docker compose stop grafana docker volume ls | grep grafana_lib docker volume rm <project>_grafana_lib docker compose up -d grafana
- Причина: старый
- В Superset ошибки
DB engine ErrorиCannot load filter, а в логах естьCan't load plugin: sqlalchemy.dialects:clickhouse.connect:- Причина: некорректный URI диалекта ClickHouse (
clickhouse+connect://...). - Используйте URI
clickhousedb://...и пересоберите сервисы Superset:docker compose build superset superset-init docker compose up -d clickhouse docker compose up -d --force-recreate superset-init superset
- Причина: некорректный URI диалекта ClickHouse (
- После
docker compose down -vнужно повторно прогнатьmake generated-history-analytics. - После
make clean/down -vSuperset стартует, но витриныdm.*ещё пустые или отсутствуют до прогона стартовой истории; используйтеmake generated-history-analytics. - Архивную загрузку
make dataиспользовать только для ручных экспериментов.