Бесшовная (почти) миграция между мажорными релизами PostgreSQL с помощью логической репликации

У нас в True Engineering на одном проекте назрела необходимость в смене версии PostgreSQL с 9.6 на 11.1.

Зачем? База данных на проекте уже объемом 1,5 Tb и растет. Перформанс — одно из основных требований к системе. А сама структура данных эволюционирует: добавляются новые колонки, меняются существующие. Новая версия Postgres научилась эффективно работать с добавлением новых колонок с дефолтным значением, так что не нужно городить кастомных костылей на уровне приложения. Ещё в новой версии добавили несколько новых способов партиционирования таблиц, что тоже крайне полезно в условиях большого объема данных.

Итак, решено, мигрируем. Конечно, можно поднять параллельно со старой новую версию сервера PostgreSQL, остановить приложение, через dump/restore (или pg_upgrade) переместить базу и снова запустить приложение. Нам это решение не подошло из-за большого размера базы, к тому же, приложение работает в боевом режиме, и на даунтайм есть считанные минуты.

Поэтому мы решили попробовать миграцию с помощью логической репликации в PostgreSQL с помощью стороннего плагина под названием pglogical.

В процессе «проб» мы столкнулись с весьма обрывочной документацией по этому процессу (а на русском языке её вообще нет), а также некоторыми подводными камнями и неочевидными нюансами. В этой статье мы хотим изложить свой опыт в виде Tutorial.

cpi7oixtsenmioqdrcyyotgagyk.png

TL; DR

  • Всё получилось (не без костылей, о них и статья).
  • Мигрировать можно в рамках PostgreSQL версии от 9.4 до 11.x, с любой версии на любую, вниз или вверх.
  • Даунтайм равен времени, которое требуется вашему приложению, чтобы переподключиться к новому серверу БД (в нашем случае это был перезапуск всего приложения, но в дикой природе, очевидно, «возможны варианты»).


Почему нам не подошло решение «в лоб»


Как мы уже сказали, самый простой выход: поднять параллельно со старой новую версию сервера PostgreSQL, остановить приложение, через dump/restore (или pg_upgrade) переместить базу и снова запустить приложение. Для баз небольшого объёма, в принципе, это вполне подходящий вариант (или, в общем случае — объём неважен, когда у вас есть возможность даунтайма приложения на время «переливания» БД со старого сервера на новый, каким бы долгим это время ни было). Но в нашем случае база занимает порядка 1,5 Tb на диске, и её перемещение — это вопрос не минут, а нескольких часов. Приложение же, в свою очередь, работает в боевом режиме, и даунтайма дольше пары минут очень хотелось избежать.

Также против этого варианта играл ещё и тот факт, что мы используем Master-Slave репликацию и не можем безболезненно выключить Slave-сервер из рабочего процесса. А значит, для переключения приложения со старой версии PostgreSQL на новую после миграции Master-сервера требовалось бы готовить и новый Slave-сервер до запуска приложения. А это ещё несколько часов простоя, пока создастся Slave (хотя и значительно меньше, чем миграция Master).

Поэтому решили попробовать миграцию с помощью логической репликации в PostgreSQL с помощью стороннего плагина под названием pglogical.

Общая информация


pglogical — это система логической репликации, использующая нативный Logical Decoding в PostgreSQL и реализованная в виде PostgreSQL extension. Позволяет настраивать выборочную репликацию с помощью модели подписок/публикаций. Не требует создания триггеров в базе или использования каких-либо внешних утилит для репликации.

Расширение работает на любой версии PostgreSQL, начиная с 9.4 (поскольку Logical Decoding впервые появился в 9.4), и позволяет осуществлять миграцию между любыми поддерживаемыми версиями PostgreSQL в любом направлении.

Настройка репликации с помощью pglogical вручную не очень тривиальна, хотя в принципе и вполне возможна. К счастью, существует сторонняя утилита pgrepup для автоматизации процесса настройки, которой мы и воспользуемся.

Памятка о свободном месте на диске


Поскольку мы планируем поднимать новую версию PostgreSQL на тех же серверах параллельно со старой, требования к диску под БД на серверах Master и Slave удваиваются. Казалось бы, это очевидно, но… Просто позаботьтесь о достаточном количестве свободного места перед запуском репликации, чтобы не жалеть о бесцельно прожитых годах.

В нашем случае потребовались модификации базы, плюс формат хранения при миграции между 9.6 и 11 «пухнет» не в пользу свежей версии, поэтому место на диске пришлось в итоге увеличивать не в 2, а примерно в 2.2 раза. Хвала LVM, это можно сделать в процессе миграции на лету.

В общем, take care of it.

Устанавливаем PostgreSQL 11 на Master


Note: Мы используем Oracle Linux, и всё нижеследующее будет заточено под этот дистрибутив. Не исключено, что другие дистрибутивы Linux потребуют небольшой доработки напильником, но вряд ли она будет существенной.

# добавляем репозиторий
yum install https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-7-x86_64/pgdg-oraclelinux11-11-2.noarch.rpm

# устанавливаем пакеты postgresql11
yum install postgresql11 postgresql11-devel postgresql11-server postgresql11-contrib

# инициализируем базу
/usr/pgsql-11/bin/postgresql-11-setup initdb


Старый datadir расположен в /var/lib/pgsql/9.6/data, новый, соответственно, ложится в /var/lib/pgsql/11/data

Копируем настройки доступа (pg_hba.conf) и настройки сервера (postgresql.conf) из 9.6 в 11.

Чтобы запустить два сервера PostgreSQL на одной машине, в конфиге postgresql.conf 11 версии меняем порт на 15432 (port = 15432).

Здесь нужно плотно подумать, что ещё нужно сделать в новой версии PostgreSQL конкретно в вашем случае, чтобы он запустился с вашим postgresql.conf (и ваше приложение могло в итоге с ним работать). В нашем случае требовалось установить в новую версию используемые нами расширения PostgreSQL. Это выходит за рамки статьи, просто сделайте так, чтобы новый PostgreSQL запустился, работал и полностью вас устраивал :)

# ставим расширения, тюним конфиги, добавляем shared libraries, whatever...
# ....
# запускаемся
systemctl enable postgresql-11
systemctl start postgresql-11


Смотрим в /var/lib/pgsql/11/data/pg_log/. Всё хорошо? Продолжаем!

Устанавливаем и настраиваем pgrepup

# ставим python
yum install python
yum install python2-pip

# ставим pgrepup
pip install pgrepup

# создаём конфигурацию
pgrepup config

zgxz9oct5l-qglkxyeyrqejh4we.png

Нюансы:

  1. В качестве app_owner указываем пользователя, под которым запущены серверы PostgreSQL.
  2. В качестве Database указываем template1.
  3. Username и Password — данные для доступа суперюзера. В нашем случае в pg_hba.conf для локальных подключений пользователя postgres был прописан метод trust, поэтому пароль можно указать произвольный.

Настраиваем репликацию

# запускаем проверку
pgrepup check


Получаем на выходе список из множества параметров, которые должны быть настроены требуемым образом.

Пример результатов проверки:

h3wuzmcevo7idf-xlyrz3ae8av8.png

58zhbbubdbfvzzgat051bviywqy.png

Все ошибки при проверке нужно будет устранить. В настройках обоих серверов должен быть выставлен wal_level=LOGICAL (для работы Logical Decoding), нужные настройки для движка репликации (количество слотов и wal_senders). Подсказки утилиты pgrepup достаточно информативны, по большинству пунктов вопросов возникнуть не должно.

Вносим все необходимые настройки, которые просит pgrepup.

В оба файла pg_hba.conf добавляем права доступа для пользователя, который будет делать репликацию, всё по подсказке pgrepup:

host replication pgrepup_replication 127.0.0.1/32 md5
host all pgrepup_replication 127.0.0.1/32 md5

Добавляем Primary Keys


Для работы репликации во всех таблицах должен быть определён Primary Key.

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

Список таблиц без PK, среди прочего, выдаёт pgrepup check. Для всех таблиц из этого списка нужно добавить primary key любым приемлемым для вас способом. В нашем случае это было нечто вида:

ALTER TABLE %s ADD COLUMN temporary_pk BIGSERIAL NOT NULL PRIMARY KEY


У утилиты pgrepup есть встроенная команда для проведения этой операции (pgrepup fix), и при её использовании даже подразумевается, что при успешной репликации эти временные колонки будут автоматически удалены. Но, к сожалению, этот функционал так неиллюзорно и феерически глючил на больших базах, что мы решили не использовать его, а сделать эту операцию вручную так, как нам удобно.

Устанавливаем pglogical extension


Инструкции по установке расширения можно почитать тут. Расширение необходимо установить в оба сервера.

# добавляем репозитории с нужными нам версиями
curl https://access.2ndquadrant.com/api/repository/dl/default/release/9.6/rpm | bash
curl https://access.2ndquadrant.com/api/repository/dl/default/release/11/rpm | bash

# устанавливаем пакеты
yum install postgresql96-pglogical postgresql11-pglogical


Добавляем загрузку библиотеки в postgresql.conf обоих серверов:

shared_preload_libraries = 'pglogical'

Устанавливаем pgl_ddl_deploy extension


Это вспомогательное расширение, которое pgrepup использует для логической репликации DDL.

# и его придётся собирать вручную
git clone https://github.com/enova/pgl_ddl_deploy.git

# сборка и установка для старого сервера
PATH=/usr/pgsql-9.6/bin/:$PATH
USE_PGXS=1 make
USE_PGXS=1 make install
make clean

# сборка и установка для нового сервера
PATH=/usr/pgsql-11/bin/:$PATH
make CLANG=true
make install


Добавляем загрузку библиотеки в postgresql.conf обоих серверов:

shared_preload_libraries = 'pglogical,pgl_ddl_deploy'

Проверяем внесённые изменения

# перезапускаем новый postgresql
systemctl restart postgresql-11


Теперь с помощью pgrepup check необходимо убедиться, что с целевым сервером всё стало ок и все замечания касательно целевого сервера полностью устранены.

Если всё хорошо, можно перезапускать старый сервер. Здесь нужно подумать о том, как к перезагрузке сервера БД отнесётся ваше приложение, возможно, следует его предварительно остановить.

# перезапускаем
systemctl restart postgresql-9.6

# проверяем
pgrepup check


Теперь в выводе команды все до единого пункты должны быть отмечены как ОК.

Казалось бы, можно запускать миграцию, но…

Правим баги pgrepup


В актуальной версии pgrepup есть несколько багов, делающих миграцию невозможной. Pull request«ы отправлены, но увы, остаются без внимания, поэтому придётся сделать исправления вручную.

Идём в папку установки pgrepup (наш случай — /usr/lib/python2.7/site-packages/pgrepup/commands/).

Делай раз. В каждом файле *.py добавляем пропущенные **kwargs в описании функции. Картинка лучше тысячи слов:

l3zcuo2exe8lq_0u3gwv_nq8jty.png

Коммит тут.

Делай два. В setup.py делаем поиск по «sh -c», два вхождения, все многострочные команды shell нужно сделать однострочными.

Коммит тут.

Запускаем миграцию

# подготовка
pgrepup setup


Этой командой pgrepup подготавливает оба сервера к запуску репликации, создаёт пользователя, настраивает pglogical, переносит схему БД.

wwkmjdmmpmwyvgbdqyktohnhmmy.png

# запускаем репликацию
pgrepup start


Он сказал «Поехали!» и махнул рукой:

3kxc_rgde-t7q6yuhtvwkol5acs.png

Репликация запущена. Текущую ситуацию можно увидеть с помощью команды pgrepup status:

iggswbqldii-c6cd5a1y7qyqawu.png

Здесь мы видим, что две БД уже переехали и репликация идёт, а одна ещё в процессе переезда. Теперь остаётся только пить кофе и ждать, пока прокачается весь объём исходной БД.

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

SELECT * FROM pg_replication_origin_status ORDER BY remote_lsn DESC;
SELECT *,pg_xlog_location_diff(s.sent_location,s.replay_location) byte_lag FROM pg_stat_replication s;
SELECT query FROM pg_stat_activity WHERE application_name='subscription_copy'


Вдоволь напившись кофе (на тестовом сервере при написании этой статьи миграция ~700Gb данных длилась в районе суток), мы наконец видим такую картину:

ep2eq2c_mtywxboftk4ocb0e_1c.png

И это означает, что пришло время готовить новый Slave.

Устанавливаем PostgreSQL 11 на Slave


Здесь всё просто и по учебнику, никаких нюансов.

# добавляем репозиторий
yum install https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-7-x86_64/pgdg-oraclelinux11-11-2.noarch.rpm

# устанавливаем postgresql 11
yum install postgresql11 postgresql11-devel postgresql11-server postgresql11-contrib

# переливаем данные с нового мастера
su - postgres
pg_basebackup -h db-master.hostname -p 15432 -D /var/lib/pgsql/11/data/ -R -P -U replication -X stream -c fast

Копируем настройки доступа (pg_hba.conf) и настройки сервера (postgresql.conf) из 9.6 в 11. В конфиге postgresql.conf 11 версии меняем порт на 15432 (port = 15432)

# запускаем
systemctl enable postgresql-11
systemctl start postgresql-11
# проверяем статус репликации на Master
SELECT *,pg_wal_lsn_diff(s.sent_lsn,s.replay_lsn) AS byte_lag FROM pg_stat_replication s;

# проверяем статус репликации на Slave
SELECT now()-pg_last_xact_replay_timestamp();


Промежуточные итоги


После всех этих процедур у нас получится вот такая хитрая схема репликаций:

70nsejcslxlccih_yktvesptqjg.png

Здесь в качестве последней проверки (ну и, в конце концов, это просто красиво) можно сделать какой-нибудь UPDATE в базу на 9.6 Master и пронаблюдать, как он среплицируется на остальные три сервера.

image

Переключение приложения на новую версию PostgreSQL


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

Для интереса ответим на оба вопроса «да» и приступим.

Останавливаем приложение.

# проверяем, что нет коннектов, например:
SELECT * FROM pg_stat_activity;
# останавливаем логическую репликацию
# при этом также произойдёт финальная синхронизация sequences.
pgrepup stop


czhc2t8k6pjk-6ivvilidj5k9xq.png

# чистим за собой все хвосты
pgrepup uninstall


2wuddqovqbninwwz9rx9fia8psa.png

# на master:
# выключаем старый сервис
systemctl disable postgresql-9.6
# останавливаем оба сервиса, сначала старый, затем новый.
systemctl stop postgresql-9.6
systemctl stop postgresql-11

# на slave:
# выключаем старый сервис
systemctl disable postgresql-9.6
# останавливаем оба сервиса, сначала старый, затем новый.
systemctl stop postgresql-9.6
systemctl stop postgresql-11


Возвращаем стандартный порт в конфиге postgresql.conf новой версии на Master и Slave.

На новом Slave также меняем порт на стандартный в recovery.conf.

Попутно есть предложение от греха подальше поменять порт на становящейся неактивной старой версии:
Выставляем нестандартный порт в postgresql.conf старой версии на Master и Slave.
На старом Slave также меняем порт на нестандартный в recovery.conf.

# запускаем на master
systemctl enable postgresql-11
systemctl start postgresql-11

# запускаем на slave:
systemctl enable postgresql-11
systemctl start postgresql-11


Проверяем логи.

Проверяем статус репликации на Master.

SELECT *,pg_wal_lsn_diff(s.sent_lsn,s.replay_lsn) AS byte_lag FROM pg_stat_replication s;


Запускаем приложение. Радуемся полчаса.

А напоследок полезная литература по теме:


Успехов!

© Habrahabr.ru