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

от автора

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

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

А ещё переход на свежую версию 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.

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


ссылка на оригинал статьи https://habr.com/ru/articles/866250/