DispelEvilBot/scripts/clickhouse_backup.sh

130 lines
6.6 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Бэкап сырых событий ClickHouse: домашний сервер забирает данные с VPS (pull).
#
# Запускается НА ДОМАШНЕМ СЕРВЕРЕ по cron. Входящий SSH на домашний сервер не нужен:
# скрипт сам ходит на VPS по ключу и запускает там docker compose.
#
# Что делает:
# 1) по ssh выгружает на VPS вчерашнюю партицию analytics.events в Parquet;
# 2) складывает events_YYYY-MM-DD.parquet.gz в BACKUP_DIR;
# 3) импортирует архив в локальный ClickHouse (идемпотентно, scripts/clickhouse_import.sh);
# 4) сверяет количество строк на VPS и локально;
# 5) только после успешной сверки просит VPS удалить партиции старше BACKUP_GRACE_DAYS.
#
# Ошибка на любом шаге оставляет данные на VPS (партиции не удаляются) и возвращает ненулевой код.
#
# Использование:
# ./scripts/clickhouse_backup.sh test # или prod
# make backup E=test
#
# Настройки — env/backup.env (см. env/backup.env.example).
set -euo pipefail
E="${1:-test}"
BACKUP_ENV="${BACKUP_ENV:-env/backup.env}"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ОШИБКА: $*" >&2
exit 1
}
[ -f "$BACKUP_ENV" ] || fail "нет файла настроек ${BACKUP_ENV} (скопируйте env/backup.env.example)"
# shellcheck disable=SC1090
set -a && . "./${BACKUP_ENV}" && set +a
: "${VPS_SSH:?задайте VPS_SSH в env/backup.env}"
: "${BACKUP_DIR:?задайте BACKUP_DIR в env/backup.env}"
VPS_PORT="${VPS_PORT:-22}"
VPS_PROJECT_DIR="${VPS_PROJECT_DIR:?задайте VPS_PROJECT_DIR в env/backup.env}"
VPS_SSH_KEY="${VPS_SSH_KEY:-$HOME/.ssh/id_ed25519_vps}"
VPS_COMPOSE_FILE="${VPS_COMPOSE_FILE:-docker-compose.prod.yml}"
BACKUP_GRACE_DAYS="${BACKUP_GRACE_DAYS:-7}"
BACKUP_TZ="${BACKUP_TZ:-Europe/Moscow}"
IMPORT_SCRIPT="${IMPORT_SCRIPT:-$(dirname "$0")/clickhouse_import.sh}"
SCHEMA_FILE="${SCHEMA_FILE:-$(dirname "$0")/../clickhouse/init/001_events.sql}"
SSH=(ssh -i "$VPS_SSH_KEY" -p "$VPS_PORT" -o BatchMode=yes -o ServerAliveInterval=30 "$VPS_SSH")
# Запустить команду на VPS. Пароль ClickHouse читается из окружения контейнера,
# поэтому клиент вызывается через `sh -c` уже внутри контейнера.
# ssh склеивает аргументы в одну строку и отдаёт её удалённому shell, поэтому каждый
# аргумент экранируем: в SQL есть пробелы, скобки и кавычки.
vps_ch() {
local quoted=() arg
for arg in "$@"; do
quoted+=("$(printf '%q' "$arg")")
done
"${SSH[@]}" bash -s "$VPS_PROJECT_DIR" "$E" "$VPS_COMPOSE_FILE" "${quoted[@]}" <<'REMOTE'
set -euo pipefail
project_dir="$1"; contour="$2"; compose_file="$3"; shift 3
cd "$project_dir"
exec docker compose -p "$contour" --env-file "env/$contour.env" -f "$compose_file" exec -T clickhouse \
sh -c 'clickhouse-client --user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" "$@"' sh "$@"
REMOTE
}
# Какие сервисы контура запущены на VPS (пусто, если проекта/контура нет).
vps_running_services() {
"${SSH[@]}" bash -s "$VPS_PROJECT_DIR" "$E" "$VPS_COMPOSE_FILE" <<'REMOTE'
set -uo pipefail
if [ ! -d "$1" ]; then
echo "каталог проекта $1 на VPS не найден" >&2
exit 0
fi
cd "$1" || exit 0
docker compose -p "$2" --env-file "env/$2.env" -f "$3" ps --services --status running 2>/dev/null || true
REMOTE
}
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')"
log "Бэкап событий контура ${E} за ${DAY} (грейс ${BACKUP_GRACE_DAYS} дней, порог ${GRACE_CUTOFF})"
# Бот может быть не запущен (или ещё не развёрнут) — это не ошибка, просто пропускаем прогон.
if ! RUNNING="$(vps_running_services)"; then
fail "не удалось получить состояние контура ${E} на VPS (проверьте ssh и VPS_SSH)"
fi
if ! grep -qx 'clickhouse' <<<"$RUNNING"; then
log "Контур ${E} на VPS не запущен — бэкап пропущен, на VPS ничего не меняем"
exit 0
fi
mkdir -p "$BACKUP_DIR"
ARCHIVE="${BACKUP_DIR}/events_${DAY}.parquet.gz"
ROWS_VPS="$(vps_ch --param_day="$DAY" --query "SELECT count() FROM analytics.events WHERE toDate(event_time) = toDate({day:String})" | tr -d '[:space:]')"
log "На VPS строк за ${DAY}: ${ROWS_VPS}"
if [ "$ROWS_VPS" = "0" ]; then
log "ВНИМАНИЕ: за ${DAY} событий нет — партиция пустая, продолжаю (сверка 0 = 0)"
fi
log "Выгружаю партицию в ${ARCHIVE}"
vps_ch --param_day="$DAY" --query "SELECT * FROM analytics.events WHERE toDate(event_time) = toDate({day:String}) FORMAT Parquet" |
gzip -9 >"${ARCHIVE}.part"
[ -s "${ARCHIVE}.part" ] || fail "не удалось выгрузить архив ${DAY}"
mv "${ARCHIVE}.part" "$ARCHIVE"
log "Размер архива: $(du -h "$ARCHIVE" | cut -f1)"
log "Импортирую архив в ClickHouse домашнего сервера"
ROWS_HOME="$(BACKUP_DIR="$BACKUP_DIR" SCHEMA_FILE="$SCHEMA_FILE" "$IMPORT_SCRIPT" "$DAY" | tail -n1 | tr -d '[:space:]')"
log "На домашнем сервере строк за ${DAY}: ${ROWS_HOME}"
[ "$ROWS_VPS" = "$ROWS_HOME" ] || fail "расхождение строк (VPS ${ROWS_VPS}, домашний ${ROWS_HOME}) — партиции на VPS не удаляю"
log "Удаляю на VPS партиции старше ${GRACE_CUTOFF}"
DROPPED="$(vps_ch --query "SELECT DISTINCT partition FROM system.parts WHERE database='analytics' AND table='events' AND active AND partition < '${GRACE_CUTOFF}'")"
if [ -z "$DROPPED" ]; then
log "Удалять нечего"
else
for partition in $DROPPED; do
log "DROP PARTITION ID '${partition}'"
vps_ch --query "ALTER TABLE analytics.events DROP PARTITION ID '${partition}'"
done
fi
log "Готово: события за ${DAY} в архиве ${ARCHIVE}, на VPS остались агрегаты и последние ${BACKUP_GRACE_DAYS} дней сырых событий"