WAL-G: бекапи та відновлення СУБД PostgreSQL

Вже давно відомо, що робити бекапи в SQL-дампи (використовуючи pg_dump або pg_dumpall) – не найкраща ідея. Для резервного копіювання СУБД PostgreSQL краще використовувати команду pg_basebackupяка робить бінарну копію WAL-журналів. Але коли ви почнете вивчати весь процес створення копії та відновлення, то зрозумієте, що потрібно написати як мінімум пару триколісних велосипедів, щоб все це працювало і не викликало у вас біль як зверху, так і знизу. Щоб полегшити страждання було розроблено WAL-G.

WAL-G – це інструмент, написаний на Go для резервного копіювання та відновлення PostgreSQL баз даних (а з недавнього часу і MySQL/MariaDB, MongoDB та FoundationDB). Він підтримує роботу зі сховищами Amazon S3 (і аналогами, наприклад Yandex Object Storage), а також Google Cloud Storage, Azure Storage, Swift Object Storage і просто з файловою системою. Все налаштування зводиться до простих кроків, але через те, що статті про нього розрізнені по інтернету - немає повного how-to мануалу, який включав би всі кроки від і до (на Хабре є кілька постів, але багато моментів там втрачені).

WAL-G: бекапи та відновлення СУБД PostgreSQL

Ця стаття написана в першу чергу, щоб систематизувати свої знання. Я не є DBA і десь можу висловлюватись обивательсько-розробницькою мовою, тому вітаються будь-які виправлення!

Окремо відзначу, що все наведене нижче актуально і перевірено для PostgreSQL 12.3 на Ubuntu 18.04, всі команди повинні виконуватися від привілейованого користувача.

Встановлення

На момент написання цієї статті стабільна версія WAL-G – v0.2.15 (березень 2020). Її ми і будемо використовувати (але якщо ви захочете самостійно зібрати його з master-гілки, то в репозиторії на github є всі інструкції для цього). Для скачування та встановлення потрібно виконати:

#!/bin/bash

curl -L "https://github.com/wal-g/wal-g/releases/download/v0.2.15/wal-g.linux-amd64.tar.gz" -o "wal-g.linux-amd64.tar.gz"
tar -xzf wal-g.linux-amd64.tar.gz
mv wal-g /usr/local/bin/

Після цього потрібно налаштувати спочатку WAL-G, а потім сам PostgreSQL.

Налаштування WAL-G

Для зберігання бекапів буде використовуватися Amazon S3 (тому що він ближчий до моїх серверів і його використання обходиться дуже дешево). Для роботи з ним потрібен "s3-бакет" та ключі доступу.

У всіх попередніх статтях про WAL-G використовувалося конфігурування за допомогою змінних оточення, але з цього релізу налаштування можна розташувати в .walg.json файлі у домашній директорії користувача postgres. Для його створення виконаємо наступний bash-скрипт:

#!/bin/bash

cat > /var/lib/postgresql/.walg.json << EOF
{
    "WALG_S3_PREFIX": "s3://your_bucket/path",
    "AWS_ACCESS_KEY_ID": "key_id",
    "AWS_SECRET_ACCESS_KEY": "secret_key",
    "WALG_COMPRESSION_METHOD": "brotli",
    "WALG_DELTA_MAX_STEPS": "5",
    "PGDATA": "/var/lib/postgresql/12/main",
    "PGHOST": "/var/run/postgresql/.s.PGSQL.5432"
}
EOF
# обязательно меняем владельца файла:
chown postgres: /var/lib/postgresql/.walg.json

Трохи поясню за всіма параметрами:

  • WALG_S3_PREFIX - Шлях до вашого S3-бакету куди будуть заливатись бекапи (можна як в корінь, так і в папку);
  • AWS_ACCESS_KEY_ID – ключ доступу до S3 (у разі відновлення на тестовому сервері – ці ключі повинні мати ReadOnly Policy! Про це докладніше написано у розділі про відновлення);
  • AWS_SECRET_ACCESS_KEY – секретний ключ у сховищі S3;
  • WALG_COMPRESSION_METHOD – метод компресії, краще використовувати Brotli (оскільки це золота середина між підсумковим розміром та швидкістю стиснення/розтискання);
  • WALG_DELTA_MAX_STEPS – кількість «дельт» до створення повного бекапу (дозволяють економити час і розмір даних, що завантажуються, але можуть трохи сповільнити процес відновлення, тому не бажано використовувати великі значення);
  • PGDATA – шлях до директорії з даними вашої бази (можна дізнатися, виконавши команду pg_lsclusters);
  • PGHOST - Підключення до бази, при локальному бекапі краще робити через unix-socket як у цьому прикладі.

Інші параметри можна переглянути в документації: https://github.com/wal-g/wal-g/blob/v0.2.15/PostgreSQL.md#configuration.

Налаштування PostgreSQL

Щоб архіватор усередині бази сам заливав WAL-журнали у хмару та відновлювався з них (у разі потреби) – потрібно задати кілька параметрів у конфігураційному файлі /etc/postgresql/12/main/postgresql.conf. Тільки для початку вам потрібно переконатися, що жодні з наведених нижче налаштувань не задані в якісь інші значення, щоб при перезавантаженні конфігурації – СУБД не впала. Додати ці параметри можна за допомогою:

#!/bin/bash

echo "wal_level=replica" >> /etc/postgresql/12/main/postgresql.conf
echo "archive_mode=on" >> /etc/postgresql/12/main/postgresql.conf
echo "archive_command='/usr/local/bin/wal-g wal-push "%p" >> /var/log/postgresql/archive_command.log 2>&1' " >> /etc/postgresql/12/main/postgresql.conf
echo “archive_timeout=60” >> /etc/postgresql/12/main/postgresql.conf
echo "restore_command='/usr/local/bin/wal-g wal-fetch "%f" "%p" >> /var/log/postgresql/restore_command.log 2>&1' " >> /etc/postgresql/12/main/postgresql.conf

# перезагружаем конфиг через отправку SIGHUP сигнала всем процессам БД
killall -s HUP postgres

Опис параметрів, що встановлюються:

  • wal_level – скільки інформації писати у WAL журнали, «replica» – писати все;
  • archive_mode – увімкнення завантаження WAL-журналів за допомогою команди з параметра archive_command;
  • archive_command – команда для архівації завершеного WAL-журналу;
  • archive_timeout - архівування журналів проводиться тільки коли він завершений, але якщо ваш сервер мало змінює/додає даних в БД, то є сенс виставити тут ліміт в секундах, після якого команда архівації буде викликана примусово (у мене інтенсивний запис в базу кожну секунду, тому я відмовився від встановлення цього параметра у продакшені);
  • Команда відновлення - команда відновлення WAL-журналу з бекапу, використовуватиметься у разі якщо в «повному бекапі» (base backup) бракуватиме останніх змін у БД.

Докладніше про всі ці параметри можна прочитати у перекладі офіційної документації: https://postgrespro.ru/docs/postgresql/12/runtime-config-wal.

Налаштування розкладу резервного копіювання

Як не крути, але найзручнішим способом для запуску є cron. Саме його ми налаштуємо для створення резервних копій. Почнемо з команди створення повного бекапу: у wal-g це аргумент запуску backup-push. Але для початку краще виконати цю команду вручну від користувача postgres, щоб переконатися, що все добре (і немає якихось помилок доступу):

#!/bin/bash

su - postgres -c '/usr/local/bin/wal-g backup-push /var/lib/postgresql/12/main'

У аргументах запуску вказано шлях до директорії з даними – нагадую, що його можна дізнатися, виконавши pg_lsclusters.

Якщо все пройшло без помилок і дані завантажилися в сховище S3, то можна налаштувати періодичний запуск в crontab:

#!/bin/bash

echo "15 4 * * *    /usr/local/bin/wal-g backup-push /var/lib/postgresql/12/main >> /var/log/postgresql/walg_backup.log 2>&1" >> /var/spool/cron/crontabs/postgres
# задаем владельца и выставляем правильные права файлу
chown postgres: /var/spool/cron/crontabs/postgres
chmod 600 /var/spool/cron/crontabs/postgres

У цьому прикладі процес бекапу запускається щодня о 4:15 ранку.

Видалення старих резервних копій

Швидше за все вам не потрібно зберігати всі бекапи з мезозойської ери, тому буде корисно періодично «підчищати» ваше сховище (як «повні бекапи», так і WAL-журнали). Ми зробимо це все також через завдання cron:

#!/bin/bash

echo "30 6 * * *    /usr/local/bin/wal-g delete before FIND_FULL $(date -d '-10 days' '+%FT%TZ') --confirm >> /var/log/postgresql/walg_delete.log 2>&1" >> /var/spool/cron/crontabs/postgres
# ещё раз задаем владельца и выставляем правильные права файлу (хоть это обычно это и не нужно повторно делать)
chown postgres: /var/spool/cron/crontabs/postgres
chmod 600 /var/spool/cron/crontabs/postgres

Cron буде виконувати це завдання щодня о 6:30 ранку, видаляючи все (повні бекапи, дельти та WAL'и) крім копій за останні 10 днів, але залишить щонайменше один бекап до зазначеної дати, щоб будь-яка точка після дати потрапляла до PITR.

Відновлення із резервної копії

Ні для кого не секрет що запорука здорової бази – у періодичному відновленні та перевірці цілісності даних усередині. Як відновитись за допомогою WAL-G – розповім у цьому розділі, а про перевірки поговоримо після.

Окремо варто відзначити що для відновлення на тестовому оточенні (все те, що не production) – потрібно використовувати Read Only обліковий запис у S3, щоб випадково не перезаписати бекапи. У випадку WAL-G потрібно задати користувачеві S3 такі права в Group Policy (Effect: Allow): s3:GetObject, s3: ListBucket, s3:GetBucketLocation. І, звичайно, попередньо не забути виставити archive_mode=off у файлі налаштувань postgresql.confщоб ваша тестова база не захотіла збекапитися по-тихому.

Відновлення проводиться легким рухом руки з видаленням усіх даних PostgreSQL (у тому числі користувачів), тому, будь ласка, будьте гранично обережні, коли запускатимете наступні команди.

#!/bin/bash

# если есть балансировщик подключений (например, pgbouncer), то вначале отключаем его, чтобы он не нарыгал ошибок в лог
service pgbouncer stop
# если есть демон, который перезапускает упавшие процессы (например, monit), то останавливаем в нём процесс мониторинга базы (у меня это pgsql12)
monit stop pgsql12
# или останавливаем мониторинг полностью
service monit stop
# останавливаем саму базу данных
service postgresql stop
# удаляем все данные из текущей базы (!!!); лучше предварительно сделать их копию, если есть свободное место на диске
rm -rf /var/lib/postgresql/12/main
# скачиваем резервную копию и разархивируем её
su - postgres -c '/usr/local/bin/wal-g backup-fetch /var/lib/postgresql/12/main LATEST'
# помещаем рядом с базой специальный файл-сигнал для восстановления (см. https://postgrespro.ru/docs/postgresql/12/runtime-config-wal#RUNTIME-CONFIG-WAL-ARCHIVE-RECOVERY ), он обязательно должен быть создан от пользователя postgres
su - postgres -c 'touch /var/lib/postgresql/12/main/recovery.signal'
# запускаем базу данных, чтобы она инициировала процесс восстановления
service postgresql start

Для тих, хто хоче перевіряти процес відновлення – нижче підготовлений невеликий шматок bash-магії, щоб у разі проблем у відновленні – скрипт упав із ненульовим exit code. У даному прикладі робиться 120 перевірок з таймаутом в 5 секунд (всього 10 хвилин на відновлення), щоб дізнатися, чи відійшов сигнальний файл (це означатиме, що відновлення пройшло успішно):

#!/bin/bash

CHECK_RECOVERY_SIGNAL_ITER=0
while [ ${CHECK_RECOVERY_SIGNAL_ITER} -le 120 ]
do
    if [ ! -f "/var/lib/postgresql/12/main/recovery.signal" ]
    then
        echo "recovery.signal removed"
        break
    fi
    sleep 5
    ((CHECK_RECOVERY_SIGNAL_ITER+1))
done

# если после всех проверок файл всё равно существует, то падаем с ошибкой
if [ -f "/var/lib/postgresql/12/main/recovery.signal" ]
then
    echo "recovery.signal still exists!"
    exit 17
fi

Після успішного відновлення не забудьте запустити всі процеси (pgbouncer/monit і тд).

Перевірка даних після відновлення

Обов'язково потрібно перевірити цілісність бази після відновлення, щоб не виникла ситуація з побитою/кривою резервною копією. І краще робити це з кожним створеним архівом, але де і як залежить тільки від вашої фантазії (можна піднімати окремі сервери на погодинній оплаті або запускати перевірку в CI). Але як мінімум – необхідно перевіряти дані та індекси у базі.

Для перевірки даних достатньо прогнати їх через дамп, але краще, щоб при створенні бази у вас були включені контрольні суми (data checksums):

#!/bin/bash

if ! su - postgres -c 'pg_dumpall > /dev/null'
then
    echo 'pg_dumpall failed'
    exit 125
fi

Для перевірки індексів – існує модуль amcheck, sql-запит до нього візьмемо з тестів WAL-G і навколо вибудуємо невелику логіку:

#!/bin/bash

# добавляем sql-запрос для проверки в файл во временной директории
cat > /tmp/amcheck.sql << EOF
CREATE EXTENSION IF NOT EXISTS amcheck;
SELECT bt_index_check(c.oid), c.relname, c.relpages
FROM pg_index i
JOIN pg_opclass op ON i.indclass[0] = op.oid
JOIN pg_am am ON op.opcmethod = am.oid
JOIN pg_class c ON i.indexrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE am.amname = 'btree'
AND c.relpersistence != 't'
AND i.indisready AND i.indisvalid;
EOF
chown postgres: /tmp/amcheck.sql

# добавляем скрипт для запуска проверок всех доступных баз в кластере
# (обратите внимание что переменные и запуск команд – экранированы)
cat > /tmp/run_amcheck.sh << EOF
for DBNAME in $(su - postgres -c 'psql -q -A -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;" ')
do
    echo "Database: ${DBNAME}"
    su - postgres -c "psql -f /tmp/amcheck.sql -v 'ON_ERROR_STOP=1' ${DBNAME}" && EXIT_STATUS=$? || EXIT_STATUS=$?
    if [ "${EXIT_STATUS}" -ne 0 ]
    then
        echo "amcheck failed on DB: ${DBNAME}"
        exit 125
    fi
done
EOF
chmod +x /tmp/run_amcheck.sh

# запускаем скрипт
/tmp/run_amcheck.sh > /tmp/amcheck.log

# для проверки что всё прошло успешно можно проверить exit code или grep’нуть ошибку
if grep 'amcheck failed' "/tmp/amcheck.log"
then
    echo 'amcheck failed: '
    cat /tmp/amcheck.log
    exit 125
fi

Резюмуючи

Висловлюю подяку Андрію Бородіну за допомогу в підготовці публікації та окреме дякую за його внесок у розробку WAL-G!

На цьому ця замітка добігла кінця. Сподіваюся, що зміг донести легкість налаштування та величезний потенціал для застосування цього інструменту у вас у компанії. Я дуже багато чув про WAL-G, але не вистачало часу сісти і розібратися. А після того, як впровадив його у себе – з мене вийшла ця стаття.

Окремо варто зауважити, що WAL-G також може працювати з наступними СУБД:

Джерело: habr.com

Додати коментар або відгук