From f85f876a7fc702b52cb000c5bba5167e9d2ac4e5 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Fri, 16 Oct 2026 15:21:21 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B2=D0=B8=D1=81=20ClickHou?= =?UTF-8?q?se,=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D1=81=D0=BE=D0=B1=D1=8B?= =?UTF-8?q?=D1=82=D0=B8=D0=B9=20=D0=B8=20=D0=B0=D0=B3=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D0=B0=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose (prod и dev) — контейнер clickhouse 25.3 с volume, ulimits и healthcheck - порт только на loopback хоста, номер задаётся CLICKHOUSE_HTTP_PORT (test 8124, prod 8123) - clickhouse/init/001_events.sql — таблица analytics.events с дневными партициями плюс агрегаты events_daily и money_daily - бот ходит в ClickHouse по имени сервиса и стартует независимо от его готовности - env-примеры контуров, цель make ch и проверка ClickHouse в check_contour.sh - версия 0.3.1 --- Makefile | 6 ++- README.md | 2 +- clickhouse/init/001_events.sql | 72 ++++++++++++++++++++++++++++++++++ common/version.py | 2 +- docker-compose.prod.yml | 39 ++++++++++++++++++ docker-compose.yml | 35 +++++++++++++++++ env/prod.env.example | 11 +++++- env/test.env.example | 11 +++++- scripts/check_contour.sh | 16 +++++++- tests/test_version.py | 2 +- 10 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 clickhouse/init/001_events.sql diff --git a/Makefile b/Makefile index fb0bcf2..8d0d740 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ COMPOSE := docker compose -p $(E) --env-file $(FILE) -f docker-compose.prod.yml .DEFAULT_GOAL := help .RECIPEPREFIX := > -.PHONY: help up pull down restart logs ps images check psql +.PHONY: help up pull down restart logs ps images check psql ch help: > @echo "Контур задаётся через E=test|prod (по умолчанию test):" @@ -22,6 +22,7 @@ help: > @echo " make images E=test образы контура" > @echo " make check E=test проверка контура (скрипт)" > @echo " make psql E=test psql в БД контура" +> @echo " make ch E=test clickhouse-client контура" up: > $(COMPOSE) up -d @@ -49,3 +50,6 @@ check: psql: > $(COMPOSE) exec postgres sh -lc 'psql -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"' + +ch: +> $(COMPOSE) exec clickhouse sh -lc 'clickhouse-client --user "$$CLICKHOUSE_USER" --password "$$CLICKHOUSE_PASSWORD" --database analytics' diff --git a/README.md b/README.md index 7358dae..a4d712d 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ pytest ## Версионирование -- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.0`. +- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.1`. - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`). - Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3). - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. diff --git a/clickhouse/init/001_events.sql b/clickhouse/init/001_events.sql new file mode 100644 index 0000000..1711a7c --- /dev/null +++ b/clickhouse/init/001_events.sql @@ -0,0 +1,72 @@ +-- Схема аналитики ClickHouse. +-- +-- Этот каталог монтируется в контейнер как /docker-entrypoint-initdb.d, +-- поэтому скрипт применяется ТОЛЬКО при первой инициализации пустого data-каталога. +-- Flyway у нас только для Postgres; изменения схемы здесь — новым файлом +-- 00N_*.sql (и применением вручную на существующем контуре) плюс раздел в DEPLOY.md. + +CREATE DATABASE IF NOT EXISTS analytics; + +-- Единая широкая таблица событий (см. docs/events.md). +-- Партиционирование по дню: сырые партиции удаляются после ежедневного бэкапа +-- на домашний сервер (scripts/clickhouse_backup.sh), поэтому удаление должно +-- быть дешёвым (DROP PARTITION, без mutation). +CREATE TABLE IF NOT EXISTS analytics.events +( + event_time DateTime('UTC'), + event_type LowCardinality(String), + user_id Int64, + username String, + chat_id Int64, + payload String, + bot_version String, + prod UInt8 +) +ENGINE = MergeTree +PARTITION BY toYYYYMMDD(event_time) +ORDER BY (event_time, event_type, user_id); + +-- Агрегат: события по дням и типам. Остаётся на VPS, когда сырые удалены. +CREATE TABLE IF NOT EXISTS analytics.events_daily +( + day Date, + event_type LowCardinality(String), + user_id Int64, + chat_id Int64, + events UInt64 +) +ENGINE = SummingMergeTree +ORDER BY (day, event_type, user_id, chat_id); + +CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.events_daily_mv TO analytics.events_daily AS +SELECT + toDate(event_time) AS day, + event_type, + user_id, + chat_id, + count() AS events +FROM analytics.events +GROUP BY day, event_type, user_id, chat_id; + +-- Агрегат: денежные потоки по дням (сумма amount из payload событий экономики). +CREATE TABLE IF NOT EXISTS analytics.money_daily +( + day Date, + event_type LowCardinality(String), + user_id Int64, + amount_sum Int64, + events UInt64 +) +ENGINE = SummingMergeTree +ORDER BY (day, event_type, user_id); + +CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.money_daily_mv TO analytics.money_daily AS +SELECT + toDate(event_time) AS day, + event_type, + user_id, + sum(JSONExtractInt(payload, 'amount')) AS amount_sum, + count() AS events +FROM analytics.events +WHERE event_type IN ('money_accrual', 'money_spend') +GROUP BY day, event_type, user_id; diff --git a/common/version.py b/common/version.py index 3d5b690..1206e0b 100644 --- a/common/version.py +++ b/common/version.py @@ -7,4 +7,4 @@ from __future__ import annotations -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index dc008d1..a898fa8 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -46,17 +46,55 @@ services: environment: DB_HOST: postgres DB_PORT: "5432" + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_PORT: "8123" depends_on: postgres: condition: service_healthy migrate: condition: service_completed_successfully + # Без ожидания здоровья: аналитика некритична, бот работает и при недоступном ClickHouse. + clickhouse: + condition: service_started logging: driver: json-file options: max-size: "10m" max-file: "3" + # Аналитика событий (docs/events.md). Наружу не публикуется: + # только loopback хоста для бэкапа/туннеля; бот ходит внутрь сети compose. + clickhouse: + image: clickhouse/clickhouse-server:25.3 + restart: unless-stopped + environment: + CLICKHOUSE_DB: ${CLICKHOUSE_DB:-analytics} + CLICKHOUSE_USER: ${CLICKHOUSE_USER:-de_bot} + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?задайте CLICKHOUSE_PASSWORD} + # Нужно для создания пользователей/грантов (в финале — read-only пользователь для Superset). + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" + volumes: + - clickhousedata:/var/lib/clickhouse + # DDL применяется один раз при первой инициализации (пустой data-каталог). + - ./clickhouse/init:/docker-entrypoint-initdb.d:ro + ports: + # Порт контура задаётся в env (prod 8123, test 8124) — иначе конфликт на одной VPS. + - "127.0.0.1:${CLICKHOUSE_HTTP_PORT:-8123}:8123" + ulimits: + nofile: + soft: 262144 + hard: 262144 + healthcheck: + test: + - CMD-SHELL + - >- + clickhouse-client --user "$${CLICKHOUSE_USER}" --password "$${CLICKHOUSE_PASSWORD}" + --query "SELECT 1" > /dev/null || exit 1 + interval: 15s + timeout: 10s + retries: 10 + start_period: 20s + # --- Reverse proxy и TLS (Блок 2.4) --- # Включается, когда появится админка/Mini App (Блоки 4–5). # Раскомментировать и положить deploy/Caddyfile: @@ -74,5 +112,6 @@ services: volumes: pgdata: + clickhousedata: # caddy_data: # caddy_config: diff --git a/docker-compose.yml b/docker-compose.yml index ece4353..c3c788f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,16 +51,51 @@ services: # Внутри сети compose БД доступна по имени сервиса. DB_HOST: postgres DB_PORT: "5432" + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_PORT: "8123" depends_on: postgres: condition: service_healthy migrate: condition: service_completed_successfully + # Аналитика некритична: бот стартует, даже если ClickHouse ещё не готов. + clickhouse: + condition: service_started logging: driver: json-file options: max-size: "10m" max-file: "3" + # Аналитика событий (Блок 3). DDL — в clickhouse/init/. + clickhouse: + image: clickhouse/clickhouse-server:25.3 + restart: unless-stopped + environment: + CLICKHOUSE_DB: ${CLICKHOUSE_DB:-analytics} + CLICKHOUSE_USER: ${CLICKHOUSE_USER:-de_bot} + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?задайте CLICKHOUSE_PASSWORD в .env} + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" + volumes: + - clickhousedata:/var/lib/clickhouse + - ./clickhouse/init:/docker-entrypoint-initdb.d:ro + ports: + - "127.0.0.1:8123:8123" + ulimits: + nofile: + soft: 262144 + hard: 262144 + healthcheck: + test: + - CMD-SHELL + - >- + clickhouse-client --user "$${CLICKHOUSE_USER}" --password "$${CLICKHOUSE_PASSWORD}" + --query "SELECT 1" > /dev/null || exit 1 + interval: 15s + timeout: 10s + retries: 10 + start_period: 20s + volumes: pgdata: + clickhousedata: diff --git a/env/prod.env.example b/env/prod.env.example index 30f0427..b75e658 100644 --- a/env/prod.env.example +++ b/env/prod.env.example @@ -7,7 +7,7 @@ ENV_FILE=env/prod.env APP_ENV=prod -# Образ в реестре (latest = последняя сборка из master; можно закрепить или версию, напр. 0.2.5) +# Образ в реестре (latest = последняя сборка из master; можно закрепить или версию) REGISTRY=git.mpakosrv.ru IMAGE_TAG=latest @@ -30,3 +30,12 @@ ALERT_CHAT_ID=0 # Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто ADMIN_IDS= TIMEZONE=Europe/Moscow + +# ClickHouse (аналитика событий, см. docs/events.md). +# Наружу не публикуется: только loopback хоста для бэкапа/туннеля; +# бот ходит внутрь сети compose по имени сервиса clickhouse:8123. +CLICKHOUSE_DB=analytics +CLICKHOUSE_USER=de_bot +CLICKHOUSE_PASSWORD= +# Порт на хосте (для бэкапа). У test и prod он должен различаться. +CLICKHOUSE_HTTP_PORT=8123 diff --git a/env/test.env.example b/env/test.env.example index 2090426..168ab93 100644 --- a/env/test.env.example +++ b/env/test.env.example @@ -6,7 +6,7 @@ ENV_FILE=env/test.env APP_ENV=dev -# Образ в реестре (latest = последняя сборка из master; можно закрепить или версию, напр. 0.2.5) +# Образ в реестре (latest = последняя сборка из master; можно закрепить или версию) REGISTRY=git.mpakosrv.ru IMAGE_TAG=latest @@ -28,3 +28,12 @@ ALERT_CHAT_ID=0 # Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто ADMIN_IDS= TIMEZONE=Europe/Moscow + +# ClickHouse (аналитика событий, см. docs/events.md). +# Наружу не публикуется: только loopback хоста для бэкапа/туннеля; +# бот ходит внутрь сети compose по имени сервиса clickhouse:8123. +CLICKHOUSE_DB=analytics +CLICKHOUSE_USER=de_bot +CLICKHOUSE_PASSWORD= +# Порт на хосте: у test он отличается от prod (8123), иначе конфликт на одной VPS. +CLICKHOUSE_HTTP_PORT=8124 diff --git a/scripts/check_contour.sh b/scripts/check_contour.sh index 4097ea6..b2d1a4b 100644 --- a/scripts/check_contour.sh +++ b/scripts/check_contour.sh @@ -25,7 +25,7 @@ echo "== проверки ==" running="$($COMPOSE ps --services --status running || true)" fail=0 -for svc in postgres bot; do +for svc in postgres clickhouse bot; do if echo "$running" | grep -qx "$svc"; then echo "OK ${svc}: running" else @@ -49,4 +49,18 @@ else fail=1 fi +# ClickHouse отвечает и схема аналитики применена (initdb применяется один раз, +# при первой инициализации пустого data-каталога) +if $COMPOSE exec -T clickhouse sh -lc 'clickhouse-client --user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" --query "SELECT 1"' >/dev/null 2>&1; then + echo "OK clickhouse: принимает запросы" + if $COMPOSE exec -T clickhouse sh -lc 'clickhouse-client --user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" --query "SELECT count() FROM analytics.events"' >/dev/null 2>&1; then + echo "OK clickhouse: таблица analytics.events доступна" + else + echo "WARN clickhouse: нет доступа к analytics.events (см. clickhouse/init/001_events.sql)" >&2 + fi +else + echo "FAIL clickhouse: не отвечает" >&2 + fail=1 +fi + exit $fail diff --git a/tests/test_version.py b/tests/test_version.py index 9a3d7c9..d7afb64 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,4 +7,4 @@ from common import __version__ def test_version_is_set(): assert __version__ - assert __version__ == "0.3.0" + assert __version__ == "0.3.1"