DispelEvilBot/scripts/clickhouse_backup.sh

110 lines
5.5 KiB
Bash
Raw 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 на домашний сервер.
#
# Что делает:
# 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} дней сырых событий"