Вже давно відомо, що робити бекапи в 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 мануалу, який включав би всі кроки від і до (на Хабре є кілька постів, але багато моментів там втрачені).
Ця стаття написана в першу чергу, щоб систематизувати свої знання. Я не є DBA і десь можу висловлюватись обивательсько-розробницькою мовою, тому вітаються будь-які виправлення!
Окремо відзначу, що все наведене нижче актуально і перевірено для PostgreSQL 12.3 на Ubuntu 18.04, всі команди повинні виконуватися від привілейованого користувача.
Встановлення
На момент написання цієї статті стабільна версія WAL-G – v0.2.15 (березень 2020). Її ми і будемо використовувати (але якщо ви захочете самостійно зібрати його з master-гілки, то в репозиторії на github є всі інструкції для цього). Для скачування та встановлення потрібно виконати:
Після цього потрібно налаштувати спочатку WAL-G, а потім сам PostgreSQL.
Налаштування WAL-G
Для зберігання бекапів буде використовуватися Amazon S3 (тому що він ближчий до моїх серверів і його використання обходиться дуже дешево). Для роботи з ним потрібен "s3-бакет" та ключі доступу.
У всіх попередніх статтях про WAL-G використовувалося конфігурування за допомогою змінних оточення, але з цього релізу налаштування можна розташувати в .walg.json файлі у домашній директорії користувача postgres. Для його створення виконаємо наступний bash-скрипт:
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 як у цьому прикладі.
Щоб архіватор усередині бази сам заливав WAL-журнали у хмару та відновлювався з них (у разі потреби) – потрібно задати кілька параметрів у конфігураційному файлі /etc/postgresql/12/main/postgresql.conf. Тільки для початку вам потрібно переконатися, що жодні з наведених нижче налаштувань не задані в якісь інші значення, щоб при перезавантаженні конфігурації – СУБД не впала. Додати ці параметри можна за допомогою:
wal_level – скільки інформації писати у WAL журнали, «replica» – писати все;
archive_mode – увімкнення завантаження WAL-журналів за допомогою команди з параметра archive_command;
archive_command – команда для архівації завершеного WAL-журналу;
archive_timeout - архівування журналів проводиться тільки коли він завершений, але якщо ваш сервер мало змінює/додає даних в БД, то є сенс виставити тут ліміт в секундах, після якого команда архівації буде викликана примусово (у мене інтенсивний запис в базу кожну секунду, тому я відмовився від встановлення цього параметра у продакшені);
Команда відновлення - команда відновлення WAL-журналу з бекапу, використовуватиметься у разі якщо в «повному бекапі» (base backup) бракуватиме останніх змін у БД.
Як не крути, але найзручнішим способом для запуску є 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:
У цьому прикладі процес бекапу запускається щодня о 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 також може працювати з наступними СУБД: