Ежедневный бэкап событий ClickHouse на домашний сервер
- scripts/clickhouse_backup.sh выгружает вчерашнюю партицию в Parquet.gz, передаёт её по rsync, импортирует и сверяет количество строк - scripts/clickhouse_import.sh импортирует архив в ClickHouse домашнего сервера и повторный импорт дня не дублирует строки - на VPS удаляются только партиции старше грейса, и только после успешной сверки - env/backup.env.example с параметрами переноса, цель make backup - версия 0.3.4
This commit is contained in:
parent
2031133538
commit
dbf78aa1d5
6
Makefile
6
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 ch
|
||||
.PHONY: help up pull down restart logs ps images check psql ch backup
|
||||
|
||||
help:
|
||||
> @echo "Контур задаётся через E=test|prod (по умолчанию test):"
|
||||
|
|
@ -23,6 +23,7 @@ help:
|
|||
> @echo " make check E=test проверка контура (скрипт)"
|
||||
> @echo " make psql E=test psql в БД контура"
|
||||
> @echo " make ch E=test clickhouse-client контура"
|
||||
> @echo " make backup E=test бэкап вчерашних событий на домашний сервер"
|
||||
|
||||
up:
|
||||
> $(COMPOSE) up -d
|
||||
|
|
@ -53,3 +54,6 @@ psql:
|
|||
|
||||
ch:
|
||||
> $(COMPOSE) exec clickhouse sh -lc 'clickhouse-client --user "$$CLICKHOUSE_USER" --password "$$CLICKHOUSE_PASSWORD" --database analytics'
|
||||
|
||||
backup:
|
||||
> ./scripts/clickhouse_backup.sh $(E)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ pytest
|
|||
|
||||
## Версионирование
|
||||
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.3`.
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.4`.
|
||||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "0.3.3"
|
||||
__version__ = "0.3.4"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# Параметры ежедневного бэкапа событий ClickHouse с VPS на домашний сервер.
|
||||
# Скопируйте в env/backup.env (env/*.env в git не попадает) и заполните.
|
||||
#
|
||||
# Используется скриптами:
|
||||
# scripts/clickhouse_backup.sh (запускается на VPS по cron, см. DEPLOY.md)
|
||||
# scripts/clickhouse_import.sh (лежит на домашнем сервере)
|
||||
|
||||
# Куда передавать архивы: пользователь@хост и каталог на домашнем сервере.
|
||||
BACKUP_REMOTE=user@home.example
|
||||
BACKUP_DIR=/srv/dispelevilbot/events
|
||||
|
||||
# Приватный ключ для ssh/rsync (без пароля; на домашнем сервере — соответствующий public key).
|
||||
BACKUP_SSH_KEY=/root/.ssh/dispelevilbot_backup
|
||||
|
||||
# Сколько дней сырых событий оставлять на VPS (остальное удаляется после успешной сверки).
|
||||
BACKUP_GRACE_DAYS=7
|
||||
|
||||
# Таймзона расчёта «вчерашнего» дня.
|
||||
BACKUP_TZ=Europe/Moscow
|
||||
|
||||
# Как запускать импорт на домашнем сервере (путь к scripts/clickhouse_import.sh).
|
||||
REMOTE_IMPORT_SCRIPT=./clickhouse_import.sh
|
||||
|
||||
# ClickHouse домашнего сервера. Пароль лучше без кавычек и пробелов — он передаётся по ssh.
|
||||
REMOTE_CLICKHOUSE_HOST=localhost
|
||||
REMOTE_CLICKHOUSE_PORT=9000
|
||||
REMOTE_CLICKHOUSE_USER=default
|
||||
REMOTE_CLICKHOUSE_PASSWORD=
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env bash
|
||||
# Ежедневный бэкап сырых событий ClickHouse с VPS на домашний сервер.
|
||||
#
|
||||
# Что делает:
|
||||
# 1) выгружает партицию «вчерашнего» дня из analytics.events в Parquet.gz;
|
||||
# 2) передаёт файл на домашний сервер (rsync) и импортирует его в тамошний ClickHouse;
|
||||
# 3) сверяет количество строк на VPS и на домашнем сервере;
|
||||
# 4) только после успешной сверки удаляет на VPS партиции старше BACKUP_GRACE_DAYS.
|
||||
#
|
||||
# Ошибка на любом шаге оставляет данные на VPS и возвращает ненулевой код.
|
||||
#
|
||||
# Запуск (на VPS, из каталога проекта):
|
||||
# ./scripts/clickhouse_backup.sh test # или prod
|
||||
# make backup E=test
|
||||
#
|
||||
# Настройки (env/<контур>.env + env/backup.env, см. env/backup.env.example):
|
||||
# CLICKHOUSE_* доступ к ClickHouse контура
|
||||
# BACKUP_REMOTE user@host домашнего сервера
|
||||
# BACKUP_DIR каталог архива на домашнем сервере
|
||||
# BACKUP_SSH_KEY приватный ключ для ssh/rsync
|
||||
# BACKUP_GRACE_DAYS сколько дней сырых событий оставлять на VPS (по умолчанию 7)
|
||||
# BACKUP_TZ таймзона расчёта «вчера» (по умолчанию Europe/Moscow)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
E="${1:-test}"
|
||||
ENV_FILE="env/${E}.env"
|
||||
BACKUP_ENV="env/backup.env"
|
||||
COMPOSE="docker compose -p ${E} --env-file ${ENV_FILE} -f docker-compose.prod.yml"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||
fail() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ОШИБКА: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -f "$ENV_FILE" ] || fail "нет файла окружения ${ENV_FILE}"
|
||||
# shellcheck disable=SC1090
|
||||
set -a && . "./${ENV_FILE}" && set +a
|
||||
if [ -f "$BACKUP_ENV" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a && . "./${BACKUP_ENV}" && set +a
|
||||
fi
|
||||
|
||||
: "${BACKUP_REMOTE:?задайте BACKUP_REMOTE в env/backup.env}"
|
||||
: "${BACKUP_DIR:?задайте BACKUP_DIR в env/backup.env}"
|
||||
BACKUP_SSH_KEY="${BACKUP_SSH_KEY:-$HOME/.ssh/id_rsa}"
|
||||
BACKUP_GRACE_DAYS="${BACKUP_GRACE_DAYS:-7}"
|
||||
BACKUP_TZ="${BACKUP_TZ:-Europe/Moscow}"
|
||||
REMOTE_IMPORT_SCRIPT="${REMOTE_IMPORT_SCRIPT:-./clickhouse_import.sh}"
|
||||
REMOTE_CLICKHOUSE_HOST="${REMOTE_CLICKHOUSE_HOST:-localhost}"
|
||||
REMOTE_CLICKHOUSE_PORT="${REMOTE_CLICKHOUSE_PORT:-9000}"
|
||||
REMOTE_CLICKHOUSE_USER="${REMOTE_CLICKHOUSE_USER:-default}"
|
||||
REMOTE_CLICKHOUSE_PASSWORD="${REMOTE_CLICKHOUSE_PASSWORD:-}"
|
||||
SSH=(ssh -i "$BACKUP_SSH_KEY" -o BatchMode=yes "$BACKUP_REMOTE")
|
||||
|
||||
ch() {
|
||||
$COMPOSE exec -T clickhouse clickhouse-client \
|
||||
--user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" "$@"
|
||||
}
|
||||
|
||||
DAY="$(TZ="$BACKUP_TZ" date -d 'yesterday' '+%Y-%m-%d')"
|
||||
PARTITION="${DAY//-/}"
|
||||
GRACE_CUTOFF="$(TZ="$BACKUP_TZ" date -d "${BACKUP_GRACE_DAYS} days ago" '+%Y%m%d')"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
log "Контур ${E}: бэкап событий за ${DAY} (грейс ${BACKUP_GRACE_DAYS} дней, порог ${GRACE_CUTOFF})"
|
||||
|
||||
if ! $COMPOSE ps --services --status running 2>/dev/null | grep -qx clickhouse; then
|
||||
fail "контейнер clickhouse контура ${E} не запущен"
|
||||
fi
|
||||
|
||||
ROWS_LOCAL="$(ch --query "SELECT count() FROM analytics.events WHERE toDate(event_time) = toDate('${DAY}')" | tr -d '[:space:]')"
|
||||
log "На VPS строк за ${DAY}: ${ROWS_LOCAL}"
|
||||
if [ "$ROWS_LOCAL" = "0" ]; then
|
||||
log "ВНИМАНИЕ: за ${DAY} событий нет — партиция пустая, продолжаю (сверка 0 = 0)"
|
||||
fi
|
||||
|
||||
ARCHIVE="${TMP_DIR}/events_${DAY}.parquet.gz"
|
||||
log "Выгружаю партицию в ${ARCHIVE}"
|
||||
ch --query "SELECT * FROM analytics.events WHERE toDate(event_time) = toDate('${DAY}') FORMAT Parquet" | gzip -9 >"$ARCHIVE"
|
||||
[ -s "$ARCHIVE" ] || fail "не удалось выгрузить архив ${DAY}"
|
||||
log "Размер архива: $(du -h "$ARCHIVE" | cut -f1)"
|
||||
|
||||
log "Передаю архив на ${BACKUP_REMOTE}:${BACKUP_DIR}"
|
||||
rsync -az -e "ssh -i ${BACKUP_SSH_KEY} -o BatchMode=yes" "$ARCHIVE" "${BACKUP_REMOTE}:${BACKUP_DIR}/"
|
||||
|
||||
log "Импортирую архив в ClickHouse домашнего сервера"
|
||||
ROWS_REMOTE="$("${SSH[@]}" \
|
||||
"BACKUP_DIR='${BACKUP_DIR}' CLICKHOUSE_HOST='${REMOTE_CLICKHOUSE_HOST}' CLICKHOUSE_PORT='${REMOTE_CLICKHOUSE_PORT}' \
|
||||
CLICKHOUSE_USER='${REMOTE_CLICKHOUSE_USER}' CLICKHOUSE_PASSWORD='${REMOTE_CLICKHOUSE_PASSWORD}' \
|
||||
'${REMOTE_IMPORT_SCRIPT}' '${DAY}'" | tail -n1 | tr -d '[:space:]')"
|
||||
log "На домашнем сервере строк за ${DAY}: ${ROWS_REMOTE}"
|
||||
|
||||
[ "$ROWS_LOCAL" = "$ROWS_REMOTE" ] || fail "расхождение строк (VPS ${ROWS_LOCAL}, домашний ${ROWS_REMOTE}) — партиции на VPS не удаляю"
|
||||
|
||||
log "Удаляю на VPS партиции старше ${GRACE_CUTOFF}"
|
||||
OLDER="$(ch --query "SELECT DISTINCT partition FROM system.parts WHERE database='analytics' AND table='events' AND active AND partition < '${GRACE_CUTOFF}'")"
|
||||
if [ -z "$OLDER" ]; then
|
||||
log "Удалять нечего"
|
||||
else
|
||||
for partition in $OLDER; do
|
||||
log "DROP PARTITION ID '${partition}'"
|
||||
ch --query "ALTER TABLE analytics.events DROP PARTITION ID '${partition}'"
|
||||
done
|
||||
fi
|
||||
|
||||
log "Готово: события за ${DAY} в архиве, на VPS остались агрегаты и последние ${BACKUP_GRACE_DAYS} дней сырых событий"
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# Импорт дневного архива событий в ClickHouse домашнего сервера.
|
||||
#
|
||||
# Запускается на домашнем сервере (обычно по ssh из scripts/clickhouse_backup.sh).
|
||||
# Рядом должны лежать этот скрипт и канонический DDL кликхаус-схемы
|
||||
# (clickhouse/init/001_events.sql из репозитория).
|
||||
#
|
||||
# Использование: ./clickhouse_import.sh 2026-01-01
|
||||
# В stdout печатается количество строк загруженного дня (для сверки), диагностика — в stderr.
|
||||
#
|
||||
# Настройки (окружение):
|
||||
# BACKUP_DIR каталог, куда складываются архивы events_YYYY-MM-DD.parquet.gz
|
||||
# SCHEMA_FILE путь к DDL схемы (по умолчанию ./001_events.sql рядом со скриптом)
|
||||
# CLICKHOUSE_CLIENT бинарь клиента (по умолчанию clickhouse-client)
|
||||
# CLICKHOUSE_HOST/PORT/USER/PASSWORD доступ к ClickHouse домашнего сервера
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DAY="${1:?укажите дату в формате YYYY-MM-DD}"
|
||||
PARTITION="${DAY//-/}"
|
||||
ARCHIVE_DIR="${BACKUP_DIR:-.}"
|
||||
ARCHIVE="${ARCHIVE_DIR}/events_${DAY}.parquet.gz"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCHEMA_FILE="${SCHEMA_FILE:-${SCRIPT_DIR}/001_events.sql}"
|
||||
|
||||
CLIENT="${CLICKHOUSE_CLIENT:-clickhouse-client}"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2; }
|
||||
|
||||
ch() {
|
||||
"$CLIENT" \
|
||||
--host "${CLICKHOUSE_HOST:-localhost}" \
|
||||
--port "${CLICKHOUSE_PORT:-9000}" \
|
||||
--user "${CLICKHOUSE_USER:-default}" \
|
||||
--password "${CLICKHOUSE_PASSWORD:-}" "$@"
|
||||
}
|
||||
|
||||
[ -f "$ARCHIVE" ] || {
|
||||
log "нет файла архива ${ARCHIVE}"
|
||||
exit 1
|
||||
}
|
||||
[ -f "$SCHEMA_FILE" ] || {
|
||||
log "нет файла схемы ${SCHEMA_FILE} (скопируйте clickhouse/init/001_events.sql на домашний сервер)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log "Применяю схему из ${SCHEMA_FILE}"
|
||||
ch --multiquery <"$SCHEMA_FILE"
|
||||
|
||||
# Повторный импорт того же дня не должен дублировать строки.
|
||||
log "Очищаю партицию ${PARTITION} перед импортом"
|
||||
ch --query "ALTER TABLE analytics.events DROP PARTITION ID '${PARTITION}'" 2>/dev/null || true
|
||||
|
||||
log "Импортирую ${ARCHIVE} в analytics.events"
|
||||
gzip -dc "$ARCHIVE" | ch --query "INSERT INTO analytics.events FORMAT Parquet"
|
||||
|
||||
log "Импорт завершён"
|
||||
ch --query "SELECT count() FROM analytics.events WHERE toDate(event_time) = toDate('${DAY}')"
|
||||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
|||
|
||||
def test_version_is_set():
|
||||
assert __version__
|
||||
assert __version__ == "0.3.3"
|
||||
assert __version__ == "0.3.4"
|
||||
|
|
|
|||
Loading…
Reference in New Issue