- PVSM.RU - https://www.pvsm.ru -

PostgreSQL slave + btrfs и systemd = горячая тестовая база

PostgreSQL slave + btrfs и systemd=горячая тестовая база - 1

При активной разработке ПО нередко нужна тестовая база с актуальными данными из боевой базы. Хорошо, если база маленькая и развернуть копию не долго. Но если в базе десятки гигабайт данных и все нужны для полного тестирования, да ещё и посвежее, то возникают трудности. В этой статье я опишу вариант преодоления подобных неприятностей с помощью snapshot-ов btrfs. А управлять работой получившегося комплекса будет systemd – удобный и функциональный инструмент.

Подготовка

На серверах я использовал:

Debian jessie 4.7.0-0.bpo.1-amd64 #1 SMP Debian 4.7.8-1~bpo8+1 
btrfs-progs v4.7.3
systemd 230
postgresql 9.6.1

Но это не принципиально.
В разделе btrfs, который будет отдан под базу, создаём два тома:

btrfs subvolume create /var/lib/postgresql/slave
btrfs subvolume create /var/lib/postgresql/snapshot

В первом будут храниться данные базы, а во втором shapshot-ы этих данных.

Первым делом нужно поднять PostgreSQL slave базу, которая в обычном режиме будет содержать полную копию данных мастера. Алгоритмы развёртывания уже не однократно описаны, потому коротко и по существу:

Master postgresql.conf

max_wal_senders = 3             # max number of walsender processes
                                # (change requires restart)
wal_keep_segments = 64          # in logfile segments, 16MB each; 0 disables
archive_mode = on               # allows archiving to be done
                                # (change requires restart)
archive_command = 'test ! -f /var/lib/postgresql/9.6/main/archive/%f.7z && 7za a /var/lib/postgresql/9.6/main/archive/%f.7z -pочень_секретный_пароль_который_нам_ещё_пригодится -mx7 -mhe=on %p'

Таким образом по заполнении очередного WAL сегмента, запускается процесс архивирования его в специальную директорию archive в папке базы. В моём случае, дальше эти архив-логи передаются открытыми каналами и некоторое время хранятся в разных местах, потому шифруются и защищаются паролем. Срок, который будем хранить архив-логи, определит промежуток времени, на который мы можем восстановить состояние базы-slave. При активных изменениях базы количество WAL-логов может очень быстро расти, пожирая свободное место и тут архивирование очень полезно.
Чистить архив-логи нужно самостоятельно, например так:

find /var/lib/postgresql/9.6/main/archive/ -type f -mtime +10 -print0 | xargs -0 -n 13 /bin/rm

Slave postgresql.conf

data_directory = '/var/lib/postgresql/slave/9.6/main'       # use data in another directory
hot_standby = on            # "on" allows queries during recovery

Директорию базы помещаем на один уровень ниже стандартного в каталог "slave", так как /var/lib/postgresql — точка монтирования раздела btrfs.

Slave recovery.conf

standby_mode = 'on'
primary_conninfo = 'host=master.host port=5432 user=replicator password=пароль'
trigger_file = '/var/lib/postgresql/slave/9.6/main/trigger'
restore_command = '7za e /mnt/backup/postgresql/archive/%f.7z -pа_вот_и_пригодился_очень_секретный_пароль -so > %p'

При такой настройке база запускается в slave-режиме и получает изменившиеся данные напрямую от мастера. А если вдруг данных на мастере уже не будет, то поднимет необходимый сегмент данных из директории /mnt/backup/postgresql/archive/. Архив-логи туда попадают с помощью rsync, который периодически забирает обновления с мастера. Я забираю в режиме синхронизации с удалением, но можно их и хранить произвольный период времени.
Так же база следит за появлением trigger-файла и если он появился, то переключается в режим мастера и готова сама обрабатывать все запросы. С этого момента master и slave базы рассинхронизируются. А блестящая идея заключается в том, чтобы перед переключением сделать снимок slave-базы и, после необходимых экспериментов, вернуть его на место, как будто ничего и не было. Поднявшись, база обнаружит, что отстала от мастера, используя архив-логи догонит и войдёт в штатный slave-режим. Эта операция занимает существенно меньше времени, чем полное восстановление из бекапа.
Реализуем идею средствами systemd, который позволит не только автоматизировать процесс, но и учесть необходимые зависимости и нежелательные конфликты. Предполагаю, что вы уже знакомы с тем, как работает и конфигурируется systemd, но постараюсь подробно описать используемые параметры.

Настройка systemd

Первым делом смонтируем раздел, где хранятся данные базы. У меня всё собрано в LXC контейнере, в который раздел с btrfs попадает блочным устройством по пути /dev/pg-slave-test-db. Создадим элемент (unit) systemd типа .mount:

/etc/systemd/system/var-lib-postgresql.mount

[Unit]
  Description=PostgreSQL DB dir on BTRFS
  Conflicts=umount.target

[Mount]
  What=/dev/pg-slave-test-db
  Where=/var/lib/postgresql
  Type=btrfs
  Options=defaults
  TimeoutSec=30

[Install]
  WantedBy=multi-user.target

Название элемента определяется точкой монтирования. Conflicts=umount.target обеспечит отмонтирование раздела при выключении. Тут есть момент: указывать можно не только абсолютные пути, но и использовать уникальный UUID идентификатор устройства. Но, при запуске в LXC контейнере наткнулся на странное — UUID в системе не виден до тех пор, пока его явно не запросить командой blkid. Потому использую абсолютный путь.
Этот юнит можно запускать как самостоятельно, так и использовать в зависимостях, что мы и будем делать.

Создадим элемент systemd, описывающий запуск PostgreSQL в slave-режиме. База предварительно настроена на запуск в ручном режиме, в debian это делается в файле /etc/postgresql/9.6/main/start.conf. Так же, при наличии службы postgresql@9.6-main.service, её нужно выключить.

/etc/systemd/system/postgres-slave.service

[Unit]
Description=Restore PostgreSQL base snapshot and Switch the PostgreSQL base in slave mode.
After=network.target var-lib-postgresql.mount postgresql.service
Requires=var-lib-postgresql.mount
Conflicts=postgres-master.service

[Service]
Type=oneshot
RemainAfterExit=yes
User=root

TimeoutSec=300

ExecStartPre=-/usr/bin/pg_ctlcluster -m fast 9.6 main stop
ExecStartPre=/bin/bash -c 'if [[ -d "/var/lib/postgresql/snapshot/auto" ]]; then /bin/btrfs subvolume delete /var/lib/postgresql/slave; fi'
ExecStartPre=/bin/bash -c 'if [[ -d "/var/lib/postgresql/snapshot/auto" ]]; then /bin/mv -v /var/lib/postgresql/snapshot/auto /var/lib/postgresql/slave; fi'

ExecStart=/usr/bin/pg_ctlcluster 9.6 main start

ExecStop=/usr/bin/pg_ctlcluster -m fast 9.6 main stop

[Install]
WantedBy=multi-user.target

Подробно разберём.
Параметр After задаёт желаемую очерёдность запуска службы, но ни к чему не обязывает, в отличии от Requires. Последний требует, чтобы была активна указанная служба и попытается её активировать. Если это не удастся, то служба целиком перейдёт в состояние "fail". Параметр Conflicts говорит, что наша служба не может сосуществовать с указанной и одну из них нужно выключить. В данном случае, при запуске службы "postgres-slave.service" будет автоматически выключена " "postgres-master.service" (которую мы опишем ниже), что очень удобно.
Type=oneshot говорит о том, что данная служба быстро отработает и нужно подождать до конца. А RemainAfterExit=yes оставит службу в состоянии "active" после успешного завершения требуемых действий. Так как мы будем создавать снимки и управлять базой данных, то желательно увеличить таймаут, который по умолчанию 30 секунд.
Дальше, по сути, выполняется скрипт, который приводит систему в желаемое состояние. Команды ExecStartPre выполняются перед основной командой ExecStart по порядку и каждая ждёт успешного завершения предыдущей. Но у первой команды указан модификатор "-", который позволяет продолжить выполнение, не смотря на код возврата. Ведь база может быть уже остановлена на момент запуска.

выполняемый при запуске скрипт:

/usr/bin/pg_ctlcluster -m fast 9.6 main stop
/bin/bash -c 'if [[ -d "/var/lib/postgresql/snapshot/auto" ]]; then /bin/btrfs subvolume delete /var/lib/postgresql/slave; fi'
/bin/bash -c 'if [[ -d "/var/lib/postgresql/snapshot/auto" ]]; then /bin/mv -v /var/lib/postgresql/snapshot/auto /var/lib/postgresql/slave; fi'

/usr/bin/pg_ctlcluster 9.6 main start

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

  1. На первом шаге выключаем работающую базу с параметром "-m fast", чтобы не ждать отключения всех клиентов.
  2. Проверяем, есть ли директория "/var/lib/postgresql/snapshot/auto", в которой хранится последний снимок базы в slave-режиме, и которая появляется в результате работы службы "postgres-master.service". Если снимок есть, значит, текущее состояние базы — master. Удаляем данные тестовой базы (/var/lib/postgresql/slave).
  3. И восстанавливаем на это место снимок.
  4. После успешного выполнения подготовительных команд, запускаем базу.

Последняя команда "ExecStop" определяет, как служба будет выключаться. Служба самодостаточна и её можно добавлять в автозапуск при старте системы.

Создадим элемент systemd, описывающий запуск PostgreSQL в master-режиме.

/etc/systemd/system/postgres-master.service

[Unit]
Description=Create PostgreSQL base Snapshot and Switch the PostgreSQL base in master mode.
After=network.target var-lib-postgresql.mount postgresql.service
Requires=var-lib-postgresql.mount
Conflicts=postgres-slave.service

[Service]
Type=oneshot
RemainAfterExit=yes
User=root

TimeoutSec=300

ExecStartPre=-/usr/bin/pg_ctlcluster -m fast 9.6 main stop
ExecStartPre=/bin/bash -c 'if [[ -d "/var/lib/postgresql/snapshot/auto" ]]; then /sbin/btrfs subvolume delete /var/lib/postgresql/snapshot/auto; fi'
ExecStartPre=/bin/btrfs subvolume snapshot /var/lib/postgresql/slave /var/lib/postgresql/snapshot/auto
ExecStartPre=/usr/bin/touch /var/lib/postgresql/slave/9.6/main/trigger

ExecStart=/usr/bin/pg_ctlcluster 9.6 main start

ExecStop=/usr/bin/pg_ctlcluster -m fast 9.6 main stop

[Install]
WantedBy=multi-user.target

В конфликтах у службы, конечно же"postgres-slave.service".

Выполняемый скрипт сводится к следующему:

/usr/bin/pg_ctlcluster -m fast 9.6 main stop
/bin/bash -c 'if [[ -d "/var/lib/postgresql/snapshot/auto" ]]; then /sbin/btrfs subvolume delete /var/lib/postgresql/snapshot/auto; fi'
/bin/btrfs subvolume snapshot /var/lib/postgresql/slave /var/lib/postgresql/snapshot/auto
/usr/bin/touch /var/lib/postgresql/slave/9.6/main/trigger

/usr/bin/pg_ctlcluster 9.6 main start

  1. Останавливаем работающую базу.
  2. Проверяем, есть ли старый снимок базы по пути "/var/lib/postgresql/snapshot/auto", если есть — удаляем.
  3. Создаём новый снимок последних данных базы.
  4. Создаём триггер-файл, который и переведёт базу в режим мастера.
  5. Поднимаем базу.

С этого момента, можно выполнять самые смелые эксперименты над данными. А когда надоест, запустив службу "postgres-slave.service" вернуть всё на место.
Переключения базы в различные режимы можно настроить по крону, по каким-либо критериям, через зависимые службы или даже завести элемент systemd типа ".socket" и поднимать тестовую базу при первом запросе к приложению. Благо, systemd позволяет.

Мониторить текущее отставание от базы-мастера, можно с помощью запроса:

postgres=# select now() - pg_last_xact_replay_timestamp() AS replication_delay;

Который не составит труда подвесить на тот же zabbix.

Спасибо за внимание.

Автор: Lelik13a

Источник [1]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/postgresql/221138

Ссылки в тексте:

[1] Источник: https://habrahabr.ru/post/317660/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best