Сервис ClickHouse, схема событий и агрегаты

- 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
This commit is contained in:
MpakobecEx 2026-10-16 15:21:21 +05:00
parent 84f75e7c7d
commit f85f876a7f
10 changed files with 189 additions and 7 deletions

View File

@ -9,7 +9,7 @@ COMPOSE := docker compose -p $(E) --env-file $(FILE) -f docker-compose.prod.yml
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.RECIPEPREFIX := > .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: help:
> @echo "Контур задаётся через E=test|prod (по умолчанию test):" > @echo "Контур задаётся через E=test|prod (по умолчанию test):"
@ -22,6 +22,7 @@ help:
> @echo " make images E=test образы контура" > @echo " make images E=test образы контура"
> @echo " make check E=test проверка контура (скрипт)" > @echo " make check E=test проверка контура (скрипт)"
> @echo " make psql E=test psql в БД контура" > @echo " make psql E=test psql в БД контура"
> @echo " make ch E=test clickhouse-client контура"
up: up:
> $(COMPOSE) up -d > $(COMPOSE) up -d
@ -49,3 +50,6 @@ check:
psql: psql:
> $(COMPOSE) exec postgres sh -lc 'psql -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"' > $(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'

View File

@ -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`). - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3). - Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.

View File

@ -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;

View File

@ -7,4 +7,4 @@
from __future__ import annotations from __future__ import annotations
__version__ = "0.3.0" __version__ = "0.3.1"

View File

@ -46,17 +46,55 @@ services:
environment: environment:
DB_HOST: postgres DB_HOST: postgres
DB_PORT: "5432" DB_PORT: "5432"
CLICKHOUSE_HOST: clickhouse
CLICKHOUSE_PORT: "8123"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully
# Без ожидания здоровья: аналитика некритична, бот работает и при недоступном ClickHouse.
clickhouse:
condition: service_started
logging: logging:
driver: json-file driver: json-file
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" 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) --- # --- Reverse proxy и TLS (Блок 2.4) ---
# Включается, когда появится админка/Mini App (Блоки 4–5). # Включается, когда появится админка/Mini App (Блоки 4–5).
# Раскомментировать и положить deploy/Caddyfile: # Раскомментировать и положить deploy/Caddyfile:
@ -74,5 +112,6 @@ services:
volumes: volumes:
pgdata: pgdata:
clickhousedata:
# caddy_data: # caddy_data:
# caddy_config: # caddy_config:

View File

@ -51,16 +51,51 @@ services:
# Внутри сети compose БД доступна по имени сервиса. # Внутри сети compose БД доступна по имени сервиса.
DB_HOST: postgres DB_HOST: postgres
DB_PORT: "5432" DB_PORT: "5432"
CLICKHOUSE_HOST: clickhouse
CLICKHOUSE_PORT: "8123"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully
# Аналитика некритична: бот стартует, даже если ClickHouse ещё не готов.
clickhouse:
condition: service_started
logging: logging:
driver: json-file driver: json-file
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" 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: volumes:
pgdata: pgdata:
clickhousedata:

11
env/prod.env.example vendored
View File

@ -7,7 +7,7 @@ ENV_FILE=env/prod.env
APP_ENV=prod APP_ENV=prod
# Образ в реестре (latest = последняя сборка из master; можно закрепить <sha> или версию, напр. 0.2.5) # Образ в реестре (latest = последняя сборка из master; можно закрепить <sha> или версию)
REGISTRY=git.mpakosrv.ru REGISTRY=git.mpakosrv.ru
IMAGE_TAG=latest IMAGE_TAG=latest
@ -30,3 +30,12 @@ ALERT_CHAT_ID=0
# Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто # Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто
ADMIN_IDS= ADMIN_IDS=
TIMEZONE=Europe/Moscow 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

11
env/test.env.example vendored
View File

@ -6,7 +6,7 @@ ENV_FILE=env/test.env
APP_ENV=dev APP_ENV=dev
# Образ в реестре (latest = последняя сборка из master; можно закрепить <sha> или версию, напр. 0.2.5) # Образ в реестре (latest = последняя сборка из master; можно закрепить <sha> или версию)
REGISTRY=git.mpakosrv.ru REGISTRY=git.mpakosrv.ru
IMAGE_TAG=latest IMAGE_TAG=latest
@ -28,3 +28,12 @@ ALERT_CHAT_ID=0
# Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто # Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто
ADMIN_IDS= ADMIN_IDS=
TIMEZONE=Europe/Moscow 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

View File

@ -25,7 +25,7 @@ echo "== проверки =="
running="$($COMPOSE ps --services --status running || true)" running="$($COMPOSE ps --services --status running || true)"
fail=0 fail=0
for svc in postgres bot; do for svc in postgres clickhouse bot; do
if echo "$running" | grep -qx "$svc"; then if echo "$running" | grep -qx "$svc"; then
echo "OK ${svc}: running" echo "OK ${svc}: running"
else else
@ -49,4 +49,18 @@ else
fail=1 fail=1
fi 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 exit $fail

View File

@ -7,4 +7,4 @@ from common import __version__
def test_version_is_set(): def test_version_is_set():
assert __version__ assert __version__
assert __version__ == "0.3.0" assert __version__ == "0.3.1"