Как обновить PostgreSQL и не потерять данные: метод минимизации простоя

Привет! Я Тимур Низамутдинов, DevOps-инженер компании «Флант». Недавно мне потребовалось обновить кластер PostgreSQL, который обрабатывает более 20 000 транзакций в секунду и состоит из мастера и реплики, с версии 13 до 16 с минимальным простоем. Помимо перехода на более актуальную версию, это решало и ряд существующих проблем, связанных с производительностью и поддержкой.

41e9c277090115e8209476f66ebf49eb.png

Например, с ростом нагрузки на систему старые серверы стали узким местом. Их нужно было заменять на более мощные машины с увеличенными объёмами дисков. Это помогает справляться с возрастающим объёмом данных и поддерживать высокую скорость обработки транзакций.

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

В этой статье я покажу, как обновлял PostgreSQL и с какими проблемами сталкивался. Обновление будет состоять из двух основных этапов:

  1. Создание новой реплики с нужной версией PostgreSQL через логическую репликацию.

  2. Перенос роли мастера на новую реплику.

Создание новой реплики

Логическая репликация

Я сразу решил создать логическую репликацию и протестировать её для текущей базы:

1) Переносим базу PostgreSQL и схему нужной базы: 
pg_dumpall --database=postgres --host=x.x.x.x --no-password --globals-only --no-privileges | psql
pg_dump --dbname name_db --host=x.x.x.x --no-password --create --schema-only | psql
2) Создаём публикацию базы на мастере:
CREATE PUBLICATION name_pub FOR ALL TABLES;
3) Cоздаём подписку на реплике:
CREATE SUBSCRIPTION name_sub CONNECTION 'host=x.x.x.x dbname=name_db' PUBLICATION name_pub;
4) Проверяем публикацию на мастере:
select * from pg_catalog.pg_publication;
5) Проверяем подписку на реплике:
SELECT * FROM pg_stat_subscription;

В итоге скорость репликации составила около 1 ГБ в минуту. При объёме базы в 3,5 ТБ перенос займёт 2–3 дня до полной синхронизации. Но пока будет происходить перелив базы, валы (WAL) для слота репликации будут накапливаться. В итоге они могут вырасти до таких размеров, что выделенное под них место заполнится, и мастер «упадёт». 

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

Физическая и логическая репликации

Черновая схема следующая:

  1. Создаю физическую реплику.

  2. Жду, когда физическая реплика дойдёт до точки синхронизации.

  3. Перевожу на логическую репликацию.

  4. Через pg_upgrade обновляю PostgreSQL до 16-й версии.

В результате тестирования физическая репликация показала скорость в районе 10 ГБ в минуту, и скорость роста валов была не такой катастрофической.

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

select pg_create_logical_replication_slot('logical_replica_slot', 'pgoutput');
select pg_replication_slot_advance('logical_replica_slot', '0/2001260');

Как определить значение LSN для продвижения логического слота репликации? После того как реплика будет переведена через promote в автономную, в логах PostgreSQL появится строка redo done at 0/2001260. Она содержит сведения о последнем применённом местоположении транзакции. В этом случае LSN будет равен 0/2001260. Следовательно, транзакции, происходящие после этой точки, будут применены к новому инстансу.

Поняли, как работает, поэтому действуем по следующему плану:

  1. Создаём физическую реплику.

  2. Останавливаем её и переводим инстанс через promote в автономный.

  3. Переводим на логическую реплику.

  4. Делаем апдейт на 16 PostgreSQL.

Предварительно протестировал обновление с 13-й на 16-ю версию, всё прошло успешно:

postgres:~$ /usr/lib/postgresql/16/bin/pg_upgrade --old-datadir=/var/lib/postgresql/13/main --new-datadir=/var/lib/postgresql/16/main  --old-bindir=/usr/lib/postgresql/13/bin  --new-bindir=/usr/lib/postgresql/16/bin  --old-options '-c config_file=/etc/postgresql/13/main/postgresql.conf' --new-options '-c config_file=/etc/postgresql/16/main/postgresql.conf' --link
Performing Consistency Checks
Checking cluster versions                                     ok
Checking database user is the install user                    ok
Checking database connection settings                         ok
Checking for prepared transactions                            ok
Checking for system-defined composite types in user tables    ok
Checking for reg* data types in user tables                   ok
Checking for contrib/isn with bigint-passing mismatch         ok
Checking for incompatible "aclitem" data type in user tables  ok
Checking for user-defined encoding conversions                ok
Checking for user-defined postfix operators                   ok
Checking for incompatible polymorphic functions               ok
Creating dump of global objects                               ok
Creating dump of database schemas                             ok
Checking for presence of required libraries                   ok
Checking database user is the install user                    ok
Checking for prepared transactions                            ok
Checking for new cluster tablespace directories               ok
If pg_upgrade fails after this point, you must re-initdb the
new cluster before continuing.
Performing Upgrade
Setting locale and encoding for new cluster                   ok
Analyzing all rows in the new cluster                         ok
Freezing all rows in the new cluster                          ok
Deleting files from new pg_xact                               ok
Copying old pg_xact to new server                             ok
Setting oldest XID for new cluster                            ok
Setting next transaction ID and epoch for new cluster         ok
Deleting files from new pg_multixact/offsets                  ok
Copying old pg_multixact/offsets to new server                ok
Deleting files from new pg_multixact/members                  ok
Copying old pg_multixact/members to new server                ok
Setting next multixact ID and offset for new cluster          ok
Resetting WAL archives                                        ok
Setting frozenxid and minmxid counters in new cluster         ok
Restoring global objects in the new cluster                   ok
Restoring database schemas in the new cluster
                                                              ok
Adding ".old" suffix to old global/pg_control                 ok
If you want to start the old cluster, you will need to remove
the ".old" suffix from /var/lib/postgresql/13/main/global/pg_control.old.
Because "link" mode was used, the old cluster cannot be safely
started once the new cluster has been started.
Linking user relation files
                                                              ok
Setting next OID for new cluster                              ok
Sync data directory to disk                                   ok
Creating script to delete old cluster                         ok
Checking for extension updates                                notice
Your installation contains extensions that should be updated
with the ALTER EXTENSION command.  The file
    update_extensions.sql
when executed by psql by the database superuser will update
these extensions.
Upgrade Complete

Переключение физической реплики на логическую:

MASTER:
\c name_db
1) Создали слот репликации:
select pg_create_logical_replication_slot('logical_replica_slot', 'pgoutput');
2) Проверили его:
select * from pg_replication_slots ;

MASTER:
\c name_db
3) Создали публикацию всех таблиц:
CREATE PUBLICATION name_db_pub FOR ALL TABLES;

REPLICA:
\c postgres
4) Сняли с реплики роль и сделали её автономной:
SELECT pg_promote();

REPLICA:
5) Поиск LSN:
cat /var/log/postgresql/postgresql-13-main.log | grep "redo done at"
search lsn ~"2B0C7/5077C0"

MASTER:
\c name_db
6) Передвинули слот логической репликации:
select pg_replication_slot_advance('logical_replica_slot', '2B0C7/5077C0');

REPLICA:
\c name_db
7) Создали подписку:
CREATE SUBSCRIPTION name_db_sub CONNECTION 'host=x.x.x.x dbname=name_db' PUBLICATION name_db_pub WITH (copy_data=false, slot_name='logical_replica_slot', create_slot=false);

Апгрейд на 16-ю версию:

1) Установка 16-й версии:
apt install postgresql-16 postgresql-16-repack
apt purge postgresql-16 postgresql-16-repack postgresql-16-repmgr
2) Проверка:
pg_lsclusters
3) Стоп PostgreSQL:
systemctl stop postgresql
(в этот момент на слоте запоминается LSN и с этого LSN слот потом продолжит работу с публикацией)
4) Логин под юзером PostgreSQL и работа по апгрейду:
su postgres
5) Проверка возможности обновления:
/usr/lib/postgresql/16/bin/pg_upgrade \
--old-datadir=/var/lib/postgresql/13/main \
--new-datadir=/var/lib/postgresql/16/main  \
--old-bindir=/usr/lib/postgresql/13/bin  \
--new-bindir=/usr/lib/postgresql/16/bin  \
--old-options '-c config_file=/etc/postgresql/13/main/postgresql.conf' \
--new-options '-c config_file=/etc/postgresql/16/main/postgresql.conf' \
--check
5.1) Подготовка старого инстанса:
pg_dump -s pghero > pghero.sql
DROP EXTENSION pg_stat_statements cascade;
DROP SCHEMA pghero cascade;
DROP DATABASE pghero;
DROP SCHEMA okmeter cascade;
DROP SCHEMA repmgr cascade;
Иначе при переносе данных на новый инстанс будут ошибки при создании представлений и прочего.
pg_restore: creating VIEW "pghero.pg_stat_activity"
pg_restore: while PROCESSING TOC:
pg_restore: from TOC entry 203; 1259 19999367 VIEW pg_stat_activity postgres
pg_restore: error: could not execute query: ERROR:  column reference "backend_type" is ambiguous
LINE 34: "pg_stat_activity"."backend_type"
6) Апгрейд путём создания жёстких ссылок на inode файлов:
/usr/lib/postgresql/16/bin/pg_upgrade \
--old-datadir=/var/lib/postgresql/13/main \
--new-datadir=/var/lib/postgresql/16/main  \
--old-bindir=/usr/lib/postgresql/13/bin  \
--new-bindir=/usr/lib/postgresql/16/bin  \
--old-options '-c config_file=/etc/postgresql/13/main/postgresql.conf' \
--new-options '-c config_file=/etc/postgresql/16/main/postgresql.conf' \
--link
7) Выход из-под юзера PostgreSQL:
exit
8) Смена портов для инстансов PostgreSQL:
nano /etc/postgresql/16/main/postgresql.conf
    port = 5433 
    # Меняем на 5432
nano /etc/postgresql/13/main/postgresql.conf
    port = 5432 
    # Меняем на 5433
Проверяем конфиг нового PostgreSQL на наличие депрекейтнутых опций.
9) Старт службы PostgreSQL:
systemctl start postgresql
10) Логин под юзером PostgreSQL:
su postgres
11) Обновление Optimizer statistics, который не переносится из старого инстанса:
/usr/lib/postgresql/16/bin/vacuumdb --all --analyze-in-stages
12) Удаление старого инстанса:
./delete_old_cluster.sh
rm -rf /etc/postgresql/13/main
13) Выход из-под юзера PostgreSQL:
exit
14) Пробуем отключить и включить старую подписку на базу мастера, если не помогло — отключаем и заводим новую. ПРИ ЭТОМ СТАРУЮ — НЕ УДАЛЯЕМ, иначе удалится слот на мастере:
ALTER SUBSCRIPTION name_db_sub DISABLE;
ALTER SUBSCRIPTION name_db_sub ENABLE;
15) Новая подписка в случае неуспеха в 14-м пункте:
ALTER SUBSCRIPTION name_db_sub DISABLE;
CREATE SUBSCRIPTION name_db_sub_2 CONNECTION 'host=x.x.x.x dbname=name_db' PUBLICATION name_db_pub WITH (copy_data=false, slot_name='logical_replica_slot', create_slot=false);
16) Проверка:
мастер:
select usename,client_addr,state,replay_lag from pg_stat_replication;
select slot_name,slot_type,active from pg_replication_slots;
реплика:
SELECT * FROM pg_stat_subscription;

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

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

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

Скорее всего, при обновлении версии обновляется и версия расширения логической репликации pgoutput, что ломает подписку. Поэтому остаётся последний вариант: завожу физическую реплику и объединяю этап смены на логическую реплику с обновлением версии.

Объединение логической репликации с обновлением версии

План:

  1. Останавливаю физическую реплику.

  2. Запоминаю LSN.

  3. Двигаю на мастере логический слот на эту LSN.

  4. Обновляю до 16-й версии.

  5. Завожу подписку на обновленной реплике на 16-й версии для базы мастера.

Получилось следующее:

1) MASTER:
\c name_db
select usename,client_addr,state,replay_lag from pg_stat_replication;
select slot_name,slot_type,active from pg_replication_slots;
2) MASTER:
\c name_db
select pg_create_logical_replication_slot('logical_replica_slot', 'pgoutput');
select * from pg_replication_slots;
!!!check slot for database?
3) MASTER:
\c name_db
CREATE PUBLICATION name_db_pub FOR ALL TABLES;
4) REPLICA:
\c postgres
SELECT pg_promote();
5) REPLICA:
Берем LSN из 13-й версии psql и продвигаем слот через pg_replication_slot_advance:
cat /var/log/postgresql/postgresql-13-main.log | grep "redo done at"
6) MASTER:
\c name_db
select pg_replication_slot_advance('logical_replica_slot', '2BB97/CF307EA0');
7) MASTER:
Удаляем старый слот потоковой реплики:
select pg_drop_replication_slot('repmgr_slot_5');
8) REPLICA:
Установка 16-й версии:
apt-get install -y postgresql-16 postgresql-16-repack postgresql-16-repmgr postgresql-client-16
9) REPLICA:
Проверка:
pg_lsclusters
10) REPLICA
Подготовка старого инстанса:
psql -U postgres -p 5432 << EOF
drop database pghero;
\connect name_db
drop extension pg_repack cascade;
drop extension pg_stat_statements cascade;
drop schema pghero cascade;
drop schema repack cascade;
drop schema okmeter cascade;
drop schema repmgr cascade;
\connect postgres
drop extension pg_repack cascade;
drop extension pg_stat_statements cascade;
drop schema pghero cascade;
drop schema repack cascade;
drop schema okmeter cascade;
drop schema repmgr cascade;
EOF
Иначе при переносе данных на новый инстанс будут ошибки при создании представлений и прочего.
pg_restore: creating VIEW "pghero.pg_stat_activity"
pg_restore: while PROCESSING TOC:
pg_restore: from TOC entry 203; 1259 19999367 VIEW pg_stat_activity postgres
pg_restore: error: could not execute query: ERROR:  column reference "backend_type" is ambiguous
LINE 34:     "pg_stat_activity"."backend_type"
11) REPLICA:
Стоп PostgreSQL:
systemctl stop postgresql
12) REPLICA:
Логин под юзером PostgreSQL и работа по апгрейду:
su postgres
13) REPLICA:
Проверка возможности обновления:
/usr/lib/postgresql/16/bin/pg_upgrade \
--old-datadir=/var/lib/postgresql/13/main \
--new-datadir=/var/lib/postgresql/16/main  \
--old-bindir=/usr/lib/postgresql/13/bin  \
--new-bindir=/usr/lib/postgresql/16/bin  \
--old-options '-c config_file=/etc/postgresql/13/main/postgresql.conf' \
--new-options '-c config_file=/etc/postgresql/16/main/postgresql.conf' \
--check
14) REPLICA:
Апгрейд путём создания жёстких ссылок на inode файлов:
/usr/lib/postgresql/16/bin/pg_upgrade \
--old-datadir=/var/lib/postgresql/13/main \
--new-datadir=/var/lib/postgresql/16/main  \
--old-bindir=/usr/lib/postgresql/13/bin  \
--new-bindir=/usr/lib/postgresql/16/bin  \
--old-options '-c config_file=/etc/postgresql/13/main/postgresql.conf' \
--new-options '-c config_file=/etc/postgresql/16/main/postgresql.conf' \
--link
15) REPLICA:
Выход из-под юзера PostgreSQL:
exit
16) REPLICA:
Правка конфига 16-й версии и смена портов:
rsync -av /etc/postgresql/13/main/ /etc/postgresql/16/main/
sed -i '/stats_temp_directory/d' /etc/postgresql/16/main/postgresql.conf
sed -i '/vacuum_defer_cleanup_age/d' /etc/postgresql/16/main/postgresql.conf
sed -i 's/pg_stat_statements,pg_repack/pg_stat_statements/' /etc/postgresql/16/main/postgresql.conf
sed -i 's/\/13\//\/16\//' /etc/postgresql/16/main/postgresql.conf
sed -i 's/5433/5432/' /etc/postgresql/16/main/postgresql.conf
sed -i 's/13-main/16-main/' /etc/postgresql/16/main/postgresql.conf
sed -i 's/13\/main/16\/main/' /etc/postgresql/16/main/postgresql.conf
sed -i 's/5432/5433/' /etc/postgresql/13/main/postgresql.conf
17) REPLICA:
Старт службы PostgreSQL:
systemctl start postgresql
18) REPLICA:
Логин под юзером PostgreSQL:
su postgres
19) REPLICA:
Обновление Optimizer statistics, который не переносится из старого инстанса:
/usr/lib/postgresql/16/bin/vacuumdb --all --analyze-in-stages
20) REPLICA:
Удаление старого инстанса:
./delete_old_cluster.sh
rm -rf /etc/postgresql/13/main
21) REPLICA:
Выход из-под юзера PostgreSQL:
exit
22) REPLICA:
\c name_db
CREATE SUBSCRIPTION name_db_sub CONNECTION 'host=x.x.x.x dbname=name_db' PUBLICATION name_db_pub WITH (copy_data=false, slot_name='logical_replica_slot', create_slot=false);

Я настроил потоковую реплику и дождался, пока она синхронизируется с основной базой данных. Далее создал логический слот для репликации и публикацию для нужной базы, чтобы данные могли передаваться. Потом повысил статус реплики и переместил логический слот по LSN. В конце обновил реплику, настроил 16-ю версию PostgreSQL и создал подписку на реплике.

И такая схема сработала! Теперь у нас есть рабочий сценарий создания логической реплики на новом PostgreSQL из потоковой репликации.

Перенос роли мастера на новую реплику

Дальше план был простой и касался смены роли мастера на обновлённую реплику:

  1. Выполняем план запросов на новой логической реплике для «прогрева» кэша реплики.

  2. Переводим мастер в состояние read_only и дожидаемся синхронизации с репликой.

  3. Отключаем на реплике подписку на обновления, то есть отключаем репликацию.

  4. Указываем балансировщикам нагрузки ходить на реплику.

  5. Проверяем трафик.

  6. Удаляем подписку.

  7. Останавливаем PostgreSQL на мастере.

  8. Обновляем мастер и заводим потоковую реплику с нового мастера на версии 16.

План переключения мастера на обновлённый PostgreSQL:

0) REPLICA:
vacuumdb --all --analyze-in-stages
1) MASTER:
CHECKPOINT;
ALTER SYSTEM SET default_transaction_read_only TO on;
SELECT pg_reload_conf();
SHOW default_transaction_read_only;
    1.1) replay_lag доводим до 0
    MASTER:
    select usename,client_addr,state,replay_lag from pg_stat_replication;
2) REPLICA:
\c name_db
ALTER SUBSCRIPTION name_db_sub DISABLE;
3) CLUSTER:
Правка балансировщиков нагрузки для отправки трафика на новый мастер.
4) Проверка PostgreSQL со всех сторон:
    4.1) ОТКАТ:
        4.1.1) MASTER:
        ALTER SYSTEM SET default_transaction_read_only TO off;
        SELECT pg_reload_conf();
        4.1.1) CLUSTER:
        Возврат старых адресов в балансировщике нагрузки, ведущих на старый мастер:
        4.1.2) REPLICA:
        \c name_db
        ALTER SUBSCRIPTION name_db_sub ENABLE;
5) REPLICA:
\c name_db
DROP SUBSCRIPTION name_db_sub;
6) MASTER:
\c name_db
DROP PUBLICATION name_db_pub;
\c name_db
select pg_drop_replication_slot('repmgr_slot_5');
select pg_drop_replication_slot('logical_replica_slot');
exit;
sudo systemctl stop postgresql

План отработал. Время простоя составило всего 10–15 секунд, и это произошло только для записи данных. Простой был связан с выполнением операции чекпоинта и переключением трафика с одного сервера на другой.

Важно отметить, что при удалении подписки также удаляется и логический слот. Поэтому сначала я отключил слот, чтобы в случае необходимости можно было выполнить откат.

После этого нужно было обновить SEQUENCE:

ALTER SEQUENCE id_seq RESTART WITH 1698070;

Для чего это? В большинстве таблиц используется первичный ключ (primary key) в формате UUID v4. Но есть таблицы, где первичный ключ — это autoincrement, зависящий от SEQUENCE. В этих таблицах вставка новых записей не срабатывает, потому что SEQUENCE не был перенесен.

Итоги

Мне удалось обновить кластер PostgreSQL с версии 13 до 16 с минимальным простоем и высокой доступностью. Несмотря на некоторые сложности, такие как необходимость в управлении LSN и проблемы с подписками, получилось синхронизировать данные и завершить обновление без потерь. Это стало возможным благодаря объединению этапа смены на логическую реплику с обновлением версии.

Отмечу, что обновление кластера PostgreSQL — это не просто техническая необходимость, а стратегически важный шаг. Я учёл рост данных и транзакций, с которыми бы совсем скоро перестали справляться старые серверы, а также понимал, что обновление откроет доступ к дополнительным возможностям для упрощения разработки.

P. S.

Читайте также в нашем блоге:

© Habrahabr.ru