
Зачастую, предлагая обновить PostgreSQL, подразумевается условный прыжок веры на более новую версию без варианта быстрого отката на предыдущую версию без потери данных.
Привет! Я Ирек Агмалов, DBA-SRE компании Ви.Tech (ВсеИнструменты.ру). Нам потребовалось обновить pg кластер под управлением Patroni состоящий из 6 нод, где временно в ночное время его трафик можно ужать в три ноды, и чтобы было куда откатываться не теряя накопленные на новой версии базы pg данные. И конечно же чтобы простой сервиса был минимальным.
Далее опишу как мы обновляли кластер базы pg v11 на pg v15, который должен состоять из минимум трёх нод. Мы пошли дальше и подняли репликацию c pg v15 на ещё один кластер pg v11, который был для случая отката, если всё же что-то бы пошло не так, а разработчики приложения не смогли бы поправить ситуацию за короткое время.
Объём базы исчисляется терабайтами: для ускорения развертывания логических реплик делали их из физических бэкапов. В 17ом PostgreSQL превращение физической реплики в логическую реализовано - на тот момент такой возможности не было.
Итого: создаём межкластерную логическую репликацию pg v11 → pg v15 → pg v11, где в каждом кластере минимум три ноды. В ходе обновления переключаем приложение на pg v.15, а в случае отката на третий кластер на pg v11.
План обновления начинал верстаться до перехода с 2023 на 2024 год, потому версия целевого PostgreSQL в нашем обновлении - 15-ая. Далее версия в плане не менялась, так как для неё были проведены тесты различными командами, и новый этап тестирований не произошёл. Обновление было более полугода назад.
В статье опущены настройки PostgreSQL, Patroni, Consul за исключением необходимого для описания сути работ.
Подготовка двух кластеров для выполнения переключения на обновлённый PostgreSQL

Кластера pg работают под управлением Patroni, подключение приложения осуществляется через consul-dns, в зависимости от запроса используется подключение на лидера, "быструю" (fast) или "медленную" (slow) реплику. У быстрой и медленной реплики разное допустимое время отставание от лидера: на медленных репликах выполняются долгие запросы, на быстрых - короткие.
В ходе обновления включали слоты логической репликации в Patroni, а на получателе репликация настраивалась по consul-dns лидера, чтобы падение/недоступность лидера не отменило проведение работ.
Переключение приложения на новый pg за счёт выкатки приложения с новым адресом подключения.
На время обновления: от создания бэкапа и до принятия решения откатываемся или нет - не допускается никаких DDL операций в базе. Да, некоторые ограничения из DDL можно обойти, применить, но тут репликация как вверх так и вниз по версиям - ненужные риски исключаем.
Подготовка к каскадной логической репликации между разными версиями
На нодах кластера 2 устанавливается PostgreSQL 15 версии и настраивается возможность работы кластера Patroni, и consul с именем кластера отличающимся от кластера 1.
На нодах кластера 3 устанавливается PostgreSQL 11 версии, Patroni, consul с именем отличным от кластера 1 и кластера 2, всё настраивается так чтобы в случае необходимости быть способным полноценно принять трафик приложения.
На первой ноде кластера 2 дополнительно устанавливаем PostgreSQL 11-ой версии - там будем восстанавливаться из бэкапа с кластера 1.
На всех кластерах необходим уровень репликации - логический: wal_level = logical. Параметр не динамический - включение требует перезапуска сервера PostgreSQL.
Использование команды time ниже в примерах в командной строке bash вызвано из необходимости планирования хронометража каждого из этапа работ, так далее и осталось.
-- Лидер кластера 1: создаём слот логической репликации
prodb=# select pg_create_logical_replication_slot('prodb_shadow_slot', 'pgoutput');
-- Лидер кластера 1: создаём публикацию на все таблицы в базе prodb, подключившись к базе prodb
prodb=# CREATE PUBLICATION logical_prodb_11_15_11 FOR ALL TABLES;
-- проверки наличия слота и публикации
$ select * from pg_replication_slots ;
$ select * from pg_publication;
-- Лидер кластера 2: создаём бэкап с реплики без компрессии - чтобы база сразу была доступна для запуска
$ time /usr/lib/postgresql/11/bin/pg_basebackup -h {pg_replica_cluster_1} -p 5432 -U postgres -W -D /data/11/main -Fp -Xs -P -v
-- Лидер кластера 2: Запускаем полученную через бэкап базу, потребуется поправить параметры с указанием на data_directory - куда тянули базу
$ pg_ctlcluster 11 main start
-- Лидер кластера 2: Запрашиваем последний выполненный LSN, потребуется для смещения логического слота на лидере кластера 1
prodb=# select pg_last_wal_replay_lsn();
-- Лидер кластера 2: проверка что база точно в режиме восстановления
select pg_is_in_recovery();
-- Лидер кластера 3: вытягиваем бэкап с лидера кластера 2
$ time /usr/lib/postgresql/11/bin/pg_basebackup -h {pg_leader_cluster_2} -p 5432 -U postgres -W -D /data/postgresql/11/main -Fp -Xs -P -v
-- Лидер кластера 3: запускаем базу, проверяем что LSN аналогичен с лидером кластера 2
$ pg_ctlcluster 11 main start
prodb=# select pg_last_wal_replay_lsn();
-- Лидер кластера 2 и лидер кластера 3: промоутим базы
$ /usr/lib/postgresql/11/bin/pg_ctl -D /data/11/main/ promote
-- Лидер кластера 2: инициализируем целевую базу pg v15, редактируем параметры - как минимум порт 5433
/usr/lib/postgresql/15/bin/initdb -D /data/postgresql/15/main --locale="en_US.UTF-8"
-- Лидер кластера 2: выполняем обновление pg до 15ой версии
$ time /usr/lib/postgresql/15/bin/pg_upgrade --jobs=4 --link
-b /usr/lib/postgresql/11/bin/
-B /usr/lib/postgresql/15/bin/
-d /data/11/main/
-D /data/postgresql/15/main/
-o --config_file=/etc/postgresql/11/main/postgresql.conf
-O --config_file=/etc/postgresql/15/main/postgresql.conf
-- Лидер кластера 2: запускаем базу после обновления, собираем статистику, обновляем расширения (не расписано, индивидуально), выключаем
$ pg_ctlcluster 15 main start
$ time /usr/lib/postgresql/15/bin/vacuumdb --all --analyze-in-stages --jobs 8 -p 5433
$ pg_ctlcluster 15 main stop
-- Лидер кластера 2: запускаем базу под управлением Patroni, параметры порта и прочее теперь от Patroni
$ service patroni start
$ patronictl -c /etc/patroni/patroni.yml topology
-- Лидер кластера 3: запускаем базу под управлением Patroni
$ service patroni start
$ patronictl -c /etc/patroni/patroni.yml topology
Включение каскадной логической репликации между разными версиями
Логическую репликации между кластерами включаем без копирования данных таблиц. Так как база в кластера 3 создана из базы в кластере 2 и трафик приложения не в них заходил, то данные в этих базах логически идентичны.
-- Лидер кластера 2: создаём слот логической репликации, проверяем его наличие и наличие публикации созданной ещё до начала бэкапа
prodb=# select pg_create_logical_replication_slot('prodb3_v11_slot', 'pgoutput');
prodb=# select * from pg_replication_slots;
prodb=# select * from pg_publication;
-- Лидер кластера 3: создаём подписку для репликации из лидера кластера 2 без копирования данных
prodb=# CREATE SUBSCRIPTION prodb3_v11_slot
CONNECTION 'host=master.pgcluster-prodb2.service.consul port=5432 dbname=prodb user=postgres password=XXXXXXXXXXXXXX'
PUBLICATION logical_prodb_11_15_11
with (enabled=true, copy_data=false, create_slot=false);
prodb=# select * from pg_subscription;
-- Лидер кластера 2: проверяем активность созданной репликации
prodb=# select * from pg_replication_slots;
-- Лидер кластера 2: создаём слот логической репликации в Patroni
$ patronictl -c /etc/patroni/patroni.yml edit-config
slots:
prodb3_v11_slot:
database: prodb
plugin: pgoutput
type: logical
-- Лидер кластера 1: смещаем позицию для слота логической репликации на значение полученное из pg_last_wal_replay_lsn() в кластере 2, проверяем что смещение успешно
prodb=# select pg_replication_slot_advance('prodb2_v15_slot','2866E/A02A3D90');
prodb=# select * from pg_replication_slots ;
-- Лидер кластера 2: создаём подписку для репликации из лидера кластера 1 без копирования данных
prodb=# CREATE SUBSCRIPTION prodb2_v15_slot
CONNECTION 'host=master.pgcluster-prodb.service.consul port=5432 dbname=prodb user=postgres password=XXXXXXXXXXXXXXX'
PUBLICATION logical_prodb_11_15_11
with (enabled=true, copy_data=false, create_slot=false);
prodb=# select * from pg_subscription;
-- Лидер кластера 1: проверяем активность созданной репликации
prodb=# select * from pg_replication_slots;
-- Лидер кластера 1: создаём слот логической репликации в Patroni, будет перезапуск реплик - делать в технологическое окно
$ patronictl -c /etc/patroni/patroni.yml edit-config
slots:
prodb2_v15_slot:
database: prodb
plugin: pgoutput
type: logical
-- Реплики в кластерах 2 и 3: запускаем их создание через Patroni - команды вида
$$ patronictl reinit {cluster_name} {node_name}
-- Лидеры кластеров 2 и 3: можно перестраховаться и включить READ_ONLY, может быть снят на уровне сессии
prodb=# ALTER SYSTEM SET default_transaction_read_only TO on;
prodb=# SELECT pg_reload_conf();
prodb=# SHOW default_transaction_read_only;
На данном этапе настраиваем необходимые теги, днс-имена для подключения приложения, проверяем что они доступны для подключений, данные не меняем, DDL не выполняем.
На текущий момент получили репликацию из 11-ой версии pg в 15-ую, а затем из 15-ой в 11-ую версию, то есть каскадная межкластерная межверсионная репликация.
Переключение на PostgreSQL 15ой версии
До непосредственно самого переключения билдим приложение на использование имён в подключении от кластера 2 на pg v15.
1. В приложении выставляем заглушку для обращений от пользователей.
2. Отскейливаем приложение до 0, по сути выключаем его на текущей версии
3. На лидере кластера 1 включаем режим READ_ONLY
prodb=# ALTER SYSTEM SET default_transaction_read_only TO on;
prodb=# SELECT pg_reload_conf();
4. На лидере кластера 2: Выгружаем значения последовательностей (sequnces) из лидера кластера 1 в файл
$ psql -h master.pgcluster-prodb.service.consul -U postgres -W prodb
copy (select format($$ select setval('%s.%s'::regclass, %s);$$, relnamespace::regnamespace::text, oid::regclass::text, coalesce(pg_sequence_last_value(oid), (pg_sequence_parameters(oid)).start_value)) from pg_class where relkind='S' order by 1) to '/tmp/prodb_set_sequences.sql'
5. Выключаем режим READ_ONLY на лидере кластера 2
prodb=# ALTER SYSTEM SET default_transaction_read_only TO off;
prodb=# SELECT pg_reload_conf();
prodb=# SHOW default_transaction_read_only;
6. Загружаем значения последовательностей (sequnces) в лидера кластера 2, полученных из лидера кластера 1
$ psql -d prodb -a -f '/tmp/prodb_set_sequences.sql'
7. Запуск версии приложения с использованием нового соединения
8. Убираем заглушку для трафика приложений.
Выключение и включение приложения отрабатывало достаточно быстро, потому суммарно можно уложиться в минуту. Заглушка для успокоения пользователей, что всё под контролем и ожидаемо, хотя конечно же заранее все были проинформированы.
На данный момент переключение на использование PostgreSQL v.15 произошло. Кластер 1 становится ненужным, так как данные на случай отката на ранее использованную версию PostgreSQL v.11 доступны в кластере 3. Ноды из кластера 1 сразу же начали мигрировать в кластер 2, для выдерживания разного объёма нагрузок и резервирования для разных DNS подключений.
В нашем случае до переключения на pg v.15 коллеги из датааналитики переключились на вытягивание данных из кластера 3. В итоге успешно ли будет обновление или нет - у них были бы доступны актуальные данные, плюсом проверяли корректность данных в каскадной репликации.
Через несколько дней команда разработки признала переключение успешным, что позволило снять мораторий на выкладку DDL и отключить кластер 3, предварительно переключив датааналитиков на кластер на pg v.15. При отключении кластера 3 нужно убрать слот репликации в Patroni в кластере 2, чтобы в зависимости от настроек диски не забились wal-файлами.
Можно провести работы без создания кластера 3, а пересоздавая репликацию и меняя направление из кластера 2 в кластер 1 непосредственно в момент переключения пока нет трафика ни в один из кластеров - мы не стали брать на себя такие риски, благо была возможность в виде дополнительных физических серверов.
В ходе подготовительных работ было упущение, что режим READ_ONLY включали до создания реплик в кластерах 2 и 3. В итоге когда позже произошло переключение мастера в кластере 2 получили на некоторое время не пишущего лидера, так как параметр сохранился в postgresql.auto.conf и его потребовалось срочно выключить.
Итог
Нам удалось обновить кластер PostgreSQL & Patroni & Consul из нескольких нод с версии 11 на версию 15 с кратким прерыванием работы приложения.
Мы развернули кластер отката на pg 11-ой версии, имевший в себе все актуальные изменения происходящие в кластере на pg v15.
Всегда сохраняли не менее минимально необходимого количество нод в кластере - в нашем случае три ноды.
Логическая репликация разворачивалась из физических бэкапов для существенного ускорения развертывания реплик.
Автор: iagmalov