Про restic я уже рассказывал в статье Бэкап-хранилище для тысяч виртуальных машин свободными инструментами, с тех пор он остаётся моим любимым инструментом для бэкапа.

Сегодня я опишу вам готовый рецепт того как настроить эффективное резервное копирование из Stdin, с дедупликацией и автоматической очисткой репозитория от старых копий.

Несмотря на то, что restic отлично подходит для сохранения целых каталогов с данными в этой статье мне хотелось бы сделать упор на сохранении резервных копий на лету, прямо из Stdin.

Как правило это бывает актуально для сохранения бэкапов виртуальных машин, баз данных и других, представленных одним большим файлом, данных, которые можно последовательно вычитывать и сразу отправлять в систему бэкапирования.

Итак вот вам несколько правил, которых я сам стараюсь придерживаться:

1. Не используйте монорепозиторий для бэкапов.

Всегда разносите сущности по разным репозиториям, к примеру создавайте репозиторий на каждую базу данных или на каждую виртуальную машину.

Делать это стоит по нескольким причинам. Бэкапить всё в один репо не даст вам никаких преимуществ, а вот неудобства создаст, так как:

  • Дедупликация не даёт особого прироста, так как данные между двумя разными базами / виртуалками всегда разные.

  • Блокировки работают на уровне репозитория и вы не можете выполнять конкурентные операции с каждым репозиторием.

  • В случае порчи метаданных вы теряете все данные в репозитории, а не отдельные его копии.

  • На каждый репозиторий вы можете установить отдельный пароль

2. Используйте встроенную компрессию restic

При использовании gzip всегда имеете шанс получить неповторяемый набор данных, даже при малейшем изменении источника. Таким образом дедупликация restic не сработает. Тем не менее бэкапить без компрессии идея тоже плохая, потому что размер несжатого бэкапа может различаться в десятки и сотни раз от сжатого.

Одной из опций до недавнего времени могло бы стать использование gzip с флагов --rsyncable, например:

mysqldump | gzip --rsyncable | restic

Но начиная с версии 0.14.0 restic и сам умеет прекрасно сжимать данные, и более того, включает его по умолчанию для всех новых репозиториев, всё что вам нужно — это создать репозиторий с опцией:

restic init --repository-version 2

и тогда при бэкапе в этот репозиторий restic будет использовать сжатие по умолчанию.

3. Всегда проверяйте что бэкап создался полностью.

При использовании пайпа это не всегда очевидно, но определённо стоит разъяснения:

Если вы бэкапите из Stdin:

<your_command> | restic backup --stdin

То ваша команда всегда может внезапно завершиться с ошибкой и restic об этом ничего не узнает, так и сохранит частично созданный бэкап в репозиторий.

В качестве одного из вариантов решения можно использовать тэги:

set -e
set -o pipefail
JOB_ID="job-$(uuidgen|cut -f1 -d-)"
<your_command> | restic backup --tag "$JOB_ID" --stdin
restic tag --tag "$JOB_ID" --set "completed"
restic forget --keep-tag "completed"
  • set -o pipefail - вернёт ненулевой exit-code для вашей команды, даже если она была запущена в пайпе, но этого мало.

  • set -e - остановит выполнение скрипта при возникновении любой ошибки. В качестве альтернативы вы можете самостоятельно проверять exit-code для вашего пайпa.

  • Переменная JOB_ID устанавливается для того чтобы пометить создаваемый снапшот временным тэгом.

  • После того как бэкап успешно сделан, вы меняете тег снапшота на completed и удаляете все снапшоты которые его не содержат

Таким образом вы обезопасите себя от частично сделанных бэкапов и всегда будете знать о проблемах.

4. Не восстанавливайте напрямую в Stdout

Другими словами не делайте так:

restic dump latest dump.sql | mysql

Из-за особенностей имплементации восстановление напрямую в Stdout у Restic крайне неэффективно, ввиду того что осуществляется однопоточно и очень медленно.

Вместо этого восстанавливайте бэкап в промежуточную директорию:

restic restore latest --target /tmp/
mysql < /tmp/dump.sql

и ваша операция будет выполнена намного быстрее.


Ну и как обещал, вот вам готовый скрипт, который выполняет бэкап для набора баз данных последовательно вычитывая их через Stdin:

#!/bin/sh
set -e
set -o pipefail

# Генерируем временный тэг для нашей джобы
JOB_ID="job-$(uuidgen|cut -f1 -d-)"

# Получаем список баз данных для бэкапа и перемешиваем его
DB_LIST=$(psql -Atq -c 'SELECT datname FROM pg_catalog.pg_database;' | grep -v '^\(postgres\|app\|template.*\)$')
DB_LIST=$(echo "$DB_LIST" | shuf)

echo "Job ID: $JOB_ID"
echo "Target repo: $REPO_PREFIX"
echo "Cleanup strategy: $CLEANUP_STRATEGY"
echo "Start backup for:"
echo "$DB_LIST"
echo
echo "Backup started at `date +%Y-%m-%d\ %H:%M:%S`"
for db in $DB_LIST; do
  (
    set -x
    # Создаём репозиторий, если ещё не создан
    restic -r "s3:${REPO_PREFIX}/$db" cat config >/dev/null 2>&1 || \
      restic -r "s3:${REPO_PREFIX}/$db" init --repository-version 2
    # В моём случае Kubernetes гарантирует эксклюзивность выполнения джоб, поэтому я без зазрения совести снимаю любые возможные локи
    restic -r "s3:${REPO_PREFIX}/$db" unlock --remove-all >/dev/null 2>&1 || true
    # Создаём бэкап (не забываем указать имя файла и его расширение, чтобы было предельно ясно какой тип данных находится в бэкапе)
    pg_dump -Z0 -Ft -d "$db" | \
      restic -r "s3:${REPO_PREFIX}/$db" backup --tag "$JOB_ID" --stdin --stdin-filename dump.tar
    # Помечаем бэкап как completed
    restic -r "s3:${REPO_PREFIX}/$db" tag --tag "$JOB_ID" --set "completed"
  )
done
echo "Backup finished at `date +%Y-%m-%d\ %H:%M:%S`"

echo
echo "Run cleanup:"
echo

echo "Cleanup started at `date +%Y-%m-%d\ %H:%M:%S`"
for DB in $DB_LIST; do
  (
    set -x
    # Удаляем все снапшоты без тэга completed
    restic forget -r "s3:${REPO_PREFIX}/$db" --keep-tag "completed"
    # Удаляем снапшоты которые не соответсвуют нашей стратегии очистки
    restic forget -r "s3:${REPO_PREFIX}/$db" $CLEANUP_STRATEGY
    # Запускаем операцию для непосредственно удаления данных из репозитория
    restic prune -r "s3:${REPO_PREFIX}/$db"
  )
done
echo "Cleanup finished at `date +%Y-%m-%d\ %H:%M:%S`"

Этот скрипт запущен в Kuberentes как CronJob с concurrencyPolicy: Forbid, для него я передаю следующие переменные окружения:

# Набор ключей для очистики репозитория
- name: CLEANUP_STRATEGY
  value: "--keep-last=3 --keep-daily=3 --keep-weekly=1 --keep-monthly=1"
# Адрес к бакету с репозиториями
- name: REPO_PREFIX
  value: "s3.myprovider.com/backups/postgres-backups"
# Пароль для репозитория
- name: RESTIC_PASSWORD
  valueFrom:
    secretKeyRef:
      name: mydatabase-backup
      key: resticPassword
# Креды для доступа в S3
- name: AWS_ACCESS_KEY_ID
  valueFrom:
    secretKeyRef:
      name: mydatabase-backup
      key: s3AccessKey
- name: AWS_SECRET_ACCESS_KEY
  valueFrom:
    secretKeyRef:
      name: mydatabase-backup
      key: s3SecretKey
- name: AWS_DEFAULT_REGION
  value: us-east-1
# Данные для доступа в БД
- name: PGUSER
  valueFrom:
    secretKeyRef:
      name: mydatabase-superuser
      key: username
- name: PGPASSWORD
  valueFrom:
    secretKeyRef:
      name: mydatabase-superuser
      key: password
- name: PGHOST
  value: mydatabase-service
- name: PGPORT
  value: "5432"
- name: PGDATABASE
  value: postgres

Dockerfile

FROM alpine:3.18
RUN apk add --no-cache postgresql15-client uuidgen
RUN wget https://github.com/restic/restic/releases/download/v0.16.0/restic_0.16.0_linux_amd64.bz2 -O- | bzcat > /usr/bin/restic \
 && chmod +x /usr/bin/restic

Важное замечание: При адаптации моего скрипта к другим платформам, уделите особое внимание блокировкам. Если ваша платформа не гарантирует эксклюзивное выполнение, не используйте restic unlock --remove-all

Комментарии (2)


  1. Ryav
    26.10.2023 04:36

    Расскажите, а зачем вообще делать бэкапы из Stdin?


    1. Pinkbyte
      26.10.2023 04:36

      Например если делаешь бэкап с другого сервера по SSH (если нет возможности развернуть restic на целевом сервере). Да, безусловно можно сначала выкачать бэкап и положить в локальный файл, а уже потом на него натравить restic - но это дополнительное время.

      А вот про то, что restic не оптимально восстанавливает на stdout - это полезное замечание, я не знал. Я правда restic вообще не использовал с stdin/stdout - у меня нет проблем положить один бинарник на все сервера, где нужно делать бэкап, но замечание интересное...