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


    При активной разработке ПО нередко нужна тестовая база с актуальными данными из боевой базы. Хорошо, если база маленькая и развернуть копию не долго. Но если в базе десятки гигабайт данных и все нужны для полного тестирования, да ещё и посвежее, то возникают трудности. В этой статье я опишу вариант преодоления подобных неприятностей с помощью 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.


    UPD pg_rewind


    В комментариях прозвучали многочисленные опасения в стабильности btrfs как таковой и сомнения в пригодности использования её для хранения базы данных в частности. Поэтому, предлагаю вариант адаптации вышеизложенной концепции под возможности утилиты pg_rewind. Утилита эта находит точку рассинхронизации master-а и slave-а и приводит целевую базу в последние состояние перед рассинхронизацией. Но это только состояние, для того, чтобы восстановить данные, будут необходимы все накопившиеся WAL-логи. Также pg_rewind качает с мастера довольно большой объём данных, даже если базы разъехались только-что (в моём случае ~10Г). И целевая база должна корректно завершить работу перед операцией синхронизации.


    Итак:


    1. В конфигурации тестовой базы необходимо включить опцию wal_log_hints = on, которая необходима для pg_rewind, а также может ухудшить производительность. На master-е необходимо разрешить доступ с тестового сервера пользователю с правами супер-пользователя.


    2. Из скриптов systemd убираем все вхождения btrfs и работу со snapshot-ами. "postgres-master.service" станет совсем простой и сведётся к созданию trigger-файла.


    3. Скрипт "postgres-slave.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 [[ -e "/var/lib/postgresql/slave/9.6/main/recovery.done" ]]; then /usr/lib/postgresql/9.6/bin/pg_rewind -D /var/lib/postgresql/slave/9.6/main/ --source-server="host=master.host port=5432 user=postgres password=очень_очень_секретный_пароль" --progress ; /bin/cp /var/lib/postgresql/recovery.conf /var/lib/postgresql/slave/9.6/main/recovery.conf;  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

    Перед началом работы pg_rewind необходимо корректно завершить работу базы.
    Если база в состоянии master, то начинаем её синхронизацию с основной базой:


    1. Прогоняем pg_rewind с указанием целевой директории базы (-D), параметрами подключения к мастеру и ключом подробного вывода.
    2. Копируем заранее сохранённый файл recovery.conf, который удаляется в результате работы pg_rewind.
    3. Поднимаем базу, которая начнёт догонять убежавшего мастера, используя WAL-логи.

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


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

    Поделиться публикацией

    Комментарии 34

      +1
      Нет особого смысла хранить архивлоги за большой период. Если только вам не нужно откатиться на заданную минуту в прошлом, тогда пригодятся и снапшоты и архивлоги. Для случаев отставания реплики достаточно периода в несколько дней. Само отставание (в секундах) мониторить можно так
      select extract(epoch from now() - pg_last_xact_replay_timestamp())::integer

      Останавливать базу для создания снапшота необязательно, достаточно выполнить два запроса
      psql -U pgsql postgres -t -A <<EOT
      SELECT select pg_xlog_replay_pause();
      CHECKPOINT;
      EOT
      

      и после создания снапшота
      psql -U pgsql postgres -t -A -c "select pg_xlog_replay_resume();"
      


        0
        Архив-логи я храню с целью бекапа, позволяет уменьшить частоту полного дампа. Да и смелые эксперименты могут затянутся и приходится догонять несколько дней.

        Базу можно и не останавливать, но для данной задачи это не важно, зато так проще и скрипты чище.

        Заббиксом конечно же удобнее мониторить в секундах, спасибо за дополнение.
          0
          Загрузка полного дампа на больших базах — это последнее средство, когда все плохо. У меня только дамп базы делается несколько дней, загрузка его — раза в полтора-два дольше. А накатывание архивлогов ведется в один поток, и при большом потоке изменений в базе накатить логи за 4 дня займет едва ли не сутки.

          В этом случае снапшоты — наше все. Они в случае btrfs практически бесплатные — то есть расходуют только занимаемое место, по объему примерно как сжатые gzip'ом архивлоги. Хранить их можно, пока свободное место позволяет (процентов до 20).

          Снапшоты в btrfs — записываемые (вернее, доступен для записи каталог, куда снапшот монтируется). Нет необходимости переводить основную реплику в мастер, можно поднять еще один постгрес (лучше еще один контейнер) на снапшоте и его перевести в мастер. В этом случае не придется нагонять репликацию — у вас всегда будет актуальная реплика, с нее можно снимать дампы (хотя в этом есть нюансы).
          В таком сценарии делать снапшоты базы без ее остановки весьма удобно — у вас всегда есть некоторое количество снапшотов, для отката базы или для поднятия тонких копий в качестве девелоперской базы
            0
            Да, инструмент снапшотов мне тоже очень нравится. Активно его использую. А ещё классная вещь — сжатие btrfs. Раздел данных логов в elasticsearch занимает раза в полтора меньше места.

        0
        Я не специалист в postgresql, но разве pg_rewind делает не то же самое?
          0
          Да, можно и этой утилитой получить подобный результат. Но она так же завязана на WAL логи, которые так же придётся хранить и использовать для восстановления нужного состояния. Вариант со снапшотом мне кажется аккуратнее и прозрачнее.
          В статья я старался сделать акцент не столько на инструмент снапшотов, сколько на широкие возможности systemd.
            0
            Учтя многочисленные возгласы сомнения из зала в стабильности btrfs, дополнил статью вариантом использования pg_rewind.
            0
            Как санитизируются чувствительные данные — емейлы, хэши паролей, профили людей?
              0
              Не совсем понятно, что имеется в ввиду?
              Если сохранность данных, после таких операций — то никак. Тут полностью доверяем механизму надёжности WAL логов PostgreSQL
              Если обезличивание данных — то тоже никак :). Сервера полностью под контролем и открытыми каналами данные передаются только в шифрованном виде.
                0
                Понятно. Вот так они и сливаются, персональные данные с тестовых баз, по шифрованным каналам.
                  +1
                  Здоровая паранойя — вещь полезная, но всё хорошо в меру.
                    0
                    Дело не паранойе и не в мере, а в том, сколько бизнес потеряет при сливе копии. В некоторых случаях, такие копии прямо запрещены регулятором. Вы предложили решение проблемы, это замечательно. Я всего лишь уточнил применимость решения.
                    0
                    Вероятно, имелось ввиду что девелоперская база находится в DMZ и снаружи недоступна. Непущать разработчиков продакшен базу — в целом здравая идея. Пусть разрабатывают набор начальных данных для заливки на тестовую базу. И на все вопросы отвечают — а у нас все работает ;)
                      0
                      Разработчики и без базы так отвечают. Могут и тест емейл-рассылки запустить. Всякое бывает. Вопрос был по применимости решения. Он отвечен. Похожее решение я делал с санитизацией, например. Но то вне рамок данной статьи.
                0
                btrfs уже можно использовать?
                  0
                  Давно. Использую как лично, так и на серверах (где применимо). Также Synology по-умолчанию начала применять btrfs.
                    0
                    Для продакшена гонять пока боязно. Но для всяких служебных второстепенных нужд использовать такие возможности как снапшоты и сжатие очень удобно. У меня активно используется в разных вариантах и версиях — проблем пока не возникало.
                      +1
                      Для такой поделки наверное можно. Но гонять продакшн на чём то отличном от ext4 и xfs я бы лично не стал.
                        0
                        У меня на личном ноуте покрашилась после отключения питания. Данные слил бэкапом нормально, но запустить обратно саму fs не смог. ubuntu 16.04, ssd, luks partition, btrfs root & home
                          0
                          Применял для одного проекта по нейросетям, обрабатывающем фотографии. Всего было обработано 400,000 изображений (итого 800к — оригиналы и результат), никаких сбоев/проседаний или ещё чего в таком духе не было. Так что да, давно можно.
                            0
                            Ни в коем случае нельзя. Использовал на проде для мультидискового видеохранилища. В итоге начала деградировать скорость записи. Проблема выглядит так: одно из ядер CPU загружено на 100%, драйвер btrfs затыкается в коде поиска свободного блока.

                            Самое ужасное, что описание проблемы есть в багтрекере btrfs, но там отвечают, что «у меня всё работает, на моём 200МБайтном тестовом разделе ничего не воспроизводится».
                              –1
                              если это ответ разработчика, то о проекте можно забыть
                                0
                                Нашёл эту переписку: https://bugzilla.kernel.org/show_bug.cgi?id=74761

                                Уже сейчас есть люди, активно использующие ZFS on Linux на проде, и уже сейчас у ZFS больше шансов. Btrfs это ходячий труп.
                                  +2
                                  У ZFS-on-Linux нет предпосылок к успеху. Я столкнулся с тем, что ARC cache отображается в used memory, низкая производительность и деградации как следствие.
                                  Т.е. при всём моём уважении к ZFS, он создавался не для этой OS.
                            0
                            del
                              0
                              А версия FS и утилит?
                              А какие параметры монтирования?
                              Так, для общего развития.
                                0
                                Полгода назад было, уже не найду инфу, к сожалению.
                                Стоковая ubuntu 16.04 с дефолтными опциями.
                                Насколько помню, было что-то похожее: ошибки на чтение CRC tree, снимки не монтировались, как ro система не монтировалась, repair и init-csum-tree не помогли…
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Опции монтирования по умолчанию:
                                /dev/pg-slave-test-db on /var/lib/postgresql type btrfs (rw,relatime,space_cache,subvolid=5,subvol=/)
                                

                                Тобишь CoW включён. Специального тестирования не производил, но тестовый сервер с btrfs ведёт себя так же, как и без. Тесты на данных базы проходят быстро и успешно, каких-либо отклонений в производительности в сравнению с сервером базы данных на ext4 не замечено. Восстановление состояния по WAL логам происходит быстро. Если в мастер-базу вносятся глобальные изменения, то порой база на btrfs показывает лучшую успеваемость, чем основная резервная база с ext4. Но я посмотрю внимательно в эту сторону, мало ли.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Предложенную систему можно кстати усовершенствовать:
                                1) создаём снапшот
                                2) на снапшоте создаём triggerfile
                                3) запускаем postgres в docker и указываем ему наш снапшот как volume
                                4) реплика остаётся репликой (нетронутой), плюс у нас есть база для тестов
                                5) после тестов просто удаляем снапшот

                                P.S.> В принципе можно обойтись и без докера, нужно только при старте postgres указать другой port.

                                У меня была идея сделать что-то подобное пару лет назад, но к сожалению при большом количестве disk writes btrfs вела себя непредсказуемо, вся система периодически замирала, иногда на доли секунды, а иногда до нескольких секунд :(
                                  0
                                  Можно и так.
                                  В этом плане Linux — отличный конструктор.
                                  0
                                  А как же многочисленные предупреждения, что CoW файловые системы (btrfs, zfs) нельзя использовать как бекенд к базе данных, потому что у БД есть свой слой косвенности?
                                    0
                                    Поставленные цели сборка выполняет отлично. А поведение btrfs под базой данных и базы данных на btrfs буду наблюдать, на то он и тестовый стенд.

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

                                  Самое читаемое