110 lines
5.5 KiB
Bash
110 lines
5.5 KiB
Bash
#!/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} дней сырых событий"
|