Перейти к содержимому

Администрирование PostgreSQL. Резервное копирование и репликация

Логическое и физическое резервное копирование, архивирование WAL, физическая и логическая репликация, архитектуры кластеров.

Администрирование PostgreSQL. Резервное копирование и репликация

Логическое и физическое резервное копирование, архивирование WAL, физическая и логическая репликация, архитектуры кластеров.

Курс сохраняет официальный порядок тем. Русские материалы извлечены из PDF в читаемый нативный Markdown для дальнейшей редакторской работы.

Состояние материалов: 官方 PDF 已提取为原生 Markdown

Базовый язык: Базовый язык: English. Текущий английский архив используется как базовый; русская версия сохранена для сверки глав.

О курсе

Логическое и физическое резервное копирование, архивирование WAL, физическая и логическая репликация, архитектуры кластеров.

Состояние материалов

ЯзыкТекущий архивВерсияТемыСтраница курса
EnglishDBA3-handouts-16-en-20260917.zip16-202609179HTTP 404
РусскийDBA3-handouts-16.zip169HTTP 200

Содержание

Дополнительные материалы

  • dba3_new_features.pdf
  • dba3_student_guide.pdf

Редакторский статус

Русский комплект доступен в виде нативного Markdown. Если английский комплект не найден, английский маршрут оставлен для будущего источника.

00 Занятие 00: Резервное копирование и репликация / PostgreSQL 16

Исходная страница в нативном Markdown: Резервное копирование и репликация / PostgreSQL 16
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_00_introduction.html Archive: DBA3-handouts-16.zip Страниц PDF: 10

Page 1: Резервное копирование и репликация / PostgreSQL 16

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Администраторам

Source slide page 2: Администраторам{#source-figure-002 caption=“Source slide page 2: Администраторам”}

  • DBA2, 4 дня

Комментарии преподавателя

Для прохождения всех курсов необходимо знание основ языка SQL. Курса по SQL в нашей линейке нет, но имеется множество книг и образовательных ресурсов для освоения SQL. Мы рекомендуем книгу Евгения Моргунова «PostgreSQL. Основы языка SQL»:

https://postgrespro.ru/education/books/sqlprimer

Для администраторов мы предлагаем следующие курсы.

Курс DBA1 – базовый, он дает сведения об архитектуре PostgreSQL, об установке, настройке и управлении сервером. Приводятся обзоры разграничения доступа, резервного копирования и репликации.

В курсе DBA2 обсуждается настройка конфигурационных параметров исходя из понимания внутренней организации сервера; говорится о мониторинге сервера с использованием обратной связи для итеративной настройки параметров.

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

Курс DBS рассказывает про настройку подключений клиентов к СУБД и про управление доступом внутри самой СУБД.

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

В курсе PGPRO рассматриваются дополнительные возможности, которые предоставляет СУБД Postgres Pro Enterprise.

Page 3: Разработчикам

Source slide page 3: Разработчикам{#source-figure-003 caption=“Source slide page 3: Разработчикам”}

  • DEV2, 4 дня

Комментарии преподавателя

Для разработчиков серверной части приложений мы предлагаем следующие курсы.

Базовым курсом для разработчиков является курс DEV1, в котором даются общие сведения об архитектуре PostgreSQL и использовании основных объектов базы данных; изучается программирование на стороне сервера на языках SQL и PL/pgSQL с использованием основных типов данных (включая составной тип и массивы), а также рассматривается взаимодействие с клиентской частью.

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

Курсы DBS, QPT и PGPRO являются общими для разработчиков и администраторов.

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

https://postgrespro.ru/education/courses

Page 4: О курсе DBA3

Продолжительность: 2 дня

  • Предварительные знания
    • основы SQL
    • владение ОС Unix
    • знакомство с архитектурой PostgreSQL в объеме курса DBA1
  • Какие навыки будут получены
    • выполнение резервного копирования различными способами
    • настройка сервера для физической и логической репликации
    • знакомство со сценариями использования репликации
    • представление о способах построения кластеров

Комментарии преподавателя

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

Page 5: Используемые материалы

Подготовленная виртуальная машина

  • ОС Xubuntu 24.04
  • PostgreSQL 16 (три кластера)
  • документация на русском языке
  • Учебные материалы
    • презентации, демонстрации, практические задания и их решение
    • (в форматах html и pdf)

Комментарии преподавателя

Если вы проходите курс самостоятельно, обязательно начните со знакомства с Руководством слушателя. В числе прочего в нем написано, где скачать и как использовать виртуальную машину курса и прочие материалы. Все материалы курса, включая Руководство слушателя, доступны по адресу: https://postgrespro.ru/education/courses/DBA3

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

Учебные материалы (презентации, демонстрации, практические задания и их решения) доступны в двух форматах. Формат html удобен для онлайн-работы, он позволяет копировать фрагменты текста и кода. Формат pdf разбит на страницы и удобен для печати.

Page 6: Предварительные настройки

В этом курсе мы будем использовать три экземпляра PostgreSQL, названные alpha, beta и gamma:

student$ sudo pg_ctlcluster 16 alpha start
student$ sudo pg_ctlcluster 16 beta start
student$ sudo pg_ctlcluster 16 gamma start

В демонстрациях команды, выполняемые на каждом из серверов, выделены и отличаются приглашением. Все практические задания выполняются под пользователем ОС student. В СУБД создан одноименный пользователь и база данных, чтобы psql можно было запускать без параметров. Сервер alpha работает на стандартном порту 5432:

student$ psql
α=> SHOW server_version;
           server_version
------------------------------------
 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
(1 row)

Сервер beta — на порту 5433. Для подключения в psql достаточно указать только номер порта:

student$ psql -p 5433
β=> SHOW server_version;
           server_version
------------------------------------
 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
(1 row)

Сервер gamma — на порту 5434:

student$ psql -p 5434
γ=> SHOW server_version;
           server_version
------------------------------------
 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
(1 row)

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

α=> \du student
                  List of roles
 Role name |             Attributes
-----------+-------------------------------------
 student   | Create role, Create DB, Replication
α=> \drg student
                     List of role grants
 Role name |       Member of        |   Options    | Grantor
-----------+------------------------+--------------+----------
 student   | pg_checkpoint          | INHERIT, SET | postgres
 student   | pg_create_subscription | INHERIT, SET | postgres
 student   | pg_monitor             | INHERIT, SET | postgres
 student   | pg_read_server_files   | INHERIT, SET | postgres
 student   | pg_write_server_files  | INHERIT, SET | postgres
(5 rows)

Также student имеет привилегии на некоторые объекты, которыми по умолчанию обладает только суперпользователь. Это право изменять параметры:

Page 7: Page 7

α=> SELECT parname, (acl).privilege_type
FROM (
  SELECT parname, aclexplode(paracl) acl
  FROM pg_parameter_acl
)
WHERE (acl).grantee = user::regrole;
           parname            | privilege_type
------------------------------+----------------
 archive_command              | ALTER SYSTEM
 archive_mode                 | ALTER SYSTEM
 archive_timeout              | ALTER SYSTEM
 checkpoint_timeout           | ALTER SYSTEM
 hot_standby_feedback         | ALTER SYSTEM
 listen_addresses             | ALTER SYSTEM
 log_recovery_conflict_waits  | ALTER SYSTEM
 max_connections              | ALTER SYSTEM
 max_slot_wal_keep_size       | ALTER SYSTEM
 max_standby_streaming_delay  | ALTER SYSTEM
 max_wal_size                 | ALTER SYSTEM
 min_wal_size                 | ALTER SYSTEM
 restore_command              | ALTER SYSTEM
 synchronous_commit           | ALTER SYSTEM
 synchronous_standby_names    | ALTER SYSTEM
 wal_keep_size                | ALTER SYSTEM
 wal_level                    | ALTER SYSTEM
 wal_receiver_status_interval | ALTER SYSTEM
(18 rows)

И право выполнять системные функции (оно выдано в базе template1 и, следовательно, во всех базах, созданных командой CREATE DATABASE без указания шаблона):

α=> SELECT proname, (acl).privilege_type
FROM (
  SELECT proname, aclexplode(proacl) acl
  FROM pg_proc
)
WHERE (acl).grantee = user::regrole;
            proname             | privilege_type
--------------------------------+----------------
 pg_hba_file_rules              | EXECUTE
 pg_read_binary_file            | EXECUTE
 pg_stat_file                   | EXECUTE
 pg_stat_reset_replication_slot | EXECUTE
 pg_log_standby_snapshot        | EXECUTE
 pg_switch_wal                  | EXECUTE
 pg_wal_replay_resume           | EXECUTE
 pg_ls_dir                      | EXECUTE
 pg_read_binary_file            | EXECUTE
(9 rows)

Локальные подключения любых пользователей разрешены без ввода пароля:

α=> \c - postgres
You are now connected to database "student" as user "postgres".
α=> SELECT type, database, user_name, auth_method FROM pg_hba_file_rules();
 type  |   database    | user_name |  auth_method
-------+---------------+-----------+---------------
 local | {all}         | {all}     | trust
 host  | {all}         | {all}     | scram-sha-256
 host  | {all}         | {all}     | scram-sha-256
 local | {replication} | {all}     | trust
 host  | {replication} | {all}     | scram-sha-256
 host  | {replication} | {all}     | scram-sha-256
(6 rows)

Page 8: Организационные вопросы

День: ~8 академических часов + обед (1час)

  • Каждая тема, как правило, состоит из
    • презентации и демонстрации: ~40–70 мин
    • практических заданий: ~30–40 мин, включая перерыв

Page 9: День 1

Резервное копирование

    1. Логическое резервирование
    1. Базовая резервная копия
    1. Архив журнала предзаписи
  • Репликация
      1. Физическая репликация

Page 10: День 2

Репликация (продолжение)

    1. Переключение на реплику 06. Логическая репликация 07. Сценарии использования Кластерные технологии
      1. Обзор

01 Занятие 01: Резервное копирование / Логическое резервирование

Исходная страница в нативном Markdown: Резервное копирование / Логическое резервирование
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_01_backup_logical.html Archive: DBA3-handouts-16.zip Страниц PDF: 33

Page 1: Резервное копирование / Логическое резервирование

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Понятие логической резервной копии
  • Копирование и восстановление отдельных таблиц
  • Копирование и восстановление баз данных
  • Копирование и восстановление кластера

Page 3: Логическая копия

Команды SQL для создания объектов и наполнения данными

    • можно сделать копию отдельного объекта или отдельной базы
    • можно восстановиться на другой версии или архитектуре
    • (не требуется двоичная совместимость)
  • − невысокая скорость работы
  • − восстановление только на момент создания резервной копии

Комментарии преподавателя

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

Команды можно выполнить на другой версии СУБД или даже на другой платформе и архитектуре, так как не требуется двоичная совместимость.

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

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

https://postgrespro.ru/docs/postgresql/16/backup-dump

Page 4: SQL-команда COPY

Source slide page 4: SQL-команда COPY{#source-figure-004 caption=“Source slide page 4: SQL-команда COPY”}

YKS   Якутск      (129.770
MJZ   Мирный      (114.039
KHV   Хабаровск-Новый   (1
PKC   Елизово     (158.453
UUS   Хомутово    (142.718
VVO   Владивосток       (1
LED   Пулково     (30.2625
KGD   Храброво    (20.5925
KEJ   Кемерово    (86.1072
CEK   Челябинск   (61.5033
MQF   Магнитогорск      (5
PEE   Пермь       (56.0211
SGC   Сургут      (73.4018
  • файл в ФС сервера, принадлежащий владельцу экземпляра PostgreSQL можно выбрать столбцы или использовать произвольный запрос при восстановлении строки добавляются к имеющимся в таблице

Комментарии преподавателя

Команда COPY TO записывает содержимое таблицы или результат запроса в файл или поток вывода. Строки представлений или секционированных таблиц можно скопировать с помощью запроса.

Для использования COPY TO требуется роль pg_write_server_file.

Команда COPY FROM добавляет в таблицу строки из файла или стандартного потока ввода. Она работает существенно быстрее, чем аналогичные команды INSERT. Следить за процессом копирования можно в представлении pg_stat_progress_copy.

Для COPY FROM требуется роль pg_read_server_files (а для чтения потока ввода — pg_execute_server_program).

Тонкость: при выполнении команды COPY FROM не применяются правила (rules), хотя ограничения целостности и триггеры выполняются. Если столбец таблицы при вставке должен получать значения, генерируемые последовательностью, но COPY заполняет этот столбец явно, последовательность не будет изменять свое состояние (счетчик).

Оба варианта команды COPY позволяют выбрать столбцы таблицы, а также указать такие параметры, как:

● FORMAT — формат данных (текстовый, CSV или двоичный);

● DELIMITER — разделитель полей;

● NULL — текстовое представление NULL;

● ENCODING — кодировку;

● HEADER — заголовок, описывающий столбцы. https://postgrespro.ru/docs/postgresql/16/sql-copy

Page 5: Команда psql \copy

Source slide page 5: Команда psql \copy{#source-figure-005 caption=“Source slide page 5: Команда psql \copy”}

YKS   Якутск      (129.770
MJZ   Мирный      (114.039
KHV   Хабаровск-Новый   (1
PKC   Елизово     (158.453
UUS   Хомутово    (142.718
VVO   Владивосток       (1
LED   Пулково     (30.2625
KGD   Храброво    (20.5925
KEJ   Кемерово    (86.1072
CEK   Челябинск   (61.5033
MQF   Магнитогорск      (5
PEE   Пермь       (56.0211
SGC   Сургут      (73.4018
  • файл в ФС клиента и доступен пользователю ОС, запустившему psql происходит пересылка данных между клиентом и сервером синтаксис и возможности аналогичны команде COPY

Комментарии преподавателя

В psql существует метакоманда \copy — вариант команды COPY с аналогичным синтаксисом, — выполняющаяся на стороне клиента.

Файл должен быть доступен пользователю операционной системы, запустившему psql. Обращение к файлу происходит на клиенте, а на сервер передается только содержимое.

https://postgrespro.ru/docs/postgresql/16/app-psql#APP-PSQL-META-COM MANDS-COPY

Page 6: Команда COPY

Создадим базу данных и таблицу.

α=> CREATE DATABASE backup_logical;
CREATE DATABASE
α=> \c backup_logical
You are now connected to database "backup_logical" as user "student".
α=> CREATE TABLE t(
  id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  s text
);
CREATE TABLE
α=> INSERT INTO t(s) VALUES ('Привет, мир!'), (''), (NULL);
INSERT 0 3
α=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Выведем содержимое таблицы в терминал:

α=> COPY t TO stdout;
1
Привет, мир!
2
3
\N

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

α=> COPY t TO stdout WITH (NULL '<NULL>', DELIMITER ',');
1,Привет\, мир!
2,
3,<NULL>

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

α=> COPY (SELECT * FROM t WHERE s IS NOT NULL) TO stdout;
1
Привет, мир!
2

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

α=> COPY t TO stdout WITH (FORMAT csv);
1,"Привет, мир!"
2,""
3,

Аналогично работает и ввод данных из файла или с терминала.

α=> TRUNCATE TABLE t;
TRUNCATE TABLE

Но при вводе с терминала требуется маркер конца файла — обратная косая черта с точкой; в обычном файле он не нужен. Чтобы данные загрузились, при вводе надо указать те же параметры, что были указаны при выводе.

Page 7: Page 7

При загрузке также можно указать условие:

α=> COPY t FROM stdin WHERE id != 2;
1
Привет, мир!
2
3
\N
\.
COPY 2

Вот что загрузилось в таблицу (для наглядности настроим в psql вывод неопределенных значений):

α=> \pset null '\\N'
Null display is "\N".
α=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  3 | \N
(2 rows)

Загрузим все строки:

α=> TRUNCATE TABLE t;
TRUNCATE TABLE
α=> COPY t FROM stdin;
1
Привет, мир!
2
3
\N
\.
COPY 3
α=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 | \N
(3 rows)

Page 8: Копия базы данных

Source slide page 8: Копия базы данных{#source-figure-008 caption=“Source slide page 8: Копия базы данных”}

--
-- PostgreSQL database dum
--
-- Dumped from database ve
-- Dumped by pg_dump versi
SET statement_timeout = 0;
SET lock_timeout = 0;
SET
idle_in_transaction_sessio
SET client_encoding = 'UTF
SET standard_conforming_st
  • формат: команды SQL при выгрузке можно выбрать отдельные объекты базы данных новая база должна быть создана из шаблона template0 заранее должны быть созданы роли и табличные пространства после загрузки имеет смысл выполнить ANALYZE

Комментарии преподавателя

Для создания полноценной резервной копии базы данных используется утилита pg_dump.

Если не указать имя файла (-f, –file), то утилита выведет результат в стандартный поток вывода.

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

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

Следует иметь в виду, что базу данных для восстановления надо создавать из шаблона template0, так как все изменения, сделанные в template1, попадут в резервную копию.

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

После восстановления базы данных имеет смысл выполнить команду ANALYZE — она соберет статистику, необходимую оптимизатору для планирования запросов.

https://postgrespro.ru/docs/postgresql/16/app-pgdump

Page 9: Утилита pg_dump

При запуске без дополнительных параметров утилита pg_dump выдает команды SQL, создающие все объекты в текущей базе данных. Чтобы получить команды, позволяющие восстановить копию на системе с другим набором ролей, могут пригодиться ключи: -O, –no-owner — не генерировать команды для установки владельца объектов; -x, –no-acl — не генерировать команды для установки привилегий.

student$ pg_dump -d backup_logical --no-acl
--
-- PostgreSQL database dump
--
\restrict 9SVmaQgVs0mNXwAdBl23jRpxukDiAurf2WaDlfki9mmjnWo9zicdFaR4DvxXuxG
-- Dumped from database version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
-- Dumped by pg_dump version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: t; Type: TABLE; Schema: public; Owner: student
--
CREATE TABLE public.t (
    id integer NOT NULL,
    s text
);
ALTER TABLE public.t OWNER TO student;
--
-- Name: t_id_seq; Type: SEQUENCE; Schema: public; Owner: student
--
ALTER TABLE public.t ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
    SEQUENCE NAME public.t_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1
);
--
-- Data for Name: t; Type: TABLE DATA; Schema: public; Owner: student
--
COPY public.t (id, s) FROM stdin;
1
Привет, мир!
2
3
\N
\.
--

Page 10: Page 10

-- Name: t_id_seq; Type: SEQUENCE SET; Schema: public; Owner: student
--
SELECT pg_catalog.setval('public.t_id_seq', 3, true);
--
-- Name: t t_pkey; Type: CONSTRAINT; Schema: public; Owner: student
--
ALTER TABLE ONLY public.t
    ADD CONSTRAINT t_pkey PRIMARY KEY (id);
--
-- PostgreSQL database dump complete
--
\unrestrict 9SVmaQgVs0mNXwAdBl23jRpxukDiAurf2WaDlfki9mmjnWo9zicdFaR4DvxXuxG

Видно, что pg_dump создал таблицу t и заполнил ее с помощью уже рассмотренной нами команды COPY. Ключ –column-inserts позволяет использовать команды INSERT, но загрузка будет работать существенно дольше. Логическая резервная копия в формате plain может содержать злонамеренно вставленные метакоманды, например \gexec. Чтобы избежать проблем при выполнении скрипта в psql, pg_dump добавляет в его начало метакоманду \restrict с последующим ключом. Эта команда включает ограниченный режим, в котором psql игнорирует все метакоманды, кроме команды \unrestrict с таким же ключом, отменяющей ограниченный режим. Эти ключи полезны для выгрузки и загрузки данных частями: -s, –schema-only — выгрузить только определения объектов без данных; -a, –data-only — выгрузить только данные, без создания объектов. Удобны, если восстанавливать копию на системе, в которой уже есть данные (и наоборот, на чистой системе): -c, –clean — генерировать команды DROP для создаваемых объектов; -C, –create — генерировать команды создания БД и подключения к ней. Важный момент: в выгрузку попадают и изменения, сделанные в шаблонной БД template1. Поэтому восстанавливать резервную копию лучше на базе данных, созданной из template0. При использовании ключа –create это учитывается автоматически:

student$ pg_dump --create -d backup_logical | grep 'CREATE DATABASE'
CREATE DATABASE backup_logical WITH TEMPLATE = template0 ENCODING = 'UTF8'
LOCALE_PROVIDER = libc LOCALE = 'en_US.UTF-8';

Существуют ключи для выбора объектов, которые должны попасть в резервную копию: -n, –schema — шаблон для имен схем; -t, –table — шаблон для имен таблиц. И наоборот, включить в копию все, кроме указанного: -N, –exclude-schema — шаблон для имен схем; -T, –exclude-table — шаблон для имен таблиц. Например, восстановим таблицу t в другой базе данных на другом сервере.

student$ psql -p 5433
β=> CREATE DATABASE backup_logical_1;
CREATE DATABASE
student$ pg_dump --table=t -d backup_logical | psql -p 5433 -d backup_logical_1
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET

Page 11: Page 11

SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
COPY 3
 setval
--------
      3
(1 row)
ALTER TABLE
β=> \c backup_logical_1
You are now connected to database "backup_logical_1" as user "student".
β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Page 12: Формат custom

Source slide page 12: Формат custom{#source-figure-012 caption=“Source slide page 12: Формат custom”}

;
; Archive created at 2017-
;     dbname: demo
;     TOC Entries: 146
;     Compression: -1
;     Dump Version: 1.12-0
;     Format: CUSTOM
;
; Selected TOC Entries:
;
2297; 1262 475453 DATABASE
5; 2615 475454 SCHEMA - bo
2298; 0 0 COMMENT - SCHEMA
  • внутренний формат с оглавлением отдельные объекты базы данных можно выбрать на этапе восстановления возможна загрузка в несколько параллельных потоков

Комментарии преподавателя

Утилита pg_dump поддерживает несколько форматов резервной копии. По умолчанию это plain — простые команды для psql.

Формат custom (-F c, –format=custom) создает резервную копию в специальном формате, содержащем не только объекты, но и оглавление. Наличие оглавления позволяет выбирать объекты для восстановления не при создании копии, а непосредственно при восстановлении. Файл формата custom по умолчанию сжат.

Для восстановления потребуется другая утилита — pg_restore. Она читает файл и преобразует его в команды psql. Если не указать явно имя базы данных (в ключе -d), то команды будут выведены в стандартный поток вывода. Если же база данных указана — утилита соединится с этой БД и выполнит команды без участия psql.

Чтобы восстановить только часть объектов, можно воспользоваться одним из двух подходов. Во-первых, можно ограничить объекты аналогично тому, как они ограничиваются в pg_dump. Вообще, pg_restore использует многие параметры утилиты pg_dump.

Во-вторых, можно получить из оглавления список объектов, содержащихся в резервной копии (ключ –list). Затем этот список можно отредактировать вручную, удалив ненужное и передать измененный список на вход pg_restore (ключ –use-list).

Загрузку можно выполнить параллельно, указав количество потоков ключом -j — это еще одно преимущество формата custom.

https://postgrespro.ru/docs/postgresql/16/app-pgdump

Page 13: Утилита pg_dump — формат custom

Source slide page 13: Утилита pg_dump — формат custom{#source-figure-013 caption=“Source slide page 13: Утилита pg_dump — формат custom”}

Серьезное ограничение обычного формата (plain) состоит в том, что выбирать объекты нужно в момент выгрузки. Формат custom позволяет сначала сделать полную копию, а выбирать объекты уже при загрузке.

student$ pg_dump --format=custom -d backup_logical -f /home/student/tmp/backup_logical.custom

Для восстановления объектов из такой копии предназначена утилита pg_restore. Повторим восстановление таблицы t:

β=> DROP TABLE t;
DROP TABLE
student$ pg_restore --table=t -p 5433 -d backup_logical_1 --no-acl
/home/student/tmp/backup_logical.custom

Формат резервной копии указывать не обязательно — утилита распознает его сама. Утилита pg_restore понимает те же ключи для фильтрации объектов, что и pg_dump, и даже больше: -I, –index — загрузить определенные индексы; -P, –function — загрузить определенные функции; -T, –trigger — загрузить определенные триггеры. Проверим, как восстановилась таблица:

β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Еще один пример: восстановим целиком исходную базу данных backup_logical на другом сервере.

student$ pg_restore --create -p 5433 -d postgres --no-acl /home/student/tmp/backup_logical.custom

Здесь мы указали БД postgres, но могли указать любую — утилита сама создаст нужную БД и тут же переключится в нее. Проверим:

β=> \c backup_logical
You are now connected to database "backup_logical" as user "student".
β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Page 14: Формат directory

Source slide page 14: Формат directory{#source-figure-014 caption=“Source slide page 14: Формат directory”}

каталог с оглавлением и отдельными файлами на каждый объект базы отдельные объекты базы данных можно выбрать на этапе восстановления и выгрузка, и загрузка возможны в несколько параллельных потоков

Комментарии преподавателя

Еще один формат резервной копии — directory. В таком случае будет создан не один файл, а каталог, содержащий объекты и оглавление. По умолчанию файлы внутри каталога будут сжаты.

Преимущество перед форматом custom состоит в том, что такая резервная копия может создаваться параллельно в несколько потоков (количество указывается в ключе -j, –jobs).

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

https://postgrespro.ru/docs/postgresql/16/functions-admin#FUNCTIONS-SN APSHOT-SYNCHRONIZATION

Восстановление, как и для формата custom, возможно в несколько потоков.

В остальном возможности по работе с форматом directory не отличаются от ранее рассмотренных: поддерживаются те же ключи и подходы.

Page 15: Утилита pg_dump — формат directory

Формат directory интересен тем, что позволяет выгружать данные в несколько параллельных потоков.

student$ pg_dump --format=directory \
  --jobs=2 \
  -d backup_logical \
  -f /home/student/tmp/backup_logical.directory

При этом гарантируется согласованность данных: все параллельные потоки будут использовать один и тот же снимок данных. Заглянем внутрь каталога:

student$ ls -l /home/student/tmp/backup_logical.directory
total 12
-rw-rw-r-- 1 student student   60 апр 10 17:59 3442.dat.gz
-rw-rw-r-- 1 student student 4985 апр 10 17:59 toc.dat

В нем находится файл оглавления и по одному файлу на каждый выгружаемый объект (у нас он всего один):

student$ zcat /home/student/tmp/backup_logical.directory/3442.dat.gz
1
Привет, мир!
2
3
\N
\.

Для восстановления из резервной копии предварительно отключимся от базы данных backup_logical:

β=> \q

В команду восстановления добавляем ключ –clean, который генерирует команду удаления существующей БД:

student$ pg_restore --clean \
  --create \
  --jobs=2 \
  -p 5433 \
  -d postgres /home/student/tmp/backup_logical.directory \
  --no-acl
student$ psql --pset null='\N' -p 5433 -d backup_logical -c 'SELECT * FROM t'
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 | \N
(3 rows)

Page 16: Сравнение форматов

plain custom directory tar

  • утилита для восстановления psql pg_restore
  • сжатие gzip, lz4 или zstd
  • выборочное восстановление да да да
  • параллельное резервирование да
  • параллельное восстановление да да

Комментарии преподавателя

В приведенной таблице разные форматы сравниваются с точки зрения предоставляемых ими возможностей.

Имеется и четвертый формат — tar. Он не рассматривался, так как не привносит ничего нового и не дает преимуществ перед другими форматами. Фактически он соответствует созданию tar-файла из каталога в формате directory, но не поддерживает сжатие и параллелизм.

Page 17: Копия кластера БД

Source slide page 17: Копия кластера БД{#source-figure-017 caption=“Source slide page 17: Копия кластера БД”}

--
-- PostgreSQL database clu
--
SET default_transaction_re
SET client_encoding = 'UTF
SET standard_conforming_st
--
-- Roles
--
CREATE ROLE postgres;
  • формат: команды SQL выгружает весь кластер, включая роли и табличные пространства пользователь должен иметь доступ ко всем объектам кластера не поддерживает параллельную выгрузку

Комментарии преподавателя

Чтобы создать резервную копию всего кластера, включая роли и табличные пространства, можно воспользоваться утилитой pg_dumpall.

Поскольку pg_dumpall требуется доступ ко всем объектам всех БД, имеет смысл запускать ее от имени суперпользователя. Утилита по очереди подключается к каждой БД кластера и выгружает информацию с помощью pg_dump. Кроме того, она сохраняет и данные, относящиеся к кластеру в целом.

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

Результатом работы pg_dumpall является скрипт для psql. Другие форматы не поддерживаются. Это означает, что pg_dumpall не поддерживает параллельную выгрузку данных, что может оказаться проблемой при больших объемах данных. В таком случае можно воспользоваться ключом –globals-only, чтобы выгрузить только роли и табличные пространства, а сами базы данных выгрузить отдельно с помощью pg_dump в параллельном режиме.

https://postgrespro.ru/docs/postgresql/16/app-pg-dumpall

Page 18: Утилита pg_dumpall

Утилита pg_dump годится для выгрузки одной базы данных, но никогда не выгружает общие объекты кластера БД, такие, как роли и табличные пространства. Чтобы сделать полную копию кластера, нужна утилита pg_dumpall. Утилиты pg_dumpall, pg_dump и pg_restore не требуют каких-то отдельных привилегий, но у выполняющей их роли должны быть привилегии на чтение (создание) всех затронутых объектов. Утилитой pg_dump может, например, пользоваться владелец базы данных. Но поскольку для копирования кластера надо иметь доступ ко всем БД, мы выполняем pg_dumpall под суперпользовательской ролью.

student$ pg_dumpall --clean -U postgres -f /home/student/tmp/alpha.sql

В копию кластера попадают: команды, которые выгружает pg_dump для каждой базы; команды для общих объектов кластера (ролей и табличных пространств). Команды для общих объектов можно получить отдельно:

student$ pg_dumpall --clean --globals-only -U postgres
--
-- PostgreSQL database cluster dump
--
\restrict 3es50MMtas3DuQEICBlRybTIYFuQo6UjJ6QSte0gHU6YC3Cu9FPpqeA5bz5XfUS
SET default_transaction_read_only = off;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
--
-- Drop roles
--
DROP ROLE postgres;
DROP ROLE student;
--
-- Roles
--
CREATE ROLE postgres;
ALTER ROLE postgres WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION
BYPASSRLS PASSWORD
'SCRAM-SHA-256$4096:yfk/5FFxV407/S3oyBsCgw==$OXcRX+r8Qf1rHG4twj/5+MvsSQCLZ3Coe4dpq05MMow=:
v0NHWjIxeJKzuvakkOGY42c9o2EtM/wkNcgJuhVXE7E=';
CREATE ROLE student;
ALTER ROLE student WITH NOSUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION
NOBYPASSRLS PASSWORD
'SCRAM-SHA-256$4096:G2Dm/mJ7z4+gqTtmhCcqZA==$C/rauObLdXjclYh6ggwRIaRneqQs5QjUDjRIjHKx/t8=:
VGpRpwauhFGf5KaEVb7adV9+KZuEEZ0vGKrIa80yuJM=';
--
-- User Configurations
--
--
-- Role memberships
--
GRANT pg_checkpoint TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_create_subscription TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_monitor TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_read_server_files TO student WITH INHERIT TRUE GRANTED BY postgres;
GRANT pg_write_server_files TO student WITH INHERIT TRUE GRANTED BY postgres;
--
-- Role privileges on configuration parameters
--

Page 19: Page 19

GRANT ALTER SYSTEM ON PARAMETER archive_command TO student;
GRANT ALTER SYSTEM ON PARAMETER archive_mode TO student;
GRANT ALTER SYSTEM ON PARAMETER archive_timeout TO student;
GRANT ALTER SYSTEM ON PARAMETER checkpoint_timeout TO student;
GRANT ALTER SYSTEM ON PARAMETER hot_standby_feedback TO student;
GRANT ALTER SYSTEM ON PARAMETER listen_addresses TO student;
GRANT ALTER SYSTEM ON PARAMETER log_recovery_conflict_waits TO student;
GRANT ALTER SYSTEM ON PARAMETER max_connections TO student;
GRANT ALTER SYSTEM ON PARAMETER max_slot_wal_keep_size TO student;
GRANT ALTER SYSTEM ON PARAMETER max_standby_streaming_delay TO student;
GRANT ALTER SYSTEM ON PARAMETER max_wal_size TO student;
GRANT ALTER SYSTEM ON PARAMETER min_wal_size TO student;
GRANT ALTER SYSTEM ON PARAMETER restore_command TO student;
GRANT ALTER SYSTEM ON PARAMETER synchronous_commit TO student;
GRANT ALTER SYSTEM ON PARAMETER synchronous_standby_names TO student;
GRANT ALTER SYSTEM ON PARAMETER wal_keep_size TO student;
GRANT ALTER SYSTEM ON PARAMETER wal_level TO student;
GRANT ALTER SYSTEM ON PARAMETER wal_receiver_status_interval TO student;
\unrestrict 3es50MMtas3DuQEICBlRybTIYFuQo6UjJ6QSte0gHU6YC3Cu9FPpqeA5bz5XfUS
--
-- PostgreSQL database cluster dump complete
--

Восстановление выполняется с помощью psql — никакой другой формат не поддерживается.

student$ psql -p 5433 -U postgres -f /home/student/tmp/alpha.sql
SET
SET
SET
DROP DATABASE
DROP DATABASE
psql:/home/student/tmp/alpha.sql:26: ERROR:  current user cannot be dropped
psql:/home/student/tmp/alpha.sql:27: ERROR:  role "student" cannot be dropped because
some objects depend on it
DETAIL:  owner of database backup_logical_1
1 object in database backup_logical_1
psql:/home/student/tmp/alpha.sql:34: ERROR:  role "postgres" already exists
ALTER ROLE
psql:/home/student/tmp/alpha.sql:36: ERROR:  role "student" already exists
ALTER ROLE
psql:/home/student/tmp/alpha.sql:48: NOTICE:  role "student" has already been granted
membership in role "pg_checkpoint" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:49: NOTICE:  role "student" has already been granted
membership in role "pg_create_subscription" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:50: NOTICE:  role "student" has already been granted
membership in role "pg_monitor" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:51: NOTICE:  role "student" has already been granted
membership in role "pg_read_server_files" by role "postgres"
GRANT ROLE
psql:/home/student/tmp/alpha.sql:52: NOTICE:  role "student" has already been granted
membership in role "pg_write_server_files" by role "postgres"
GRANT ROLE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT

Page 20: Page 20

SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
UPDATE 1
DROP DATABASE
CREATE DATABASE
ALTER DATABASE
You are now connected to database "template1" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
COMMENT
ALTER DATABASE
You are now connected to database "template1" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
REVOKE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "backup_logical" as user "postgres".
SET
SET

Page 21: Page 21

SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
COPY 3
 setval
--------
      3
(1 row)
ALTER TABLE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
DROP DATABASE
CREATE DATABASE
ALTER DATABASE
You are now connected to database "postgres" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
COMMENT
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET

Page 22: Page 22

CREATE DATABASE
ALTER DATABASE
You are now connected to database "student" as user "postgres".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT

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

student$ psql -p 5433
β=> SELECT datname FROM pg_database;
     datname
------------------
 template0
 backup_logical_1
 template1
 backup_logical
 postgres
 student
(6 rows)
β=> \c backup_logical
You are now connected to database "backup_logical" as user "student".
β=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Привет, мир!
  2 |
  3 |
(3 rows)

Влияние политик защиты строк

Если на таблицах определены политики защиты строк (подробно рассматриваются в теме «Доступ на уровне строк» курса DBS), то есть опасность выгрузить неполные данные и даже не узнать об этом. Чтобы этого не произошло, перед выполнением команды COPY можно установить параметр row_security в значение off — в этом случае применение политики приведет к явной ошибке. Простой пример. Настроим политику так, чтобы не были видны пустые строки, и включим ее для владельца таблицы:

α=> CREATE POLICY t_s_not_null ON t USING (s IS NOT NULL);
CREATE POLICY
α=> ALTER TABLE t ENABLE ROW LEVEL SECURITY;
ALTER TABLE
α=> ALTER TABLE t FORCE ROW LEVEL SECURITY;
ALTER TABLE

Теперь COPY выгрузит только две строки:

Page 23: Page 23

α=> COPY t TO stdout;
1
Привет, мир!
2

Но если установить параметр в off, получим ошибку:

α=> SET row_security = off;
SET
α=> COPY t TO stdout;
ERROR:  query would be affected by row-level security policy for table "t"
HINT:  To disable the policy for the table's owner, use ALTER TABLE NO FORCE ROW LEVEL
SECURITY.

Утилиты pg_dump и pg_dumpall автоматически устанавливают row_security = off, так что дополнительные действия не нужны:

student$ pg_dump -d backup_logical -t t > /dev/null
pg_dump: error: query failed: ERROR:  query would be affected by row-level security
policy for table "t"
HINT:  To disable the policy for the table's owner, use ALTER TABLE NO FORCE ROW LEVEL
SECURITY.
pg_dump: detail: Query was: COPY public.t (id, s) TO stdout;

Утилита попыталась выгрузить строки таблицы командой COPY и завершилась ошибкой — она сообщает, что выгрузить все строки таблицы t не удается. Если все-таки решим выгрузить только доступные строки, можно использовать –enable-row-security:

student$ pg_dump -d backup_logical -t t --enable-row-security | grep -A3 '^COPY'
COPY public.t (id, s) FROM stdin;
1
Привет, мир!
2
\.

Чтобы обойти политики защиты строк и выгрузить все данные, пользователь должен иметь атрибут BYPASSRLS:

α=> \c - postgres
You are now connected to database "backup_logical" as user "postgres".
α=> ALTER USER student BYPASSRLS;
ALTER ROLE

Теперь запросы от роли student будут игнорировать политики защиты строк:

student$ pg_dump -d backup_logical -t t --data-only
--
-- PostgreSQL database dump
--
\restrict 7lfgcqAyFvDojclaH2Z2g2Q0XjsgfiBgIufeXBMlBtwEvtm604zA95pzqmRtbh2
-- Dumped from database version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
-- Dumped by pg_dump version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Data for Name: t; Type: TABLE DATA; Schema: public; Owner: student
--
COPY public.t (id, s) FROM stdin;
1
Привет, мир!
2
3
\N
\.

Page 24: Page 24

--
-- Name: t_id_seq; Type: SEQUENCE SET; Schema: public; Owner: student
--
SELECT pg_catalog.setval('public.t_id_seq', 3, true);
--
-- PostgreSQL database dump complete
--
\unrestrict 7lfgcqAyFvDojclaH2Z2g2Q0XjsgfiBgIufeXBMlBtwEvtm604zA95pzqmRtbh2

Page 25: Итоги

Логическое резервирование позволяет сделать копию всего кластера, базы данных или отдельных объектов

  • Хорошо подходит
    • для данных небольшого объема
    • для длительного хранения, за время которого меняется версия сервера
    • для миграции на другую платформу
  • Плохо подходит
    • для восстановления после сбоя с минимальной потерей данных

Page 26: Практика

  1. Создайте пару баз данных с таблицами и представлениями, получите с помощью pg_dumpall копию только глобальных объектов кластера, а затем сделайте копии баз данных с помощью утилиты pg_dump в параллельном режиме. Восстановите кластер на другом сервере, используя созданные резервные копии. 2. Создайте таблицу с двумя столбцами, один из которых является первичным ключом и заполняется последовательностью. Воспроизведите ситуацию, когда при загрузке с помощью COPY последовательность не генерирует новые значения, вызывая нарушение уникальности при последующей вставке строк.

Комментарии преподавателя

  1. Объявите столбец как GENERATED ALWAYS AS IDENTITY.

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

Page 27: 1. Логическое копирование

Создадим базы данных, пару таблиц и представлений.

α=> CREATE DATABASE backup_logical1;
CREATE DATABASE
α=> \c backup_logical1
You are now connected to database "backup_logical1" as user "student".
α=> CREATE TABLE t1(n integer);
CREATE TABLE
α=> INSERT INTO t1 VALUES (1), (2), (3);
INSERT 0 3
α=> CREATE VIEW v1 AS SELECT * FROM t1;
CREATE VIEW
α=> CREATE DATABASE backup_logical2;
CREATE DATABASE
α=> \c backup_logical2
You are now connected to database "backup_logical2" as user "student".
α=> CREATE TABLE t2(n integer);
CREATE TABLE
α=> INSERT INTO t2 VALUES (1), (2), (3);
INSERT 0 3
α=> CREATE VIEW v2 AS SELECT * FROM t2;
CREATE VIEW

Сделаем копию глобальных объектов.

student$ pg_dumpall --clean --globals-only -U postgres -f /home/student/tmp/alpha_globals.sql

Логические копии баз данных в параллельном режиме:

student$ pg_dump --jobs=2 \
  --format=directory \
  -d backup_logical1 \
  -f /home/student/tmp/backup_logical1.directory
student$ pg_dump --jobs=2 \
  --format=directory \
  -d backup_logical2 \
  -f /home/student/tmp/backup_logical2.directory

Теперь восстановим кластер. Сначала восстанавливаем глобальные объекты:

student$ psql -p 5433 -U postgres -f /home/student/tmp/alpha_globals.sql 2> /dev/null
SET
SET
SET
ALTER ROLE
ALTER ROLE
GRANT ROLE
GRANT ROLE
GRANT ROLE
GRANT ROLE
GRANT ROLE
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT

Page 28: Page 28

GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT
GRANT

Затем — две базы данных в параллельном режиме:

student$ pg_restore -p 5433 \
  -U postgres \
  -d postgres \
  --create \
  --jobs=2 \
  /home/student/tmp/backup_logical1.directory
student$ pg_restore -p 5433 \
  -U postgres \
  -d postgres \
  --create \
  --jobs=2 \
  /home/student/tmp/backup_logical2.directory

Проверим:

student$ psql -p 5433
β=> \c backup_logical1
You are now connected to database "backup_logical1" as user "student".
β=> \d
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+---------
 public | t1   | table | student
 public | v1   | view  | student
(2 rows)
β=> \c backup_logical2
You are now connected to database "backup_logical2" as user "student".
β=> \d
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+---------
 public | t2   | table | student
 public | v2   | view  | student
(2 rows)

Удалим созданные базы данных.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_logical1;
DROP DATABASE
α=> DROP DATABASE backup_logical2;
DROP DATABASE

2. COPY для таблицы с последовательностью

Таблица с двумя столбцами:

α=> CREATE DATABASE backup_logical;
CREATE DATABASE
α=> \c backup_logical
You are now connected to database "backup_logical" as user "student".

Page 29: Page 29

α=> CREATE TABLE t(
  id int PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  txt text
);
CREATE TABLE

Созданы таблица и последовательность:

α=> \d
           List of relations
 Schema |   Name   |   Type   |  Owner
--------+----------+----------+---------
 public | t        | table    | student
 public | t_id_seq | sequence | student
(2 rows)

Загружаем строки, не указывая значения для первого столбца.

α=> COPY t(txt) FROM STDIN;
Один
Два
Три
\.
COPY 3

Содержимое таблицы соответствует ожиданиям:

α=> SELECT * FROM t;
 id | txt
----+------
  1 | Один
  2 | Два
  3 | Три
(3 rows)

Счетчик последовательности корректен:

α=> SELECT currval('t_id_seq');
 currval
---------
       3
(1 row)

Можно добавить очередную строку:

α=> INSERT INTO t(txt) VALUES('Четыре');
INSERT 0 1

Вставляя строки с помощью команды INSERT, мы не можем указывать значение столбца, объявленного как GENERATED ALWAYS. Оно должно генерироваться последовательностью:

α=> INSERT INTO t(id,txt) VALUES(5,'Пять');
ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.

Однако COPY дает такую возможность:

α=> COPY t FROM STDIN;
5
Пять
6
Шесть
7
Семь
\.
COPY 3

На первый взгляд все нормально…

α=> SELECT * FROM t;

Page 30: Page 30

 id |  txt
----+--------
  1 | Один
  2 | Два
  3 | Три
  4 | Четыре
  5 | Пять
  6 | Шесть
  7 | Семь
(7 rows)

Но это не так:

α=> INSERT INTO t(txt) VALUES('Восемь');
ERROR:  duplicate key value violates unique constraint "t_pkey"
DETAIL:  Key (id)=(5) already exists.

Нарушение первичного ключа произошло из-за того, что COPY не обращается к последовательности:

α=> SELECT currval('t_id_seq');
 currval
---------
       5
(1 row)

Исправим ситуацию, изменив состояние последовательности.

α=> SELECT setval('t_id_seq', max(id)) FROM t;
 setval
--------
      7
(1 row)

Теперь добавление удастся:

α=> INSERT INTO t(txt) VALUES('Восемь');
INSERT 0 1
α=> SELECT * FROM t;
 id |  txt
----+--------
  1 | Один
  2 | Два
  3 | Три
  4 | Четыре
  5 | Пять
  6 | Шесть
  7 | Семь
  8 | Восемь
(8 rows)

Уборка

Удалим базу данных.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_logical;
DROP DATABASE

Page 31: Практика+

  1. Создайте таблицу с несколькими строками. Подберите такие параметры \copy, чтобы при записи копии в файл перепутались корректные данные и неопределенные значения (NULL) из-за неудачного выбора кода. 2. Используя \copy, создайте текстовый файл, в котором отсутствие значения обозначается как N/A. Загрузите строки из этого файла в таблицу, в которой отсутствие значения представляет NULL. 3. Создайте таблицу с данными размером несколько мегабайт и получите размеры логических копий в пользовательском формате с разными способами сжатия. Также измерьте время, затраченное при копировании с каждым методом сжатия.

Комментарии преподавателя

  1. Если ошибочно использовать код для NULL, который будет совпадать с каким-либо нормальным содержимым столбцов таблицы, получившаяся копия не позволит корректно восстановить данные.

  2. Сгенерировать данные можно, например, запросом SELECT (random()::numeric*(g.i))^(g.i) AS n FROM generate_series(1,2_500) AS g(i);

Измерить время можно командой /usr/bin/time -f %e pg_dump … .

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

Page 32: 1. Ломаем COPY

Создадим базу данных.

α=> CREATE DATABASE backup_logical;
CREATE DATABASE
α=> \c backup_logical
You are now connected to database "backup_logical" as user "student".

Например, можно установить отображение неопределенных значений, совпадающее с какими-либо данными:

α=> CREATE TABLE anticopy(s text);
CREATE TABLE
α=> INSERT INTO anticopy(s) VALUES ('N'), (NULL);
INSERT 0 2

Установим для неопределенных значений код N:

α=> \copy anticopy to '/home/student/tmp/anticopy.dump' with (null 'N');
COPY 2

Вывод двух разных значений теперь неотличим друг от друга. Корректно восстановить из такой копии данные невозможно.

student$ cat ${HOME}/tmp/anticopy.dump
N
N

Осторожнее с изменением формата по умолчанию!

2. Загрузка данных с пропусками

Создадим файл, в котором отсутствие данных будет отмечено как N/A.

α=> \copy (VALUES (0,'N/A'), (1,'Раз'), (2,'N/A'), (3,'Три')) to /home/student/tmp/backup_logical.txt
COPY 4
α=> \! cat /home/student/tmp/backup_logical.txt
0
N/A
1
Раз
2
N/A
3
Три

Создадим таблицу для загрузки данных.

α=> CREATE TABLE t (k int, v text);
CREATE TABLE

Загрузим данные в таблицу из файла. Значение N/A должно загружаться как NULL.

α=> \copy t from /home/student/tmp/backup_logical.txt null 'N/A'
COPY 4
α=> SELECT *, v IS NULL "IsNull" FROM t;
 k |  v  | IsNull
---+-----+--------
 0 |     | t
 1 | Раз | f
 2 |     | t
 3 | Три | f
(4 rows)

3. Сжатие в pg\_dump

Создадим таблицу достаточно большого размера.

Page 33: Page 33

α=> CREATE TABLE mb AS
  SELECT (random()::numeric*(g.i))^(g.i) AS n
  FROM generate_series(1,2_500) AS g(i);
SELECT 2500
α=> SELECT pg_size_pretty(pg_table_size('mb'));
 pg_size_pretty
----------------
 5128 kB
(1 row)

Измерим время выгрузки данных без сжатия (а также прогреем кеш).

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=none --file=/home/student/tmp/backup_logical.none
0.44

Сколько потребуется времени на выгрузку со сжатием gzip (используется по умолчанию)?

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=gzip --file=/home/student/tmp/backup_logical.gzip
1.81

Теперь испытаем lz4.

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=lz4 --file=/home/student/tmp/backup_logical.lz4
0.35

А теперь — zstd.

student$ /usr/bin/time -f %e pg_dump \
  --format=custom --dbname=backup_logical \
  --compress=zstd --file=/home/student/tmp/backup_logical.zstd
0.58

Сравним размеры полученных копий:

student$ ls -lhS ~/tmp/backup_logical.*
-rw-rw-r-- 1 student student 8,3M апр 10 18:12 /home/student/tmp/backup_logical.none
-rw-rw-r-- 1 student student 7,9M апр 10 18:12 /home/student/tmp/backup_logical.lz4
-rw-rw-r-- 1 student student 4,1M апр 10 18:12 /home/student/tmp/backup_logical.zstd
-rw-rw-r-- 1 student student 3,9M апр 10 18:12 /home/student/tmp/backup_logical.gzip
-rw-rw-r-- 1 student student   30 апр 10 18:12 /home/student/tmp/backup_logical.txt

Выводы: gzip сжимает лучше всех, однако работает в несколько раз дольше; lz4 — самый быстрый, но дает весьма небольшой эффект; zstd — хороший компромисс: степень сжатия лишь немного уступает gzip, а скорость сравнима с lz4.

Уборка

Удалим созданные базы данных и временные файлы.

student$ rm ~/tmp/backup_logical.*
student$ rm ~/tmp/anticopy.dump
α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_logical;
DROP DATABASE

02 Занятие 02: Резервное копирование / Базовая резервная копия

Исходная страница в нативном Markdown: Резервное копирование / Базовая резервная копия
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_02_backup_base.html Archive: DBA3-handouts-16.zip Страниц PDF: 26

Page 1: Резервное копирование / Базовая резервная копия

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Понятие физической резервной копии
  • Холодное и горячее резервирование
  • Протокол репликации и слот
  • Клонирование кластера и архивная копия
  • Проверка целостности
  • API резервирования

Page 3: Физическая копия

Базовая физическая копия — копия всех файлов кластера

    • быстрее, чем логическое резервирование
    • восстанавливается статистика
  • − можно восстановиться только на совместимой системе
  • − и на той же самой основной версии PostgreSQL
  • − выборочная копия невозможна, копируется весь кластер

Комментарии преподавателя

Физическое резервирование подразумевает копирование всех файлов, относящихся к кластеру БД (как файлов данных, так и служебных файлов), то есть создание полной двоичной копии.

Копирование файлов работает быстрее, чем выгрузка SQL-команд при логическом резервировании; запустить сервер из созданной физической копии — дело нескольких минут, в отличие от восстановления из логической копии. Кроме того, нет необходимости заново собирать статистику — она также восстанавливается из копии.

Но есть и минусы. Из физической резервной копии можно восстановить систему только на совместимой платформе (та же ОС, та же разрядность, тот же порядок байтов в представлении чисел и т. п.) и только на той же основной версии PostgreSQL. Кроме того, невозможно сделать физическую копию отдельных баз данных кластера, возможно копирование только всего кластера целиком.

Page 4: Холодное резервирование

Выключенный сервер

  • если выполнена контрольная точка, то достаточно скопировать файлы
  • копию можно развернуть на другом сервере, независимо от PGDATA
    • простота
  • − требуется прерывание обслуживания
  • Снимок файловой системы
    • как при неаккуратном выключении: при старте потребуется
    • восстановление, но в копию войдут все нужные файлы журнала
    • данные, в т. ч. табличные пространства, должны войти в один снимок
      • не надо останавливать сервер
    • − файловые системы, поддерживающие снимки, работают медленней

Комментарии преподавателя

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

Сделать такую копию можно лишь корректно остановив сервер с выполнением контрольной точки. Минус понятен: необходима остановка сервера (надолго при большом объеме данных).

Время простоя можно сократить за счет предварительного выполнения rsync (или аналогичного инструмента) при работающем сервере. Тогда после останова сервера rsync докопирует только изменения (которых, предположительно, будет немного). Но простоя все равно не избежать.

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

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

https://postgrespro.ru/docs/postgresql/16/backup-file

Page 5: Холодная файловая копия

Файлы остановленного кластера можно скопировать и запустить с ними второй сервер. При этом не важно, был ли сервер остановлен корректно. Создадим базу данных и таблицу.

student$ psql
α=> CREATE DATABASE backup_base;
CREATE DATABASE
α=> \c backup_base
You are now connected to database "backup_base" as user "student".
α=> CREATE TABLE t(s text);
CREATE TABLE
α=> INSERT INTO t VALUES ('Эта строка вставлена на α...');
INSERT 0 1

Аварийно останавливаем сервер alpha и копируем файлы на сервер beta:

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)
student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo cp -a /var/lib/postgresql/16/alpha /var/lib/postgresql/16/beta

Сервер beta запускается, восстанавливая согласованность:

student$ sudo pg_ctlcluster 16 beta start
student$ sudo tail -n 7 /var/log/postgresql/postgresql-16-beta.log
2026-04-10 18:00:49.842 MSK [66007] LOG:  database system was not properly shut down;
automatic recovery in progress
2026-04-10 18:00:49.858 MSK [66007] LOG:  redo starts at 0/1B83330
2026-04-10 18:00:49.932 MSK [66007] LOG:  invalid record length at 0/1FB5708: expected at
least 24, got 0
2026-04-10 18:00:49.932 MSK [66007] LOG:  redo done at 0/1FB56E0 system usage: CPU: user:
0.00 s, system: 0.01 s, elapsed: 0.07 s
2026-04-10 18:00:50.063 MSK [66005] LOG:  checkpoint starting: end-of-recovery immediate
wait
2026-04-10 18:00:53.423 MSK [66005] LOG:  checkpoint complete: wrote 932 buffers (5.7%);
0 WAL file(s) added, 0 removed, 0 recycled; write=0.046 s, sync=3.242 s, total=3.375 s;
sync files=308, longest=0.026 s, average=0.011 s; distance=4297 kB, estimate=4297 kB;
lsn=0/1FB5708, redo lsn=0/1FB5708
2026-04-10 18:00:53.448 MSK [66004] LOG:  database system is ready to accept connections
student$ psql -p 5433 -d backup_base
β=> SELECT * FROM t;
              s
------------------------------
 Эта строка вставлена на α...
(1 row)

Page 6: Горячее резервирование

Работающий сервер

  • страницы не согласованы между собой, как при неаккуратном
  • выключении
  • страница может меняться во время ее копирования
  • (проблема неатомарного чтения и записи)
  • для восстановления согласованности необходимы журнальные записи
  • от последней контрольной точки за все время копирования
  • (в процессе копирования сервер может удалить часть файлов)
    • не требуется прерывание обслуживания
  • − нужны специальные инструменты

Комментарии преподавателя

Горячее резервирование выполняется на работающем сервере, поэтому в копию наверняка попадут несогласованные между собой страницы.

Более того. При резервном копировании данные читаются не через буферный кеш, а напрямую из файлов. Содержимое файлов на диске, очевидно, изменяется во время копирования, а гарантии атомарности чтения/записи страниц PostgreSQL может и не быть (это зависит от файловой системы и оборудования и обсуждается в теме «Настройка журнала» курса DBA2). Поэтому в резервную копию могут попадать «безнадежные» страницы, к которым даже нельзя применить журнальные записи. Это первая сложность.

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

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

https://postgrespro.ru/docs/postgresql/16/continuous-archiving#BACKUP-BA SE-BACKUP

Page 7: Протокол репликации

Задачи

  • управление резервным копированием и репликацией
  • в частности, получение потока журнальных записей
  • Обслуживается процессом wal sender
    • max_wal_senders = 10
    • wal_level = replica или logical, но не minimal
  • Подключение
    • роль с атрибутом REPLICATION или SUPERUSER
    • разрешение на подключение в pg_hba.conf

Комментарии преподавателя

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

На сервере подключение по протоколу репликации обслуживается процессом wal sender. Он похож на обычный обслуживающий процесс, который запускается при обычном подключении клиента, но понимает не SQL, а специальные команды. Число одновременно работающих процессов wal_sender ограничено параметром max_wal_senders (значение по умолчанию — 10).

Уровень журнала должен быть не ниже, чем replica. Дело в том, что на уровне minimal такие команды, как CREATE TABLE AS SELECT, CREATE INDEX или COPY FROM не попадают в журнал: долговечность обеспечивается тем, что данные не остаются в оперативной памяти, а сразу записываются на диск. Этого достаточно для восстановления после сбоя и из холодной копии, но недостаточно для восстановления из горячей копии.

Чтобы использовать протокол репликации, клиент должен подключаться к серверу под ролью, имеющей атрибут REPLICATION (либо под суперпользователем). В pg_hba.conf надо разрешить подключение этой роли к базе данных replication (это не название БД, а ключевое слово). Причем разрешения для all недостаточно, подключение к replication должно быть разрешено отдельно.

https://postgrespro.ru/docs/postgresql/16/protocol-replication

Page 8: Слот репликации

Серверный объект для гарантии получения журнальных записей

  • помнит, какая запись была передана клиенту последней
  • число слотов ограничено max_replication_slots = 10
  • Не позволяет удалить сегмент WAL, пока все его записи не переданы клиенту
    • клиент может получать данные в удобном режиме,
    • в том числе отключаться на время
    • при отключении клиента журнальные файлы накапливаются на сервере
    • мониторинг состояния: pg_replication_slots

Комментарии преподавателя

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

Использование слота позволяет клиенту не беспокоиться о том, что сервер сотрет файл журнала раньше времени. Клиент даже может отключиться и затем через какое-то время подключиться вновь и продолжить получать журнальные записи с того момента, на котором остановился.

Но надо иметь в виду, что при отключении клиента файлы журнала будут накапливаться на сервере и могут занять все свободное место. Поэтому каждый созданный слот следует добавлять в мониторинг (представление pg_replication_slots) и своевременно удалять ненужные слоты.

Общее количество слотов, которые могут быть созданы, ограничено конфигурационным параметром max_replication_slots (значение по умолчанию — 10).

Подробнее применение слотов обсуждается в модуле «Репликация».

https://postgrespro.ru/docs/postgresql/16/warm-standby#STREAMING-REP LICATION-SLOTS

Page 9: Клонирование кластера

Source slide page 9: Клонирование кластера{#source-figure-009 caption=“Source slide page 9: Клонирование кластера”}

для физического копирования используется утилита pg_basebackup после копирования новый сервер готов к запуску

Комментарии преподавателя

Для немедленного развертывания нового сервера из копии вызывают pg_basebackup в обычном формате (–format=plain — по умолчанию), запуская утилиту на целевом сервере. Она подключается к серверу-источнику и копирует с него файлы в локальный каталог, указанный параметром –pgdata.

Дополнительные табличные пространства копируются по тем же абсолютным путям, что и на сервере-источнике. Пути можно переназначить, указав соответствия в параметрах –tablespace-mapping утилиты. Это необходимо, если pg_basebackup запускается на самом сервере-источнике.

При потоковой доставке (–wal-method=stream — по умолчанию) утилита устанавливает второе соединение для передачи потока журнальных записей, которые генерирует работающий сервер, в локальный каталог, указанный параметром –waldir.

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

В формате plain можно сжимать данные на сервере, уменьшая нагрузку на сеть. Переданные данные затем распаковываются утилитой. Но журнальные записи в потоковом режиме не сжимаются.

https://postgrespro.ru/docs/postgresql/16/app-pgbasebackup

Page 10: Клонирование кластера

Сделаем базовую копию работающего сервера alpha и восстановим с помощью этой копии сервер beta.

student$ sudo pg_ctlcluster 16 alpha start
student$ psql backup_base

Разрешение на локальное подключение по протоколу репликации в pg_hba.conf уже имеется:

student$ psql -U postgres << EOF
SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules() WHERE 'replication' = ANY(database);
EOF
 type  |   database    | user_name |  address  |  auth_method
-------+---------------+-----------+-----------+---------------
 local | {replication} | {all}     |           | trust
 host  | {replication} | {all}     | 127.0.0.1 | scram-sha-256
 host  | {replication} | {all}     | ::1       | scram-sha-256
(3 rows)

Чтобы утилита pg_basebackup могла подключиться к серверу под ролью student, эта роль должна иметь атрибут REPLICATION:

α=> \du student
                  List of roles
 Role name |             Attributes
-----------+-------------------------------------
 student   | Create role, Create DB, Replication

Остановим сервер beta и удалим каталог данных этого сервера.

student$ sudo pg_ctlcluster 16 beta stop
student$ sudo rm -rf /var/lib/postgresql/16/beta

Скопируем, выполнив быструю контрольную точку. Данные будут сжаты на стороне сервера и затем распакованы утилитой — это ускоряет передачу по сети, но при локальном подключении не даст эффекта. Параметр –pgdata указывает целевой каталог для записи копии. Каталог создается автоматически, но если он уже существует, то должен быть пуст.

postgres$ pg_basebackup \
  --checkpoint=fast \
  --compress=server-zstd \
  --pgdata=/var/lib/postgresql/16/beta

Эта команда использовала значения по умолчанию: –format=plain –wal-method=stream В подкаталог pg_wal скопированы файлы журнала:

postgres$ ls -l /var/lib/postgresql/16/beta/pg_wal
total 16388
-rw------- 1 postgres postgres 16777216 апр 10 18:01 000000010000000000000002
drwx------ 2 postgres postgres     4096 апр 10 18:01 archive_status

Page 11: Проверка целостности

Проверка файлов данных

  • задача: гарантировать, что в копию попадут целостные данные
  • утилиты pg_basebackup и pg_checksums
  • проверяют контрольные суммы страниц файлов отношений
  • Проверка резервной копии
    • задача: проверить, что целостность данных в копии
    • не нарушилась со временем
    • файл манифеста backup_manifest
    • утилита pg_verifybackup проверяет файлы по списку из манифеста

Комментарии преподавателя

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

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

● при каждом чтении страницы в буферный кеш;

● утилитой pg_basebackup при формировании резервной копии;

● утилитой pg_checksums в режиме проверки.

Утилита pg_basebackup включает в копию файл backup_manifest с информацией о файлах (имена, размеры, вычисленные контрольные суммы) и журнальных записях (начальная и конечная позиции WAL, линия времени), а также контрольную сумму самого манифеста.

https://postgrespro.ru/docs/postgresql/16/backup-manifest-format

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

https://postgrespro.ru/docs/postgresql/16/app-pgverifybackup

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

Page 12: Проверка целостности

Резервная копия содержит файл манифеста, вот его начало и конец:

postgres$ head -5 /var/lib/postgresql/16/beta/backup_manifest
{ "PostgreSQL-Backup-Manifest-Version": 1,
"Files": [
{ "Path": "backup_label", "Size": 225, "Last-Modified": "2026-04-10 15:01:36 GMT",
"Checksum-Algorithm": "CRC32C", "Checksum": "45a3d732" },
{ "Path": "postgresql.auto.conf", "Size": 88, "Last-Modified": "2025-11-25 15:23:08 GMT",
"Checksum-Algorithm": "CRC32C", "Checksum": "536f950b" },
{ "Path": "pg_logical/replorigin_checkpoint", "Size": 8, "Last-Modified": "2026-04-10
15:01:36 GMT", "Checksum-Algorithm": "CRC32C", "Checksum": "c74b6748" },
postgres$ tail -6 /var/lib/postgresql/16/beta/backup_manifest
{ "Path": "global/pg_control", "Size": 8192, "Last-Modified": "2026-04-10 15:01:36 GMT",
"Checksum-Algorithm": "CRC32C", "Checksum": "43872087" }
],
"WAL-Ranges": [
{ "Timeline": 1, "Start-LSN": "0/2000028", "End-LSN": "0/2000100" }
],
"Manifest-Checksum": "1717a28bdccedfcd93e1fdf00f9fdb7284dbdaaeeb1bc3e677de7cf5e384ec8a"}

Проверим целостность копии утилитой pg_verifybackup:

postgres$ /usr/lib/postgresql/16/bin/pg_verifybackup /var/lib/postgresql/16/beta
backup successfully verified

Утилита проверяет по манифесту наличие файлов, их контрольные суммы, а также наличие и возможность чтения всех записей WAL, необходимых для восстановления. Запускаем второй сервер.

student$ sudo pg_ctlcluster 16 beta start

Теперь оба сервера работают одновременно и независимо. Проверим:

student$ psql -p 5433 -d backup_base
β=> INSERT INTO t VALUES ('...а эта — на β.');
INSERT 0 1
β=> SELECT * FROM t;
              s
------------------------------
 Эта строка вставлена на α...
 ...а эта — на β.
(2 rows)

Page 13: Потоковая доставка WAL

Source slide page 13: Потоковая доставка WAL{#source-figure-013 caption=“Source slide page 13: Потоковая доставка WAL”}

создание копии для будущего использования

Комментарии преподавателя

Для создания архивных копий используется формат tar. По умолчанию используется потоковая доставка журнальных записей (параметр –wal-method=stream).

Файлы кластера будут сохранены в файле base.tar. Каждое дополнительное табличное пространство копируется в отдельный tar-файл, имя которого совпадает с OID табличного пространства. Сегменты WAL помещаются в отдельный файл pg_wal.tar.

Утилита может выполнять сжатие как на стороне клиента, так и на стороне сервера (в последнем случае pg_wal.tar не сжимается). К именам сжатых tar-архивов добавится суффикс, соответствующий методу сжатия. Например, при сжатии методом gzip основной файл будет называться base.tar.gz.

Для восстановления из такой копии сначала потребуется развернуть tar-файлы по правильным путям. При этом пути табличных пространств можно изменить: для этого нужно перед запуском сервера отредактировать файл tablespace_map или использовать параметр командной строки –tablespace-mapping.

Page 14: Файловая доставка клиенту

Source slide page 14: Файловая доставка клиенту{#source-figure-014 caption=“Source slide page 14: Файловая доставка клиенту”}

создание копии для будущего использования

Комментарии преподавателя

При создании архивной копии можно использовать другой режим — файловую доставку журнальных записей (–wal-method=fetch).

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

Журнальные сегменты записываются в файл base.tar вместе с остальными файлами кластера.

В большинстве случаев возможность использовать режим fetch при создании копии на клиенте не нужна. Она осталась с прежних версий и в настоящее время может иметь смысл только для того, чтобы передавать журнальные записи сжатыми — режим stream не дает такой возможности.

Page 15: Архивная копия на стороне клиента

Создадим архивную копию на клиенте. Сжатие будем выполнять также на стороне клиента.

student$ pg_basebackup \
  --checkpoint=fast \
  --compress=client-gzip:level=9 \
  --format=tar \
  --pgdata=/home/student/tmp/alpha-stream.backup

В этой команде по умолчанию была использована потоковая доставка WAL (–wal-method=stream). Посмотрим на получившуюся копию:

student$ ls -lh /home/student/tmp/alpha-stream.backup
total 5,2M
-rw------- 1 student student 220K апр 10 18:02 backup_manifest
-rw------- 1 student student 5,0M апр 10 18:02 base.tar.gz
-rw------- 1 student student  17K апр 10 18:02 pg_wal.tar.gz

При потоковой доставке записей WAL в формате tar для них создается отдельный файл:

student$ tar tf /home/student/tmp/alpha-stream.backup/pg_wal.tar.gz
000000010000000000000004

Теперь создадим еще одну архивную копию на клиенте, но используем режим доставки fetch. В этом случае сегменты WAL будут передаваться по тому же соединению, что и данные. Сжатие поручим серверу.

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=fetch \
  --compress=server-gzip:level=9 \
  --format=tar \
  --pgdata=/home/student/tmp/alpha-fetch.backup

В получившейся копии сегменты WAL помещены в файл base.tar.gz.

student$ ls -lh /home/student/tmp/alpha-fetch.backup
total 5,2M
-rw------- 1 student student 220K апр 10 18:02 backup_manifest
-rw------- 1 student student 5,0M апр 10 18:02 base.tar.gz
student$ tar tf /home/student/tmp/alpha-fetch.backup/base.tar.gz pg_wal
pg_wal/
pg_wal/000000010000000000000005
pg_wal/archive_status/000000010000000000000005.done

Page 16: Файловая доставка серверу

Source slide page 16: Файловая доставка серверу{#source-figure-016 caption=“Source slide page 16: Файловая доставка серверу”}

создание копии для будущего использования копия создается на сервере без передачи данных по сети

Комментарии преподавателя

Архивную копию можно создать на сервере, запуская pg_basebackup на клиенте. Данные при этом не передаются по сети.

Создание копии на сервере задается параметром командной строки pg_basebackup –target=server:path, где path — путь к каталогу в файловой системе сервера (по умолчанию подразумевается –target=client).

В этом режиме также можно использовать сжатие, но, конечно, только на стороне сервера.

При создании копии на сервере невозможно использовать потоковый режим доставки WAL (stream), поскольку журнальные записи вообще не передаются клиенту. Необходимо указать режим доставки fetch, при котором накопившиеся журнальные сегменты копируются и помещаются в файл base.tar вместе с остальными файлами кластера.

Page 17: Архивная копия на сервере

Запустим pg_basebackup на клиенте и при этом сохраним архивную копию на сервере. В этом случае мы обязаны использовать режим доставки fetch, поэтому установим параметр wal_keep_size (будем надеяться, что этого значения хватит):

α=> ALTER SYSTEM SET wal_keep_size = '32MB';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

Запустим pg_basebackup, указав каталог на сервере для сохранения архивной копии и режим сжатия zstd.

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=fetch \
  --target=server:/var/lib/postgresql/alpha.backup \
  --compress=server-zstd:level=12

Получили сжатую архивную копию в формате tar (он подразумевается).

postgres$ ls -lh /var/lib/postgresql/alpha.backup
total 4,2M
-rw------- 1 postgres postgres 220K апр 10 18:02 backup_manifest
-rw------- 1 postgres postgres 4,0M апр 10 18:02 base.tar.zst

При режиме доставки fetch утилита сохраняет сегменты WAL в общем архиве:

postgres$ tar tf /var/lib/postgresql/alpha.backup/base.tar.zst pg_wal
pg_wal/
pg_wal/000000010000000000000007
pg_wal/archive_status/000000010000000000000007.done

Удалим архивную копию.

postgres$ rm -rf /var/lib/postgresql/alpha.backup

Page 18: Низкоуровневый взгляд

Source slide page 18: Низкоуровневый взгляд{#source-figure-018 caption=“Source slide page 18: Низкоуровневый взгляд”}

на время копирования устанавливается full_page_writes = on в самом начале выполняется контрольная точка (быстро или обычно)

Комментарии преподавателя

Общий алгоритм изготовления резервной копии на низком уровне одинаков как для pg_basebackup, так и для любых сторонних средств (которые могут потребоваться, поскольку pg_basebackup предоставляет только самую базовую функциональность).

  1. Надо сообщить серверу о том, что начинается резервное копирование.

При этом, во-первых, на время копирования устанавливается параметр full_page_writes: при первом изменении страницы после контрольной точки полный образ этой страницы записывается в журнал. При восстановлении журнальные записи будут применяться не к страницам в файле (из-за неатомарности ввода-вывода части страницы могут оказаться рассогласованными), а к образу страницы из журнала.

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

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

  2. После того, как все скопировано, надо сообщить серверу, что резервное копирование завершено.

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

Page 19: API резервирования

Начало копирования: pg\_backup\_start

  • выполняет контрольную точку, устанавливает full_page_writes
  • возвращает начальную позицию в журнале
  • Конец копирования: pg_backup_stop
    • возвращает конечную позицию в журнале
  • Копирование файлов
    • либо протокол репликации, либо любые средства ОС
  • Копирование журнала от начальной до конечной позиции
    • протокол репликации + слот либо средства ОС + wal_keep_size

Комментарии преподавателя

Описанный алгоритм резервного копирования можно использовать, чтобы реализовать свою собственную утилиту копирования.

Резервное копирование начинается вызовом функции pg_backup_start. Она устанавливает параметр full_page_writes и выполняет контрольную точку, как рассматривалось выше. Возвращаемое этой функцией значение — позиция в журнале, начиная с которой в копию должны попасть журнальные записи.

В конце копирования вызывается функция pg_backup_stop. Она возвращает конечную позицию в журнале.

Все журнальные записи между начальной и конечной позициями надо поместить в резервную копию. Поскольку при своей работе сервер может удалять журнальные файлы, которые не требуются для восстановления, надо либо пользоваться протоколом репликации и слотом, получая записи во время копирования (как pg_basebackup в режиме доставки –wal-method=stream), либо — что менее удобно — копировать файлы средствами ОС, установив параметр wal_keep_size. Можно копировать файлы в несколько потоков, использовать rsync и т. п.

https://postgrespro.ru/docs/postgresql/16/continuous-archiving#BACKUP-LO WLEVEL-BASE-BACKUP

Page 20: Итоги

Базовая резервная копия — полная копия кластера, включая табличные пространства, плюс журнал для восстановления согласованности

  • Горячее резервирование позволяет не останавливать сервер
  • Инструменты
    • протокол репликации
    • слот репликации
    • низкоуровневый API резервирования

Page 21: Практика

  1. В первом кластере создайте табличное пространство и базу данных с таблицей в этом пространстве. 2. Сделайте базовую резервную копию кластера с помощью pg_basebackup в формате tar со сжатием. 3. Разверните второй кластер из этой резервной копии. Табличное пространство разместите в другом каталоге, изменив файл tablespace_map. 4. Запустите второй сервер и проверьте его работоспособность.

Комментарии преподавателя

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

Page 22: 1. Табличное пространство и база данных

student$ sudo mkdir /var/lib/postgresql/ts_dir
student$ sudo chown postgres /var/lib/postgresql/ts_dir

Табличные пространства может создавать только суперпользователь:

α=> \c - postgres
You are now connected to database "student" as user "postgres".
α=> CREATE TABLESPACE ts OWNER student LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE
α=> \c - student
You are now connected to database "student" as user "student".
α=> CREATE DATABASE backup_base;
CREATE DATABASE
α=> \c backup_base
You are now connected to database "backup_base" as user "student".
α=> CREATE TABLE t(s text) TABLESPACE ts;
CREATE TABLE
α=> INSERT INTO t VALUES ('Привет, мир!');
INSERT 0 1

2. Базовая резервная копия

Запускаем pg_basebackup от имени роли student, указывая формат tar со сжатием:

student$ pg_basebackup \
  --checkpoint=fast \
  --format=tar \
  --compress=client-gzip \
  --pgdata /home/student/tmp/backup
student$ ls -l /home/student/tmp/backup
total 5348
-rw------- 1 student student     342 апр 10 18:13 16409.tar.gz
-rw------- 1 student student  224520 апр 10 18:13 backup_manifest
-rw------- 1 student student 5224279 апр 10 18:13 base.tar.gz
-rw------- 1 student student   17097 апр 10 18:13 pg_wal.tar.gz

3. Восстановление из базовой резервной копии

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.

Очистим основной каталог данных кластера beta. Права на каталог должны быть лишь у владельца.

student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo mkdir /var/lib/postgresql/16/beta
student$ sudo chown postgres /var/lib/postgresql/16/beta
student$ sudo chmod 700 /var/lib/postgresql/16/beta

Каталог для табличного пространства ts на сервере, который будет развернут из резервной копии:

student$ sudo mkdir /var/lib/postgresql/ts_beta_dir
student$ sudo chown postgres /var/lib/postgresql/ts_beta_dir

Разворачиваем резервную копию:

student$ sudo tar xf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta

Page 23: Page 23

student$ sudo tar xf /home/student/tmp/backup/16409.tar.gz -C /var/lib/postgresql/ts_beta_dir
student$ sudo tar xf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

В каталоге pg_tblspc сейчас пусто:

student$ sudo ls /var/lib/postgresql/16/beta/pg_tblspc/

Символическая ссылка появится при старте сервера в соответствии с файлом tablespace_map (который находился внутри base.tar):

student$ sudo cat /var/lib/postgresql/16/beta/tablespace_map
16409 /var/lib/postgresql/ts_dir

Изменяем в этом файле путь для табличного пространства:

student$ sudo sed -i 's/ts_dir/ts_beta_dir/' /var/lib/postgresql/16/beta/tablespace_map

4. Запуск и проверка

Запускаем сервер.

student$ sudo pg_ctlcluster 16 beta start
student$ sudo ls -l /var/lib/postgresql/16/beta/pg_tblspc/
total 0
lrwxrwxrwx 1 postgres root 31 апр 10 18:13 16409 -> /var/lib/postgresql/ts_beta_dir
student$ psql -p 5433 -d backup_base
β=> \c backup_base
You are now connected to database "backup_base" as user "student".
β=> SELECT * FROM t;
      s
--------------
 Привет, мир!
(1 row)

Уборка

Удалим резервную копию.

student$ rm -rf /home/student/tmp/backup

Чтобы удалить базу данных, надо от нее отключиться.

α=> \c student
You are now connected to database "student" as user "student".
α=> DROP DATABASE backup_base;
DROP DATABASE
α=> DROP TABLESPACE ts;
DROP TABLESPACE
student$ sudo rm -rf /var/lib/postgresql/ts_dir

И для второго сервера:

β=> \c student
You are now connected to database "student" as user "student".
β=> DROP DATABASE backup_base;
DROP DATABASE
β=> DROP TABLESPACE ts;
DROP TABLESPACE
student$ sudo rm -rf /var/lib/postgresql/ts_beta_dir

Page 24: Практика+

  1. Подключите модуль basebackup_to_shell для создания базовой резервной копии на сервере с помощью определяемой пользователем команды. Эта команда должна сжимать tar-архив копии утилитой bzip2. 2. Выполните резервное копирование на сервере в формате tar. Убедитесь, что полученный архив сжат bzip2. 3. Восстановите кластер beta из полученного сжатого архива и проверьте его работоспособность.

Комментарии преподавателя

  1. Для подключения модуля укажите одноименную библиотеку в параметре shared_preload_libraries. Параметром basebackup_to_shell.command задайте команду, выполняемую при резервном копировании: bzip2 -c > архив.tar.bz2

https://postgrespro.ru/docs/postgresql/16/basebackup-to-shell

  1. Чтобы задействовать модуль при резервном копировании, укажите pg_basebackup –target=shell

  2. Для извлечения файлов из сжатого утилитой bzip2 архива tar воспользуйтесь командой: tar xjf архив.tar.bz2 -C каталог-назначения

После выполнения практики удалите созданные базы данных и резервную копию.

Page 25: 1. Подключение модуля

Запишем настройки для подключения и конфигурации модуля basebackup_to_shell в подключаемый файл конфигурации.

postgres$ cat << EOF > /etc/postgresql/16/alpha/conf.d/basebackup_to_shell.conf
shared_preload_libraries = basebackup_to_shell
basebackup_to_shell.command = 'bzip2 -c > /var/lib/postgresql/archive.backup/%f.bz2'
EOF

Перезапустим сервер.

student$ sudo pg_ctlcluster 16 alpha restart

Проверим установки.

student$ psql
α=> SHOW shared_preload_libraries;
 shared_preload_libraries
--------------------------
 basebackup_to_shell
(1 row)
α=> SHOW basebackup_to_shell.command;
             basebackup_to_shell.command
------------------------------------------------------
 bzip2 -c > /var/lib/postgresql/archive.backup/%f.bz2
(1 row)

Создадим базу данных и таблицу в ней.

α=> CREATE DATABASE backup_base;
CREATE DATABASE
α=> \c backup_base
You are now connected to database "backup_base" as user "student".
α=> CREATE TABLE t AS SELECT now() AS creatime;
SELECT 1

2. Резервное копирование

Создадим каталог для резервной копии.

postgres$ mkdir /var/lib/postgresql/archive.backup

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

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=fetch \
  --target=shell
postgres$ ls -l /var/lib/postgresql/archive.backup
total 4464
-rw------- 1 postgres postgres   10072 апр 10 18:13 backup_manifest.bz2
-rw------- 1 postgres postgres 4558543 апр 10 18:13 base.tar.bz2

3. Восстановление из резервной копии

Удалим файлы кластера beta.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta

Page 26: Page 26

Source slide page 26: Page 26{#source-figure-026 caption=“Source slide page 26: Page 26”}

postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta

Извлечем файлы из архива.

postgres$ tar xjf /var/lib/postgresql/archive.backup/base.tar.bz2 -C /var/lib/postgresql/16/beta

Проверим, восстановлены ли данные.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_base
β=> SELECT * FROM t;
           creatime
-------------------------------
 2026-04-10 18:13:49.437998+03
(1 row)

Уборка

α=> \c student
You are now connected to database "student" as user "student".
α=> DROP DATABASE backup_base;
DROP DATABASE
β=> \c student
You are now connected to database "student" as user "student".
β=> DROP DATABASE backup_base;
DROP DATABASE
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/archive.backup

03 Занятие 03: Резервное копирование / Архив журналов предзаписи

Исходная страница в нативном Markdown: Резервное копирование / Архив журналов предзаписи
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_03_backup_archive.html Archive: DBA3-handouts-16.zip Страниц PDF: 31

Page 1: Резервное копирование / Архив журналов предзаписи

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Файловый архив — непрерывная архивация
  • Потоковый архив — утилита pg_receivewal
  • Восстановление с использованием архива
  • Очистка архива

Page 3: Непрерывная архивация

Source slide page 3: Непрерывная архивация{#source-figure-003 caption=“Source slide page 3: Непрерывная архивация”}

  • archive_mode = on
  • archive_command
  • archive_library
  • archive_timeout

Комментарии преподавателя

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

Если же к автономной копии добавлять новые файлы журнала предзаписи, генерируемые сервером, можно будет восстановить кластер на произвольный момент времени. На практике заполненные журнальные сегменты копируются не в саму копию, а в отдельный архив. Этим занимается процесс archiver при значении параметра archive_mode = on.

В PostgreSQL есть два способа копирования сегмента WAL в архив:

  1. Командой ОС. При переключении на очередной сегмент WAL процесс выполняет команду, которую он получает из параметра archive_command.

  2. С помощью функций модуля архивирования (начиная с версии 15). Имя модуля задается параметром archive_library. https://postgrespro.ru/docs/postgresql/16/archive-modules

На слабо загруженных серверах параметром archive_timeout задают максимальное время, которое может пройти до переключения на новый сегмент WAL. Переключение можно выполнить и вручную, используя функцию pg_switch_wal.

https://postgrespro.ru/docs/postgresql/16/continuous-archiving

Page 4: archive_command

Команда ОС, архивирующая заполненный сегмент WAL

  • копирует файл %p в архив под именем %f,
  • сам архив может быть организован любым образом
  • должна завершаться со статусом 0 только при успехе
  • не должна перезаписывать уже существующие файлы
  • должна гарантировать запись в энергонезависимую память
  • удобно реализовать нужную логику в собственном скрипте
  • Мониторинг
    • текущий статус — представление pg_stat_archiver
    • удобно включить коллектор сообщений (logging_collector = on)
    • и получать диагностику в журнале сообщений сервера

Комментарии преподавателя

Команда архивирования должна скопировать указанный файл в некий архив. Архив может быть организован любым образом. Например, это может быть файловая система на отдельном сервере.

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

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

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

Текущий статус архивации показывает представление pg_stat_archiver. Удобно включить сбор сообщений с помощью процесса logging_collector, так как в этом случае сообщения об ошибках при выполнения archive_command будут попадать в журнал сервера.

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

https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-ARCHIV E-COMMAND

Page 5: Настройка непрерывной архивации

Архив будем хранить в каталоге /var/lib/postgresql/archive. Он должен быть доступен пользователю-владельцу PostgreSQL.

postgres$ mkdir /var/lib/postgresql/archive

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

α=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM

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

α=> ALTER SYSTEM SET archive_command =
'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f';
ALTER SYSTEM

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

SET archive_command =
'test ! -f /var/lib/postgresql/archive/%f && gzip <%p >/var/lib/postgresql/archive/%f';

В идеале команда архивирования должна выполнять sync, чтобы файл гарантированно попал в энергонезависимую память. Иначе при сбое он может пропасть из архива, а сервер может успеть стереть его из каталога pg_wal. Обычно удобно поместить всю необходимую логику архивирования в отдельный скрипт и вызывать его:

SET archive_command = 'archive.sh "%f" "%p"';

Изменение archive_mode требует рестарта сервера.

student$ sudo pg_ctlcluster 16 alpha restart
student$ psql

Появился процесс archiver:

α=> SELECT pid, backend_type, backend_start
FROM pg_stat_activity
WHERE backend_type='archiver';
  pid  | backend_type |         backend_start
-------+--------------+-------------------------------
 67529 | archiver     | 2026-04-10 18:02:42.120579+03
(1 row)

Проверим работу архивации. Создадим базу и таблицу.

α=> CREATE DATABASE backup_archive;
CREATE DATABASE
α=> \c backup_archive
You are now connected to database "backup_archive" as user "student".
α=> CREATE TABLE t(s text);
CREATE TABLE
α=> INSERT INTO t VALUES ('Строка №1');
INSERT 0 1

Вот какой сегмент WAL используется сейчас:

α=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000010000000000000001
(1 row)

Page 6: Page 6

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

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/1FB66D0
(1 row)
α=> INSERT INTO t VALUES ('И еще одна строка');
INSERT 0 1

Сегмент сменился:

α=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000010000000000000002
(1 row)

А предыдущий должен был попасть в архив. Проверим:

student$ sudo ls -lh /var/lib/postgresql/archive
total 16M
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000001

Текущий статус архивации показывает представление pg_stat_archiver:

α=> SELECT * FROM pg_stat_archiver \gx
-[ RECORD 1 ]------+------------------------------
archived_count     | 1
last_archived_wal  | 000000010000000000000001
last_archived_time | 2026-04-10 18:02:50.111998+03
failed_count       | 0
last_failed_wal    |
last_failed_time   |
stats_reset        | 2025-11-25 18:23:08.681015+03

Таким образом, файловая архивация настроена и работает.

Page 7: pg_receivewal

Source slide page 7: pg_receivewal{#source-figure-007 caption=“Source slide page 7: pg_receivewal”}

  • max_wal_senders
  • max_replication_slots

Комментарии преподавателя

Можно организовать пополнение архива иным способом, с помощью протокола репликации. Для этого используется утилита pg_receivewal.

Обычно утилита запускается на отдельном сервере, хранящем архив. Она формирует файлы аналогично тому, как это делает сам сервер, и записывает их в указанный каталог. Не до конца заполненные сегменты имеют суффикс .partial. По умолчанию синхронизация кеша с файловой системой происходит только при закрытии сегмента WAL, но можно сделать запись синхронной, задав параметр –synchronous.

Утилита может сжимать сегменты WAL. Для этого в параметре –compress нужно указать алгоритм (gzip или lz4) и уровень сжатия.

При первом запуске (когда архив пуст) архивирование начнется с начала текущего сегмента. Обычно же архивирование начинается с начала сегмента, следующего за последним заполненным сегментом в архиве. При использовании слота репликации это гарантирует отсутствие пропусков в архиве, даже если утилита отключалась на некоторое время.

По умолчанию утилита получает записи бесконечно, но можно завершить ее работу при получении записи с заданным LSN, если указать –endpos=lsn.

Учтите, что утилита не запускается автоматически как сервис и не демонизируется.

https://postgrespro.ru/docs/postgresql/16/app-pgreceivewal

Page 8: Потоковый архив

Чтобы настроить пополнение архива по протоколу потоковой репликации, остановим сначала файловую архивацию.

α=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Текущее состояние архива.

student$ sudo ls -lh /var/lib/postgresql/archive
total 32M
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000001
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000002

Сначала попросим утилиту pg_receivewal создать слот, чтобы гарантировать получение всех записей журнала.

postgres$ /usr/lib/postgresql/16/bin/pg_receivewal --create-slot --slot=archive

Затем запустим ее фоном в режиме архивации. Увидев, что в архиве уже есть файлы, утилита запросит у сервера следующий сегмент, чтобы в архиве не было пропусков.

postgres$ /usr/lib/postgresql/16/bin/pg_receivewal -D /var/lib/postgresql/archive --slot=archive &

Добавим в таблицу много строк и удалим их.

student$ psql -d backup_archive
α=> INSERT INTO t SELECT 'И снова здравствуйте.' FROM generate_series(1,200_000);
INSERT 0 200000
α=> DELETE FROM t WHERE s = 'И снова здравствуйте.';
DELETE 200000
α=> VACUUM t;
VACUUM

В архиве появились новые файлы.

student$ sudo ls -lh /var/lib/postgresql/archive
total 64M
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000001
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000002
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000003
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000004.partial

Последний файл, скорее всего, имеет суффикс .partial — в него идет запись. Теперь мы настроили потоковую архивацию.

Page 9: Базовая копия

Source slide page 9: Базовая копия{#source-figure-009 caption=“Source slide page 9: Базовая копия”}

Комментарии преподавателя

Если мы располагаем настроенным архивом журнала предзаписи, то в базовой резервной копии файлы журнала перестают быть обязательными — ведь их при восстановлении можно получить из архива. Поэтому pg_basebackup можно запускать с ключом –wal-method=none.

(Но и никакого вреда от журнальных файлов внутри базовой копии, кроме занимаемого объема, тоже нет. Более того, они позволят восстановить систему в ситуации, если архив окажется недоступен.)

Page 10: Базовая резервная копия

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

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=none \
  --pgdata /home/student/tmp/backup
NOTICE:  WAL archiving is not enabled; you must ensure that all required WAL segments are
copied through other means to complete the backup

В каталоге pg_wal резервной копии сегментов WAL нет:

student$ ls -l /home/student/tmp/backup/pg_wal/
total 4
drwx------ 2 student student 4096 апр 10 18:03 archive_status

Зато в архиве прибавилось файлов:

student$ sudo ls -lh /var/lib/postgresql/archive
total 96M
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000001
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000002
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000003
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000004
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000005
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000006.partial

Page 11: Восстановление

Source slide page 11: Восстановление{#source-figure-011 caption=“Source slide page 11: Восстановление”}

recovery.signal

  • сигнализирует о восстановлении из резервной копии tablespace_map
    • пути для табличных пространств
  • backup_label
    • начальная позиция и др.
  • postgresql.conf
    • restore_command
    • целевая точка
    • и др.

Комментарии преподавателя

Процессом восстановления управляют три файла и параметры.

Файл recovery.signal создается вручную. Его наличие дает указание серверу перейти в режим восстановления. Содержимое файла игнорируется. Если recovery.signal отсутствует, то PostgreSQL считает, что он выполняет автоматическое восстановление после сбоя. Если же файл есть, сервер понимает, что происходит управляемое пользователем восстановление из резервной копии и обращает внимание на два других файла, которые генерируются при создании базовой резервной копии:

● Файл tablespace_map содержит пути для табличных пространств. Если символические ссылки в pg_tblspc отсутствуют (это верно для формата tar), то они будут созданы при старте сервера на основании информации в tablespace_map.

С этим файлом мы уже встречались в теме «Базовая резервная копия».

● Файл метки backup_label содержит название, время создания копии и — самое важное — название сегмента WAL и позицию в нем, с которой надо начинать восстановление.

При восстановлении процесс startup читает файлы из архива с помощью команды из параметра restore_command и применяет журнальные записи от начальной позиции, указанной в файле метки, до целевой точки восстановления.

Page 12: Настройка восстановления

Настройку восстановления начнем с создания сигнального файла.

student$ touch /home/student/tmp/backup/recovery.signal

При создании базовой резервной копии был создан файл метки backup_label.

student$ cat /home/student/tmp/backup/backup_label
START WAL LOCATION: 0/5000028 (file 000000010000000000000005)
CHECKPOINT LOCATION: 0/5000060
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2026-04-10 18:03:00 MSK
LABEL: pg_basebackup base backup
START TIMELINE: 1

Главная информация в этом файле — начальная точка для восстановления (START WAL LOCATION). Теоретически восстановление можно начать и с более ранней (но не более поздней) позиции, но это потребует больше времени. Поскольку в нашем кластере нет дополнительных табличных пространств, файл tablespace_map не нужен. Осталось задать в конфигурационном файле restore_command и другие параметры.

Page 13: restore_command

Команда ОС, восстанавливающая сегмент WAL

  • выполняет действие, «обратное» archive_command
  • копирует файл %f из архива на сервер по пути %p
  • должна завершаться со статусом 0 только при успехе
  • (может вызываться для несуществующих файлов)
  • Мониторинг
    • log_startup_progress_interval = 10s
    • журналирование статуса процесса startup

Комментарии преподавателя

Параметр restore_command определяет команду, «обратную» команде архивирования archive_command. Она должна скопировать файл из архива в каталог pg_wal и завершаться со статусом 0 только при успехе. Это очень важно, так как в процессе восстановления сервер может выполнять команду восстановления для файлов, которых не окажется в архиве — это не ошибка, но команда должна завершиться с ненулевым статусом.

Начиная с 14-й версии PostgreSQL изменение параметра restore_command не требует рестарта сервера.

https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#RUNTIME-CO NFIG-WAL-ARCHIVE-RECOVERY

С 15-й версии за состоянием процесса восстановления можно следить в журнале сообщений сервера при включенном параметре log_startup_progress_interval. При значении по умолчанию информация сбрасывается в журнал раз в 10 секунд.

В более ранних версиях информация о текущем состоянии восстановления отображалась только в имени unix-процесса startup.

Page 14: Точка перезапуска

Аналог контрольной точки при восстановлении

  • сброс грязных буферов
  • запись в управляющий файл
  • удаление ненужных сегментов WAL
  • Выполняется при применении записи о контрольной точке
    • если после точки перезапуска прошло не менее checkpoint_timeout
    • или объем WAL приближается к max_wal_size
  • Мониторинг
    • log_checkpoints
    • pg_stat_bgwriter

Комментарии преподавателя

Когда при восстановлении процесс startup применяет журнальные записи, данные в общей памяти сервера изменяются так же, как при обычной работе: появляются грязные буферы. Если произойдет сбой, после него нужно будет повторять восстановление, и на этот случай необходимо хранить журнальные записи. Чтобы ограничить объем хранимых записей и не начинать восстановление с самого начала, сервер периодически выполняет точки перезапуска (restart points), сбрасывая, как при контрольной точке, грязные буферы структур общей памяти: буферного кеша, кеша clog и других. При успешном завершении точки перезапуска сервер делает запись в управляющем файле и удаляет сегменты WAL, которые целиком предшествуют ее началу.

Точка перезапуска выполняется, когда процесс startup встречает в WAL запись о контрольной точке, если после последней точки перезапуска прошло не менее checkpoint_timeout времени или если объем журнала приближается к значению max_wal_size. Иными словами, точка перезапуска инициируется журнальной записью о контрольной точке, но если ни одно из условий не выполняется, запись пропускается.

Если включен параметр log_checkpoints, в журнал сообщений будет попадать информация обо всех точках перезапуска, а представление pg_stat_bgwriter показывает накопленную статистику.

https://postgrespro.ru/docs/postgresql/16/wal-configuration

Page 15: Цель восстановления

Source slide page 15: Цель восстановления{#source-figure-015 caption=“Source slide page 15: Цель восстановления”}

Восстановление до определенной точки (PITR)

  • recovery_target = ‘immediate’
  • только восстановить согласованность
  • recovery_target_name = ‘имя’
  • до именованной точки, созданной
  • pg_create_restore_point(‘имя’)
  • recovery_target_time = ‘время’
  • до указанного времени
  • recovery_target_xid = ‘xid’
  • до указанной транзакции
  • recovery_target_lsn = ’lsn'
  • до указанного LSN
  • recovery_target_inclusive = on|off
  • включать ли указанную точку
  • recovery_target_action = pause

pause

  • promote
  • принимает подключения
  • shutdown
  • останавливается

Комментарии преподавателя

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

● Параметр recovery_target = ‘immediate’ остановит восстановление, как только будет достигнута согласованность. Фактически, это эквивалентно восстановлению из базовой копии без архива.

● Параметр recovery_target_name позволяет указать именованную целевую точку восстановления, созданную заранее с помощью функции pg_create_restore_point.

● Параметр recovery_target_time позволяет указать произвольное время (timestamp), recovery_target_xid — произвольную транзакцию, recovery_target_lsn — номер LSN. С помощью параметра recovery_target_inclusive можно включить или исключить саму точку.

Параметр recovery_target_action позволяет выбрать состояние, в котором окажется сервер по достижении целевой точки:

● При значении pause восстановление приостанавливается. Администратор может выполнить запросы и решить, корректно ли была выбрана цель, после чего либо завершить восстановление (функция pg_wal_replay_resume), либо задать новую цель, перезапустить сервер и продолжить применение записей WAL.

● Если задано значение promote, сервер сразу сможет принимать запросы на подключения.

● Значение shutdown приведет к остановке сервера. При этом файл recovery.signal не удаляется.

Page 16: Окончание восстановления

Source slide page 16: Окончание восстановления{#source-figure-016 caption=“Source slide page 16: Окончание восстановления”}

recovery.signal удаляется

  • backup_label backup_label.old
  • выполняется recovery_end_command
  • проблемы диагностируются по журналу сообщений

Комментарии преподавателя

После того как восстановление закончено, процесс startup завершается, postmaster запускает остальные служебные процессы, необходимые для работы экземпляра, и сервер начинает работать в обычном режиме. Файл recovery.signal удаляется, backup_label переименовывается в backup_label.old.

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

Возможна ситуация, когда запущенный сервер не стартует. В этом случае в операционной системе будут отсутствовать процессы экземпляра (и не будет файла postmaster.pid), а recovery.signal не будет удален. Причину ошибки можно узнать из журнала сообщений сервера (например, в параметрах была допущена ошибка). После исправления причины ошибки сервер следует запустить еще раз.

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

Page 17: Восстановление из базовой резервной копии

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

student$ echo "restore_command = 'cp /var/lib/postgresql/archive/%f %p'" \
  >> /home/student/tmp/backup/postgresql.auto.conf

Если не указана целевая точка восстановления (один из параметров recovery_target*), то к базовой резервной копии будут применены записи WAL из всех файлов в архиве. Выкладываем резервную копию в каталог данных сервера beta и запускаем его.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/beta
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 beta start

После успешного восстановления файл recovery.signal удаляется, а файл метки переименовывается в backup_label.old:

student$ sudo ls -l /var/lib/postgresql/16/beta | egrep 'recovery|backup_label'
-rw------- 1 postgres postgres    225 апр 10 18:03 backup_label.old

Поскольку были применены все записи WAL, сервер beta сразу начинает принимать подключения. Проверим, что восстановлено в таблице:

student$ psql -p 5433 -d backup_archive
β=> SELECT * FROM t;
         s
-------------------
 Строка №1
 И еще одна строка
(2 rows)

Накопительная статистика сервера содержит информацию о точках перезапуска:

β=> SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint
FROM pg_stat_bgwriter \gx
-[ RECORD 1 ]---------+---
checkpoints_timed     | 0
checkpoints_req       | 1
checkpoint_write_time | 31
checkpoint_sync_time  | 15
buffers_checkpoint    | 0

Page 18: Линии времени

Source slide page 18: Линии времени{#source-figure-018 caption=“Source slide page 18: Линии времени”}

Порядковый номер, +1 при каждом восстановлении

  • номер N линии времени входит в имя сегмента WAL
  • история сохраняется в файле pg_wal/N.history и архивируется
  • recovery_target_timeline = ’latest’ восстановление по последней линии (по умолчанию)
  • recovery_target_timeline = ‘current’ восстановление по текущей линии
  • recovery_target_timeline = ‘1’ восстановление по указанной линии

Комментарии преподавателя

После восстановления на момент в прошлом сервер начинает генерировать новые записи WAL, номера которых будут повторять номера записей «из прошлой жизни». Чтобы не потерять записи и, вместе с ними, возможность восстановления на другой момент, PostgreSQL вводит понятие линии времени. После каждого восстановления с использованием recovery.signal номер линии времени увеличивается, и этот номер является частью номера сегментов WAL.

Линии времени образуют древовидную структуру, на рисунках приведен пример. Первая линия имеет номер 1. Произошло восстановление на момент времени в прошлом, и началась линия 2.

Если выполнить восстановление с указанием параметра recovery_target_timeline = ’latest’ (по умолчанию), то, дойдя то точки ветвления, применение записей WAL продолжится по второй (более поздней) линии.

Если же указать параметр recovery_target_timeline = ‘1’, то восстановление продолжится по первой (указанной) линии, а если указать recovery_target_timeline = ‘current’, то по текущей (в нашем случае тоже по линии 1).

Информацию о точках ветвления PostgreSQL берет из файлов истории pg_wal/N.history. Поэтому их никогда не следует удалять из архива.

Page 19: Линии времени

Что стало с линией времени после восстановления?

β=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000020000000000000006
(1 row)

Номер увеличился на единицу. В каталоге pg_wal появился файл истории, соответствующий этой ветви:

postgres$ ls /var/lib/postgresql/16/beta/pg_wal/00000002.history
/var/lib/postgresql/16/beta/pg_wal/00000002.history

В нем есть информация о «точках ветвления», через которые мы пришли в данную линию времени:

student$ sudo cat /var/lib/postgresql/16/beta/pg_wal/00000002.history
1
0/6000000
no recovery target specified

Эти файлы PostgreSQL использует, когда мы указываем линию времени в параметре recovery_target_timeline. Поэтому файлы истории подлежат архивации вместе с сегментами WAL, и удалять из архива их не надо.

Page 20: Очистка архива

Source slide page 20: Очистка архива{#source-figure-020 caption=“Source slide page 20: Очистка архива”}

Комментарии преподавателя

Архив со временем разрастается, поэтому требуется периодическая очистка от ненужных файлов.

Для очистки можно воспользоваться утилитой pg_archivecleanup. Утилите нужно указать имя сегмента WAL, и она удалит все ему предшествующие.

https://postgrespro.ru/docs/postgresql/16/pgarchivecleanup

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

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

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

Page 21: Очистка архива

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

student$ sudo ls -lh /var/lib/postgresql/archive
total 96M
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000001
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000002
-rw------- 1 postgres postgres 16M апр 10 18:02 000000010000000000000003
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000004
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000005
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000006.partial

Утилите pg_archivecleanup нужно передать путь к архиву и имя последнего сохраняемого сегмента WAL (его можно найти в файле backup_label).

student$ sudo head -n 1 /var/lib/postgresql/16/beta/backup_label.old
START WAL LOCATION: 0/5000028 (file 000000010000000000000005)
student$ sudo pg_archivecleanup /var/lib/postgresql/archive 000000010000000000000005

Посмотрим, какие файлы остались в архиве:

student$ sudo ls -lh /var/lib/postgresql/archive
total 32M
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000005
-rw------- 1 postgres postgres 16M апр 10 18:03 000000010000000000000006.partial

Архив очищен. Утилиту можно вызывать сразу после формирования базовой резервной копии, поместив обе команды в скрипт.

Page 22: Итоги

Физическое резервирование — базовые резервные копии и архив журнала предзаписи

  • Хорошо подходит
    • для текущего периодического резервирования
    • для восстановления после сбоя с минимальной потерей данных
    • для восстановления на произвольный момент времени
    • для резервирования данных большого объема
  • Плохо подходит
    • для длительного хранения
  • Не подходит
    • для миграции на другую платформу

Page 23: Практика

  1. На сервере alpha создайте базу данных и таблицу с данными. 2. Настройте непрерывное архивирование. 3. Сделайте копию кластера без файлов журнала. 4. Вставьте еще несколько строк в таблицу и убедитесь, что текущий сегмент WAL попал в архив. 5. Восстановите сервер beta из резервной копии, указав в параметрах одну только команду восстановления. 6. Снова восстановите сервер beta из той же резервной копии, на этот раз указав целевую точку восстановления immediate.

Комментарии преподавателя

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

Page 24: 1. База данных и таблица

α=> CREATE DATABASE backup_archive;
CREATE DATABASE
α=> \c backup_archive
You are now connected to database "backup_archive" as user "student".
α=> CREATE TABLE t(s text);
CREATE TABLE
α=> INSERT INTO t VALUES ('Привет, мир!');
INSERT 0 1

2. Настройка непрерывной архивации

postgres$ mkdir /var/lib/postgresql/archive
α=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM
α=> ALTER SYSTEM SET archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

3. Базовая резервная копия

Поскольку нам потребуется восстанавливаться из одной копии два раза, сделаем ее в формате tar со сжатием.

student$ pg_basebackup \
  --checkpoint=fast \
  --wal-method=none \
  --format=tar \
  --gzip \
  --pgdata=/home/student/tmp/backup
NOTICE:  all required WAL segments have been archived
student$ ls -l /home/student/tmp/backup
total 5324
-rw------- 1 student student  224439 апр 10 18:14 backup_manifest
-rw------- 1 student student 5224402 апр 10 18:14 base.tar.gz

4. Добавление строк в таблицу

student$ psql -d backup_archive
α=> INSERT INTO t VALUES ('Еще одна строка');
INSERT 0 1

Переключаем сегмент:

α=> SELECT pg_walfile_name(pg_current_wal_lsn()), pg_switch_wal();
     pg_walfile_name      | pg_switch_wal
--------------------------+---------------
 000000010000000000000003 | 0/3006658
(1 row)

Проверяем статус архивации:

α=> SELECT last_archived_wal FROM pg_stat_archiver;
    last_archived_wal
--------------------------
 000000010000000000000003
(1 row)

5. Восстановление из базовой резервной копии

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.

Page 25: Page 25

postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta

Для простоты отключим непрерывное архивирование на резервном сервере и укажем только команду восстановления:

postgres$ echo "restore_command = 'cp /var/lib/postgresql/archive/%f %p' " \
  | tee /var/lib/postgresql/16/beta/postgresql.auto.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'

Создаем recovery.signal.

postgres$ touch /var/lib/postgresql/16/beta/recovery.signal

Запускаем сервер.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_archive
β=> SELECT * FROM t;
        s
-----------------
 Привет, мир!
 Еще одна строка
(2 rows)

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

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

6. Восстановление из базовой резервной копии — immediate

student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta

Создаем recovery.signal, в конфигурационный файл записываем команду восстановления и целевую точку:

postgres$ touch /var/lib/postgresql/16/beta/recovery.signal
postgres$ cat << EOF | tee /var/lib/postgresql/16/beta/postgresql.auto.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target = 'immediate'
EOF
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target = 'immediate'

Запускаем сервер.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_archive
β=> SELECT * FROM t;
      s
--------------
 Привет, мир!
(1 row)

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

β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();

Page 26: Page 26

Source slide page 26: Page 26{#source-figure-026 caption=“Source slide page 26: Page 26”}

 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | paused
(1 row)

Функция pg_get_wal_replay_pause_state() возвращает: not paused — приостановка не запрашивалась; pause requested — запрошена приостановка; paused — восстановление приостановлено. Поскольку нам не нужно применять последующие записи, завершаем восстановление, переводя сервер в обычный режим (нужны права суперпользователя):

β=> SELECT pg_wal_replay_resume();
 pg_wal_replay_resume
----------------------
(1 row)
β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

Чтобы не выходить из режима восстановления вручную, можно было заранее задать значение recovery_target_action = ‘promote’.

Уборка

Удалим резервную копию и базы данных, сбросим настройки параметров конфигурации.

student$ rm -rf /home/student/tmp/backup
β=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
β=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop
α=> \c student postgres
You are now connected to database "student" as user "postgres".
α=> DROP DATABASE backup_archive;
DROP DATABASE
α=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart
postgres$ rm -rf /var/lib/postgresql/archive

Page 27: Практика+

  1. На сервере alpha создайте таблицу, в которую будут периодически записываться данные. Создайте физическую резервную копию и настройте непрерывную архивацию WAL. 2. Вставьте в подготовленную таблицу трижды по 100 строк (данные за январь, февраль и март), отмечая время сразу после вставки. Между вставками удалите некоторые строки: сымитируйте ситуацию, в которой пользователь случайно удаляет часть данных в неизвестный момент, но проблему замечают не сразу. 3. Восстановите удаленные данные, не останавливая при этом сервер alpha.

Комментарии преподавателя

  1. Используйте сервер beta как вспомогательный.

Page 28: 1. Таблица и резервирование с архивом WAL

α=> CREATE DATABASE backup_archive;
CREATE DATABASE
α=> \c backup_archive
You are now connected to database "backup_archive" as user "student".

Подготовим таблицу:

α=> CREATE TABLE t (
  id integer PRIMARY KEY,
  month text
);
CREATE TABLE

После формирования структуры данных создадим физическую копию:

postgres$ pg_basebackup \
  --checkpoint=fast \
  --pgdata /var/lib/postgresql/backup

Архив сегментов WAL:

postgres$ mkdir /var/lib/postgresql/archive

Добавим файл параметров архивирования в каталог дополнительной конфигурации.

postgres$ cat << EOF | tee /etc/postgresql/16/alpha/conf.d/arch.conf
archive_mode = 'on'
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f'
archive_timeout = '30s'
EOF
archive_mode = 'on'
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p
/var/lib/postgresql/archive/%f'
archive_timeout = '30s'

Перезапускаем сервер.

student$ sudo pg_ctlcluster 16 alpha restart
student$ psql -d backup_archive

2. Вставка данных в таблицу

Наше условное приложение добавляет в таблицу 100 строк в конце каждого месяца. Вставка строк за январь:

α=> INSERT INTO t SELECT g.i, 'Январь' FROM generate_series(1,100) AS g(i);
INSERT 0 100

Запомним момент времени после завершения вставки:

α=> SELECT now();
             now
------------------------------
 2026-04-10 18:16:16.26225+03
(1 row)

Пользователь по ошибке удаляет часть данных:

α=> DELETE FROM t WHERE id BETWEEN 42 AND 111;
DELETE 59

Вставим данные за февраль:

α=> INSERT INTO t SELECT g.i, 'Февраль' FROM generate_series(101,200) AS g(i);
INSERT 0 100

Page 29: Page 29

Снова запомним момент времени после окончания транзакции:

α=> SELECT now();
              now
-------------------------------
 2026-04-10 18:16:26.577825+03
(1 row)

Пользователь зачем-то еще раз выполняет ту же команду:

α=> DELETE FROM t WHERE id BETWEEN 42 AND 111;
DELETE 11

Но ошибку пока не заметили. Выполняем еще одну вставку:

α=> INSERT INTO t SELECT g.i, 'Март' FROM generate_series(201,300) AS g(i);
INSERT 0 100

Опять запомним момент времени:

α=> SELECT now();
              now
-------------------------------
 2026-04-10 18:16:36.882398+03
(1 row)

Теперь ошибка обнаружена:

α=> SELECT month, count(*) FROM t GROUP BY month;
  month  | count
---------+-------
 Февраль |    89
 Март    |   100
 Январь  |    41
(3 rows)

3. Восстановление с помощью архива

Задача: восстановить данные, не останавливая сервер alpha. Пропали данные за январь и февраль, поэтому план действий таков: на сервере beta восстанавливаем, используя архив WAL, данные на конец загрузки январских строк; выгружаем логическую копию данных за январь; заново восстанавливаем данные, но теперь на конец загрузки февральских строк; выгружаем логическую копию данных за февраль (январские данные в этот момент могут быть повреждены); загружаем восстановленные данные в таблицу приложения на сервере alpha. Остановим сервер beta и поместим физическую копию в его каталог данных.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mv /var/lib/postgresql/backup /var/lib/postgresql/16/beta

Необходимо создать файл recovery.signal:

postgres$ touch /var/lib/postgresql/16/beta/recovery.signal

Мы хотим восстановить состояние сервера на определенный момент времени, то есть выполнить point-in-time recovery (PITR). Зададим команду восстановления и укажем целевую точку восстановления на запомненный момент сразу после загрузки данных за январь:

postgres$ cat << EOF | tee /etc/postgresql/16/beta/conf.d/recovery.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target_time = '2026-04-10 18:16:16.26225+03'
EOF
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target_time = '2026-04-10 18:16:16.26225+03'

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

Page 30: Page 30

Архивация происходит при начале записи в следующий сегмент.

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/301D630
(1 row)
α=> CHECKPOINT;
CHECKPOINT

Запускаем сервер beta.

student$ sudo pg_ctlcluster 16 beta start
student$ psql -p 5433 -d backup_archive

Проверим, что данные за январь восстановлены:

β=> SELECT month, count(*) FROM t GROUP BY month;
 month  | count
--------+-------
 Январь |   100
(1 row)

Сервер beta находится в состоянии восстановления:

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

Однако это не помешает выгрузить логическую копию данных.

postgres$ mkdir /var/lib/postgresql/tmp
β=> COPY (SELECT * FROM t WHERE month = 'Январь') TO '/var/lib/postgresql/tmp/jan.dump';
COPY 100

Переходим к февралю. Указываем новое время:

postgres$ sed -i "/recovery_target_time/c\recovery_target_time = '2026-04-10 18:16:26.577825+03'"
/etc/postgresql/16/beta/conf.d/recovery.conf
postgres$ cat /etc/postgresql/16/beta/conf.d/recovery.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
recovery_target_time = '2026-04-10 18:16:26.577825+03'

Чтобы продолжить восстановление, перезапустим сервер beta.

student$ sudo pg_ctlcluster 16 beta restart
student$ psql -p 5433 -d backup_archive

Проверяем восстановленные данные:

β=> SELECT month, count(*) FROM t GROUP BY month;
  month  | count
---------+-------
 Февраль |   100
 Январь  |    41
(2 rows)

Мы видим, что февральские строки на месте, а январские уже повреждены. Выгружаем февраль:

β=> COPY (SELECT * FROM t WHERE month = 'Февраль') TO '/var/lib/postgresql/tmp/feb.dump';
COPY 100

Теперь восстановим выгруженные данные за два месяца на сервере alpha.

α=> DELETE FROM t WHERE month IN ('Январь', 'Февраль');
DELETE 130

Page 31: Page 31

α=> COPY t FROM '/var/lib/postgresql/tmp/jan.dump';
COPY 100
α=> COPY t FROM '/var/lib/postgresql/tmp/feb.dump';
COPY 100

Все получилось:

α=> SELECT month, count(*) FROM t GROUP BY month;
  month  | count
---------+-------
 Февраль |   100
 Январь  |   100
 Март    |   100
(3 rows)

Начиная с версии PostgreSQL 17 можно было бы не удалять строки, а воспользоваться командой COPY с параметром ON ERROR ignore, чтобы пропустить уже существующие в таблице.

Уборка

Удалим базы данных и настройки параметров конфигурации.

student$ sudo pg_ctlcluster 16 beta stop
α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE backup_archive;
DROP DATABASE
postgres$ rm /etc/postgresql/16/beta/conf.d/recovery.conf
student$ sudo pg_ctlcluster 16 alpha restart

Удалим ненужные файлы.

postgres$ rm -rf /var/lib/postgresql/{tmp,archive,backup}

04 Занятие 04: Репликация / Физическая репликация

Исходная страница в нативном Markdown: Репликация / Физическая репликация
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_04_replica_physical.html Archive: DBA3-handouts-16.zip Страниц PDF: 39

Page 1: Репликация / Физическая репликация

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Задачи репликации
  • Схема работы физической репликации
  • Способы доставки журнальных записей
  • Особенности и ограничения использования реплики
  • Синхронная и асинхронная репликация
  • Мониторинг репликации
  • Возможные проблемы и способы их решения

Page 3: Задачи репликации

Репликация

  • процесс синхронизации нескольких копий кластера баз данных
  • на разных серверах
  • Базовый механизм для решения ряда задач
    • отказоустойчивость
    • при возникновении сбоя система
    • должна сохранить работоспособность
    • масштабируемость
    • распределение нагрузки между серверами

Комментарии преподавателя

Одиночный сервер, управляющий базами данных, может не удовлетворять требованиям, предъявляемым к системе. Есть две основных задачи, для решения которых требуется наличие нескольких серверов.

Во-первых, отказоустойчивость: один физический сервер — это возможная точка отказа. Если сервер выходит из строя, система становится недоступной.

Во-вторых, производительность. Один сервер может не справляться с нагрузкой. Зачастую желательна возможность не вертикального, а горизонтального масштабирования — распределения нагрузки на несколько серверов. Дело может быть как в стоимости аппаратуры, так и в необходимости различных настроек для разных типов нагрузки (например, для OLTP и отчетности).

В случае PostgreSQL распределенные системы строятся по принципу «shared nothing»: несколько серверов работают независимо друг от друга и не имеют ни общей оперативной памяти, ни общих дисков. Следовательно, если серверы должны работать с одними и теми же данными, то эти данные требуется синхронизировать между ними. Механизм синхронизации и называется репликацией.

Page 4: Мастер→реплика

Записи WAL передаются на реплику и применяются

  • поток данных только в одну сторону
  • реплицируется кластер целиком, выборочная репликация невозможна
  • Реплика — точная копия мастера
    • одна и та же основная версия сервера
    • полностью совместимые архитектуры и платформы
  • Реплика доступна только для чтения

Комментарии преподавателя

При физической репликации серверы имеют назначенные роли: один является ведущим («мастер»), а остальные — ведомыми («реплики»).

Мастер передает на реплику журнальные записи, а реплика применяет эти записи к своим файлам данных. Применение происходит чисто механически, без «понимания смысла» изменений, и выполняется быстрее, чем работало бы повторное выполнение операторов SQL. Но критична двоичная совместимость между серверами — они должны работать на одной и той же программно-аппаратной платформе и на них должны быть запущены одинаковые основные версии PostgreSQL.

https://postgrespro.ru/docs/postgresql/16/hot-standby#HOT-STANDBY-ADM IN

Журнал — общий для всего кластера, поэтому реплицировать можно только кластер целиком, а не отдельные базы данных или таблицы. Возможность «отфильтровать» журнальные записи отсутствует.

Реплика не генерирует журнальные записи, а лишь применяет записи мастера. Поэтому реплика может быть доступна только для чтения — никакие операции, изменяющие данные, на реплике не допустимы.

Page 5: Потоковая репликация

Source slide page 5: Потоковая репликация{#source-figure-005 caption=“Source slide page 5: Потоковая репликация”}

Комментарии преподавателя

При наличие файла recovery.signal сервер восстанавливается из резервной копии: проигрывает журнальные записи, восстанавливает согласованность и завершает восстановление. Запуск реплики выполняется точно так же, но восстановление не завершается: реплика продолжает читать и воспроизводить журнальные записи мастера. Сигналом к этому служит файл standby.signal.

В теме «Базовая резервная копия» мы рассматривали два способа доставки журнальных записей от сервера к клиенту: потоковая и файловая. Они же применяются и для доставки журнальных записей реплике. На практике в основном используется потоковая репликация.

В этом случае процесс wal receiver реплики подключается к мастеру по протоколу репликации, используя строку подключения из параметра primary_conninfo, и читает поток записей WAL. За счет этого при потоковой репликации отставание реплики от мастера минимально (и при необходимости может быть сведено к нулю). Полученные записи пишутся на диск в виде сегментов WAL так же, как это происходит на мастере. Далее процесс startup — тот же самый, что обеспечивает восстановление после сбоев — применяет записи к файлам данных, только теперь он работает постоянно.

Мастер запускает процесс wal sender, обслуживающий соединение с репликой. Потоковая репликация может, если нужно, использовать слот репликации. Имя слота передается в параметре primary_slot_name.

Page 6: Настройка потоковой репликации

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

α=> SELECT pg_create_physical_replication_slot('replica');
 pg_create_physical_replication_slot
-------------------------------------
 (replica,)
(1 row)

Посмотрим на созданный слот:

α=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+---------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         |
confirmed_flush_lsn |
wal_status          |
safe_wal_size       |
two_phase           | f
conflicting         |

Вначале слот не инициализирован (restart_lsn и wal_status пустые). Сформируем автономную резервную копию в каталоге данных сервера beta, используя созданный слот. При запуске с ключом -R утилита добавит в копию файлы, необходимые для запуска будущей реплики.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R --slot=replica

Снова проверим слот:

α=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+----------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/2000000
confirmed_flush_lsn |
wal_status          | reserved
safe_wal_size       |
two_phase           | f
conflicting         |

Page 7: Page 7

После выполнения резервной копии слот инициализировался, и мастер теперь хранит все файлы журнала с начала копирования (restart_lsn, wal_status). Сравните со строкой в backup_label:

postgres$ head -n 1 /var/lib/postgresql/16/beta/backup_label
START WAL LOCATION: 0/2000028 (file 000000010000000000000002)

Файл postgresql.auto.conf был подготовлен утилитой pg_basebackup, поскольку мы указали ключ -R. Этот файл содержит информацию для подключения к мастеру и имя слота репликации:

postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer host=''/var/run/postgresql'' port=5432 sslmode=prefer
sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0
target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'

По умолчанию реплика будет «горячей», то есть сможет выполнять запросы во время восстановления. Если такая возможность не нужна, реплику можно сделать «теплой» (hot_standby = off). Утилита также создала сигнальный файл standby.signal, наличие которого указывает серверу войти в режим постоянного восстановления.

postgres$ ls -l /var/lib/postgresql/16/beta/standby.signal
-rw------- 1 postgres postgres 0 апр 10 18:04
/var/lib/postgresql/16/beta/standby.signal

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

student$ sudo pg_ctlcluster 16 beta start

Page 8: Репликация с архивом

Source slide page 8: Репликация с архивом{#source-figure-008 caption=“Source slide page 8: Репликация с архивом”}

Комментарии преподавателя

Другой способ доставки журнальных записей состоит в том, чтобы предоставить реплике доступ к архиву журналов — точно так же, как при обычном восстановлении из архива (этот механизм рассмотрен в модуле «Резервное копирование»).

Если реплика по каким-то причинам не сможет получить очередную журнальную запись по протоколу репликации (например, из-за обрыва связи), она попробует прочитать ее из архива с помощью команды, заданной в параметре restore_command. При восстановлении связи реплика снова автоматически переключится на использование потоковой репликации.

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

● реплика вынужденно отстает от мастера на время заполнения сегмента;

● мастер ничего не знает о существовании реплики, что в некоторых случаях может привести к проблемам.

Page 9: Процессы

Мастер Реплика

  • wal sender
  • wal receiver
  • startup
  • archiver
  • archiver
  • (archive_mode = always)
  • checkpointer
  • checkpointer
  • (контрольные точки)
  • (точки перезапуска)
  • background writer
  • background writer
  • wal writer
  • autovacuum

Комментарии преподавателя

На мастере и на реплике выполняется разный набор процессов. Кроме уже рассмотренных wal sender (на мастере) и wal receiver и startup (на реплике), работают также другие процессы.

Общие процессы:

● background writer — записывает грязные страницы из буферного кеша на диск (на реплике, для того, чтобы применить журнальную запись, страницы точно так же читаются в буферный кеш и затем записываются в фоновом режиме или при вытеснении).

● checkpointer — выполняет точки перезапуска (restart point, см. тему «Архив журналов предзаписи»). Если в процессе восстановления случится сбой, реплика сможет продолжить с последней точки перезапуска, а не с самого начала.

Процессы только на мастере:

● wal writer — реплика не создает журналы упреждающей записи, а только получает их с мастера.

● autovacuum launcher/worker — реплика не выполняет очистку, ее выполнение на мастере передается реплике через журнал.

Есть также процесс, наличие которого зависит от настройки:

● archiver — при значении archive_mode = on выполняется только на мастере, а при archive_mode = always также и на реплике (подробнее рассматривается в теме «Переключение на реплику»).

Page 10: Процессы реплики

Сравним процессы на мастере:

student$ ps f -C postgres -o pid,command | grep alpha
  69430 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/alpha -c
config_file=/etc/postgresql/16/alpha/postgresql.conf
  69431  \_ postgres: 16/alpha: checkpointer
  69432  \_ postgres: 16/alpha: background writer
  69434  \_ postgres: 16/alpha: walwriter
  69435  \_ postgres: 16/alpha: autovacuum launcher
  69436  \_ postgres: 16/alpha: logical replication launcher
  69479  \_ postgres: 16/alpha: student student [local] idle
  69980  \_ postgres: 16/alpha: walsender postgres [local] streaming 0/3000060

И на реплике:

student$ ps f -C postgres -o pid,command | grep beta
  69975 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/beta -c
config_file=/etc/postgresql/16/beta/postgresql.conf
  69976  \_ postgres: 16/beta: checkpointer
  69977  \_ postgres: 16/beta: background writer
  69978  \_ postgres: 16/beta: startup recovering 000000010000000000000003
  69979  \_ postgres: 16/beta: walreceiver streaming 0/3000060

Page 11: Очистка архива

Source slide page 11: Очистка архива{#source-figure-011 caption=“Source slide page 11: Очистка архива”}

Комментарии преподавателя

В точках перезапуска процесс checkpointer будут удалять ненужные сегменты журнала в локальном каталоге pg_wal.

Если архив используется только для синхронизации одной реплики, можно и его очищать автоматически. Для этого в параметре archive_cleanup_command задается команда, которая будет выполняться по окончании каждой точки перезапуска. В команде удобно вызывать утилиту pg_archivecleanup (вместо %r сервер подставит имя сегмента, содержащего точку):

archive_cleanup_command = ‘pg_archivecleanup /path/to/archive %r’

https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#RUNTIME-CO NFIG-WAL-ARCHIVE-RECOVERY

https://postgrespro.ru/docs/postgresql/16/pgarchivecleanup

Page 12: Использование реплики

Допускаются

  • запросы на чтение данных (select, copy to, курсоры)
  • установка параметров сервера (set, reset)
  • управление транзакциями (begin, commit, rollback…)
  • создание резервной копии (pg_basebackup)
  • Не допускаются
    • любые изменения (insert, update, delete, truncate, nextval…)
    • блокировки, предполагающие изменение (select for update…)
    • команды DDL (create, drop…), в том числе создание временных таблиц
    • команды сопровождения (vacuum, analyze, reindex…)
    • управление доступом (grant, revoke…)

Комментарии преподавателя

Как мы уже говорили, на реплике не допускается любое изменение данных. Более точно, не допускаются:

● изменения таблиц, материализованных представлений, последовательностей;

● блокировки (так как для этого требуется изменение страниц данных);

● команды DDL, включая создание временных таблиц;

● такие команды, как VACUUM и ANALYZE;

● команды управления доступом.

При этом реплика может выполнять запросы на чтение данных, если установлен параметр hot_standby = on. Также будет работать установка параметров сервера и команды управления транзакциями — например, можно начать (читающую) транзакцию с нужным уровнем изоляции.

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

Page 13: Использование реплики

Выполним несколько команд на мастере:

α=> CREATE DATABASE replica_physical;
CREATE DATABASE
α=> \c replica_physical
You are now connected to database "replica_physical" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Привет, мир!');
INSERT 0 1

Проверим реплику:

student$ psql -p 5433
β=> \c replica_physical
You are now connected to database "replica_physical" as user "student".
β=> SELECT * FROM test;
      s
--------------
 Привет, мир!
(1 row)

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

β=> INSERT INTO test VALUES ('Replica');
ERROR:  cannot execute INSERT in a read-only transaction

Вообще реплику от мастера можно отличить с помощью функции:

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

Page 14: Ограничения

Изоляция и многоверсионность

  • большое число точек сохранения задерживает выполнение запросов
  • не поддерживается уровень изоляции Serializable
  • Триггеры и блокировки
    • триггеры и рекомендательные блокировки не работают
  • Резервное копирование с реплики
    • параметр full_page_writes должен быть заранее включен на мастере
    • задержки из-за ожидания контрольной точки и переключения сегментов
  • Порядок изменений
    • каталоги табличных пространств нужно сначала создавать на реплике
    • значения параметров max_… нужно сначала увеличивать на реплике

Комментарии преподавателя

У репликации есть ряд ограничений и особенностей.

Если на мастере выполняется транзакция с числом вложенных транзакций больше 64 (то есть активно используются точки сохранения), создание снимка на реплике будет приостановлено — это означает невозможность некоторое время выполнять запросы.

Не поддерживается уровень изоляции Serializable на реплике.

Триггеры не будут срабатывать на реплике (они выполнились на мастере и реплицируется уже результат их работы). Не будут работать и рекомендательные (advisory) блокировки.

При выполнении резервного копирования с реплики нет технической возможности полноценно управлять мастером, в частности, вызывать контрольные точки и включать параметр full_page_writes. Поэтому параметр нужно включить на мастере заранее, а контрольную точку приходится ждать (что может существенно задержать процесс).

Создание табличных пространств реплицируется, но если у пользователя ОС, под которым работает СУБД, недостаточно прав для создания каталога, произойдет ошибка. Лучше всего сначала создать каталоги на мастере и на всех репликах, а уже затем выполнять команду CREATE TABLESPACE.

Если на мастере увеличить параметр, задающий ограничение какого-либо ресурса, репликация может приостановиться. Это относится к параметрам max_connections, max_prepared_transactions, max_locks_per_transaction, max_wal_senders и max_worker_processes.

Page 15: Мониторинг

Source slide page 15: Мониторинг{#source-figure-015 caption=“Source slide page 15: Мониторинг”}

Комментарии преподавателя

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

  1. появление журнальной записи на мастере; 2) передача записи процессом wal sender; 3) получение записи процессом wal receiver; 4) синхронизация записи с диском (flush); 5) применение полученной записи процессом startup.

Все эти точки обычно отслеживаются на мастере. Первую точку дает функция pg_current_wal_lsn, остальные — представление pg_stat_replication. Реплика передает мастеру статус репликации при каждой записи на диск, но как минимум раз в интервал времени, указанный в wal_receiver_status_interval (по умолчанию — 10 секунд).

Если используется слот репликации, то информацию о нем можно получить из представления pg_replication_slots.

Часть информации о ходе репликации можно получить и на реплике в представлении pg_stat_wal_receiver и с помощью функций pg_last_wal_receive_lsn и pg_last_wal_replay_lsn.

Состояние репликации показывают функции pg_is_in_recovery и pg_get_wal_replay_pause_state. Параметр log_replication_commands включает вывод в журнал сообщений информации о выполняющихся командах протокола репликации.

Page 16: Мониторинг репликации

Сейчас реплика находится в режиме восстановления и применяет журнальные записи:

β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();
 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | not paused
(1 row)

Текущее значение max_connections на мастере:

α=> SHOW max_connections;
 max_connections
-----------------
 100
(1 row)

Если количество пользователей выросло, мы захотим увеличить это ограничение:

α=> ALTER SYSTEM SET max_connections = 150;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

А что с репликой?

β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();
 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | paused
(1 row)

Репликация остановилась. В журнале видим причину:

student$ tail -n 7 /var/log/postgresql/postgresql-16-beta.log
2026-04-10 18:04:40.731 MSK [69978] WARNING:  hot standby is not possible because of
insufficient parameter settings
2026-04-10 18:04:40.731 MSK [69978] DETAIL:  max_connections = 100 is a lower setting
than on the primary server, where its value was 150.
2026-04-10 18:04:40.731 MSK [69978] CONTEXT:  WAL redo at 0/3433088 for
XLOG/PARAMETER_CHANGE: max_connections=150 max_worker_processes=8 max_wal_senders=10
max_prepared_xacts=0 max_locks_per_xact=64 wal_level=replica wal_log_hints=off
track_commit_timestamp=off
2026-04-10 18:04:40.731 MSK [69978] LOG:  recovery has paused
2026-04-10 18:04:40.731 MSK [69978] DETAIL:  If recovery is unpaused, the server will
shut down.
2026-04-10 18:04:40.731 MSK [69978] HINT:  You can then restart the server after making
the necessary configuration changes.
2026-04-10 18:04:40.731 MSK [69978] CONTEXT:  WAL redo at 0/3433088 for
XLOG/PARAMETER_CHANGE: max_connections=150 max_worker_processes=8 max_wal_senders=10
max_prepared_xacts=0 max_locks_per_xact=64 wal_level=replica wal_log_hints=off
track_commit_timestamp=off

Последуем рекомендации: увеличим значение параметра на реплике.

β=> ALTER SYSTEM SET max_connections = 150;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta restart

Проверим реплику:

student$ psql -p 5433 -d replica_physical
β=> SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();

Page 17: Page 17

 pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
 t                 | not paused
(1 row)

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

student$ psql -d replica_physical
α=> \drg student|pg_monitor
                      List of role grants
 Role name  |       Member of        |   Options    | Grantor
------------+------------------------+--------------+----------
 pg_monitor | pg_read_all_settings   | INHERIT, SET | postgres
 pg_monitor | pg_read_all_stats      | INHERIT, SET | postgres
 pg_monitor | pg_stat_scan_tables    | INHERIT, SET | postgres
 student    | pg_checkpoint          | INHERIT, SET | postgres
 student    | pg_create_subscription | INHERIT, SET | postgres
 student    | pg_monitor             | INHERIT, SET | postgres
 student    | pg_read_server_files   | INHERIT, SET | postgres
 student    | pg_write_server_files  | INHERIT, SET | postgres
(8 rows)

Роль pg_monitor входит в роль pg_read_all_stats и наследует ее привилегии.

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 70966
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2026-04-10 18:04:46.571317+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/34375E0
write_lsn        | 0/34375E0
flush_lsn        | 0/34375E0
replay_lsn       | 0/34375E0
write_lag        | 00:00:00.000249
flush_lag        | 00:00:00.007946
replay_lag       | 00:00:00.00842
sync_priority    | 0
sync_state       | async
reply_time       | 2026-04-10 18:04:49.464114+03

Обратите внимание на поля *_lsn (и *_lag) — они показывают отставание реплики на разных этапах. Сейчас все позиции совпадают, отставание нулевое. Теперь вставим в таблицу большое количество строк, чтобы увидеть репликацию в процессе работы.

α=> INSERT INTO test SELECT 'Just a line' FROM generate_series(1,1_000_000);
INSERT 0 1000000
α=> SELECT *, pg_current_wal_lsn() from pg_stat_replication \gx

Page 18: Page 18

-[ RECORD 1 ]------+------------------------------
pid                | 70966
usesysid           | 10
usename            | postgres
application_name   | 16/beta
client_addr        |
client_hostname    |
client_port        | -1
backend_start      | 2026-04-10 18:04:46.571317+03
backend_xmin       |
state              | streaming
sent_lsn           | 0/7320000
write_lsn          | 0/72C0000
flush_lsn          | 0/7220000
replay_lsn         | 0/721FFD0
write_lag          | 00:00:00.358711
flush_lag          | 00:00:00.397451
replay_lag         | 00:00:00.397454
sync_priority      | 0
sync_state         | async
reply_time         | 2026-04-10 18:04:54.015211+03
pg_current_wal_lsn | 0/7915350

Видно, что возникла небольшая задержка. Проверим реплику:

β=> SELECT count(*) FROM test;
  count
---------
 1000001
(1 row)

Все строки успешно доставлены. И еще раз проверим состояние репликации:

α=> SELECT *, pg_current_wal_lsn() from pg_stat_replication \gx
-[ RECORD 1 ]------+------------------------------
pid                | 70966
usesysid           | 10
usename            | postgres
application_name   | 16/beta
client_addr        |
client_hostname    |
client_port        | -1
backend_start      | 2026-04-10 18:04:46.571317+03
backend_xmin       |
state              | streaming
sent_lsn           | 0/7917360
write_lsn          | 0/7917360
flush_lsn          | 0/7917360
replay_lsn         | 0/7917360
write_lag          | 00:00:00.054624
flush_lag          | 00:00:00.067903
replay_lag         | 00:00:00.074573
sync_priority      | 0
sync_state         | async
reply_time         | 2026-04-10 18:04:54.336274+03
pg_current_wal_lsn | 0/7917360

Все позиции выровнялись.

Page 19: Режимы репликации

Асинхронный

  • synchronous_commit
  • = off
  • local
  • Синхронный
    • фиксация с ожиданием подтверждения от синхронной реплики
    • обеспечивает надежность, но не согласованность
    • synchronous_commit
    • = remote_write
    • on
    • remote_apply
    • synchronous_standby_names = список реплик
    • FIRST N (список реплик)
    • ANY N (список реплик)

Комментарии преподавателя

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

Синхронная репликация включается тем же параметром synchronous_commit, что и синхронная фиксация. Разные значения этого параметра позволяют управлять уровнем надежности. При значении remote_write мастер ожидает подтверждения о получении записи (остается шанс потерять изменения, если на реплике случится сбой и она не успеет записать данные на диск).

При значении on мастер ожидает подтверждения о попадании журнальной записи на диск реплики (это надежный режим; однако приложение, обратившись к реплике, может не увидеть изменений).

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

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

Page 20: Потеря нужного сегмента

Мастер может удалить файл журнала, нужный реплике

  • если реплика не успеет его получить
  • (например, будет остановлена на некоторое время)
  • Решение 1. Слот репликации
    • вводит зависимость мастера от реплики, требует мониторинга
    • max_slot_wal_keep_size = −1
  • Решение 2. Архив журнала предзаписи
    • позволяет обойтись без слота

Комментарии преподавателя

При репликации возможен ряд сложностей, которые можно решать разными способами.

Сервер PostgreSQL периодически удаляет файлы журнала, которые не требуются для восстановления. При этом мастер может удалить файл, который содержит данные, еще не переданные реплике.

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

Но если архива нет, реплика не сможет продолжать восстановление и ее придется пересоздавать из новой резервной копии. Чтобы этого избежать, можно использовать слот репликации. Однако надо понимать, что создание слота ставит мастер в зависимость от реплики: если реплика не будет получать журнальные записи, они будут накапливаться на мастере, что может привести к заполнению файловой системы. Чтобы не допустить этого, стоит установить предел накапливаемого объема параметром max_slot_wal_keep_size. При превышении этого ограничения слот перестанет функционировать и удерживать журнальные файлы от удаления.

В любом случае слот — еще одна точка, которую необходимо включать в мониторинг.

Page 21: Влияние слота репликации

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

student$ sudo pg_ctlcluster 16 beta stop

Ограничим размер WAL двумя сегментами.

α=> ALTER SYSTEM SET min_wal_size = '32MB';
ALTER SYSTEM
α=> ALTER SYSTEM SET max_wal_size = '32MB';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

Слот неактивен, но помнит номер последней записи, полученной репликой:

α=> SELECT active, restart_lsn, wal_status FROM pg_replication_slots \gx
-[ RECORD 1 ]----------
active      | f
restart_lsn | 0/7917360
wal_status  | reserved

Вставим еще строки в таблицу.

α=> INSERT INTO test SELECT 'Just a line' FROM generate_series(1,1_000_000);
INSERT 0 1000000

Номер LSN в слоте не изменился:

α=> SELECT active, restart_lsn, wal_status FROM pg_replication_slots \gx
-[ RECORD 1 ]----------
active      | f
restart_lsn | 0/7917360
wal_status  | extended

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

α=> CHECKPOINT;
CHECKPOINT

Каков теперь размер журнала?

α=> SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
 pg_size_pretty
----------------
 96 MB
(1 row)

Контрольная точка не удалила сегменты — их удерживает слот, несмотря на превышение ограничения. Если оставить слот без присмотра, дисковое пространство может быть исчерпано. Скорее всего, бесперебойная работа основного сервера важнее, чем синхронизация реплики. Чтобы предотвратить разрастание журнала, можно ограничить объем, удерживаемый слотом:

α=> ALTER SYSTEM SET max_slot_wal_keep_size = '16MB';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload
α=> CHECKPOINT;
CHECKPOINT

Теперь журнал очищается контрольной точкой:

α=> SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();

Page 22: Page 22

 pg_size_pretty
----------------
 32 MB
(1 row)

Но слот уже не обеспечивает наличие записей WAL (wal_status=lost):

α=> SELECT active, restart_lsn, wal_status FROM pg_replication_slots \gx
-[ RECORD 1 ]-----
active      | f
restart_lsn |
wal_status  | lost

А реплика не может синхронизироваться:

student$ sudo pg_ctlcluster 16 beta start
student$ sudo tail -n 2 /var/log/postgresql/postgresql-16-beta.log
2026-04-10 18:05:03.584 MSK [72247] FATAL:  could not receive data from WAL stream:
ERROR:  requested WAL segment 000000010000000000000007 has already been removed
2026-04-10 18:05:03.584 MSK [72242] LOG:  waiting for WAL to become available at 0/7002000

Для синхронизации реплики в таких случаях можно воспользоваться архивом, задав параметр restore_command. Если это невозможно, придется заново настроить репликацию, повторив формирование базовой копии.

Page 23: Конфликты: проблема

Записи WAL, несовместимые с запросами на реплике

  • очистка удаляет версии строк, нужные запросам на реплике
  • исключительные блокировки объектов
  • закрепление буферов
  • Мониторинг
    • pg_stat_database_conflicts

Комментарии преподавателя

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

● очистка может удалить версию строки, которая используется запросом, или потребовать блокировку, несовместимую с запросом;

● может прийти запись об исключительной блокировке объекта (например, при удалении таблицы);

● иногда конфликт возникает из-за закрепления буфера.

Возникновение таких конфликтов можно отслеживать с помощью представления pg_stat_database_conflicts.

Page 24: Конфликты: решения

Решение 0: избегать конфликтов

  • не выполнять команды, вызывающие блокировки
  • отменить усечение файла в конце очистки: параметр хранения
  • vacuum_truncate = off
  • Решение 1: откладывать применение конфликтующих записей
    • max_standby_streaming_delay
    • max_standby_archive_delay
    • кроме взаимоблокировок, DROP TABLESPACE, DROP DATABASE

Комментарии преподавателя

Один из путей борьбы с конфликтами — избегать действий на мастере, приводящих к исключительным блокировкам. Например, можно не выполнять команды DROP TABLE, TRUNCATE, LOCK, DROP INDEX, DROP TRIGGER и т. п. или выполнять их в то время, когда на реплике заведомо нет выполняющихся запросов.

Некоторых неявных блокировок можно избежать, отключая соответствующую функциональность на мастере. Например, если в результате очистки освободились страницы в конце файла, при уменьшении его размера устанавливается исключительная блокировка таблицы. Но можно отменить усечение файла, задав параметр хранения таблицы vacuum_truncate = off. Однако отменять или надолго откладывать очистку нельзя, поэтому избежать конфликта в случае очистки нужных запросу версий строк таким образом не получится.

Другой способ — откладывать применение полученных несовместимых журнальных записей на реплике. Этот способ работает и при потоковой репликации, и при использовании архива; он откладывает записи, относящиеся и к очистке, и к исключительным блокировкам. В параметрах устанавливается максимальная задержка применения (от времени получения записи). Если к этому моменту конфликтующий запрос не успеет завершиться, он будет прерван.

Однако конфликты, вызванные редкими причинами (удаление базы данных или табличного пространства, взаимоблокировки) не откладываются и разрешаются немедленной отменой запроса.

Page 25: Конфликты: решения

Решение 2: обратная связь по протоколу репликации

  • снимок на реплике предотвращает удаление версий строк
  • так же, как и локальный
  • hot_standby_feedback = on
  • только для конфликта очистки с запросом на реплике
  • Мониторинг
    • pg_stat_replication.backend_xmin
    • pg_replication_slots.xmin

Комментарии преподавателя

Еще один путь решения — откладывать несовместимые действия на мастере. Для этого включается обратная связь.

Этот способ работает только для очистки (исключительную блокировку отложить не получится) и только в случае потоковой репликации (если реплика переключается на получение записей из архива, она фактически перестает существовать для мастера).

Каждый снимок данных определяет так называемый горизонт — это значение счетчика транзакций, по которому проходит граница между ненужными и нужными версиями строк. Минимальный горизонт по всем снимкам реплики определяет возможность очистки. Подробности обсуждаются в теме «Снимки данных» курса DBA2.

Благодаря обратной связи мастер понимает, какие версии строк нужны реплике. В таком случае (с точки зрения очистки) запрос на реплике ничем не отличается от запроса на самом мастере.

Обратная связь позволяет отслеживать горизонт очистки на стороне мастера. Если слот репликации не используется, горизонт виден в поле pg_stat_replication.backend_xmin, а при использовании слота — в pg_replication_slots.xmin. Нужно учитывать, что при включенной обратной связи слот, даже неактивный, будет задерживать очистку, что может привести к увеличению размеров файлов данных. Этот эффект будет исследован в практике.

Page 26: Итоги

Механизм репликации основан на передаче журнальных записей на реплику и их применении

  • основной режим: потоковая репликация
  • может быть поддержана архивом журнальных записей
  • Реплика — точная копия мастера
    • не генерирует собственные записи WAL, доступна только для чтения
  • Сложный механизм, требующий настройки и мониторинга
    • настройка существенно зависит от решаемых задач

Page 27: Практика

  1. Настройте потоковую репликацию между двумя серверами в синхронном режиме, со слотом, без обратной связи. 2. Проверьте работу репликации. Убедитесь, что при остановленной реплике фиксация не завершается. 3. Воспроизведите ситуацию, при которой запрос на реплике прерывается из-за очистки версий строк на мастере. 4. Запретите откладывать применение конфликтующих изменений; проверьте, что запрос отменяется сразу. 5. Включите обратную связь и убедитесь, что очистка на мастере не прерывает выполнение запроса. 6. Остановите реплику и убедитесь, что слот препятствует очистке. Удалите слот, чтобы очистка возобновилась.

Комментарии преподавателя

  1. Для синхронного режима установите на мастере параметры:

● synchronous_commit = on

● synchronous_standby_names = ‘“16/beta”’

  1. Для этого на реплике можно начать транзакцию с уровнем изоляции Repeatable Read и периодически выполнять запросы к таблице, а на мастере изменить эту таблицу и выполнить очистку.

Учтите, что параметр max_standby_streaming_delay по умолчанию имеет значение 30 секунд.

  1. Установите параметр max_standby_streaming_delay в ноль.

  2. Установите на реплике параметр hot_standby_feedback = on. Максимальный интервал оповещений устанавливается в параметре wal_receiver_status_interval (по умолчанию 10 секунд).

  3. После выполнения практических заданий удалите созданную базу данных и сбросьте настройки параметров конфигурации.

Не забудьте отменить синхронную фиксацию, иначе при остановленной реплике не удастся зафиксировать транзакцию. Не устанавливайте synchronous_commit = off. Вместо этого установите synchronous_commit = ’local’ или сбросьте значение параметра synchronous_standby_names.

Page 28: 1. Физическая потоковая репликация в синхронном режиме

α=> CREATE DATABASE replica_physical;
CREATE DATABASE

Создаем и запускаем реплику точно так же, как в демонстрации.

α=> \c replica_physical
You are now connected to database "replica_physical" as user "student".

Резервная копия:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  --pgdata /var/lib/postgresql/16/beta \
  --checkpoint=fast \
  -R --create-slot --slot=replica
student$ sudo pg_ctlcluster 16 beta start

Для того чтобы основной сервер мог идентифицировать реплику, в основной конфигурации уже установлен параметр cluster_name:

student$ psql -p 5433 -c 'SHOW cluster_name;'
 cluster_name
--------------
 16/beta
(1 row)

Другой вариант — добавить атрибут application_name в строку соединения:

primary_conninfo='user=student port=5432 application_name=16/beta'

Теперь включаем на мастере режим синхронной репликации.

α=> ALTER SYSTEM SET synchronous_commit = on;
ALTER SYSTEM
α=> ALTER SYSTEM SET synchronous_standby_names = '"16/beta"';
ALTER SYSTEM

Если все реплики синхронные, можно задать значение, соответствующее любому имени:

synchronous_standby_names = '*'

Однако мастер не может отличить реплику от, например, утилиты pg_receivewal. При такой настройке он будет делать попытки синхронизироваться с любым клиентом, подключившимся по протоколу репликации, и это может привести к бесконечному ожиданию при фиксации транзакций. Поэтому лучше перечислять имена явно.

student$ sudo pg_ctlcluster 16 alpha reload

2. Проверка физической репликации

α=> SELECT * FROM pg_stat_replication \gx

Page 29: Page 29

-[ RECORD 1 ]----+------------------------------
pid              | 93766
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2026-04-10 18:17:47.327757+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3000C50
write_lsn        | 0/3000C50
flush_lsn        | 0/3000C50
replay_lsn       | 0/3000C50
write_lag        | 00:00:00.000472
flush_lag        | 00:00:00.012854
replay_lag       | 00:00:00.013517
sync_priority    | 1
sync_state       | sync
reply_time       | 2026-04-10 18:17:50.171064+03

sync_state: sync говорит о том, что репликация работает в синхронном режиме; application_name: 16/beta — имя, под которым мастер знает реплику. Остановим реплику.

student$ sudo pg_ctlcluster 16 beta stop
α=> BEGIN;
BEGIN
α=> CREATE TABLE test(id integer);
CREATE TABLE
α=> COMMIT;

Фиксация ждет появления синхронной реплики…

student$ sudo pg_ctlcluster 16 beta start

После старта реплики фиксация завершается.

COMMIT

3. Отмена запроса из-за очистки на мастере

α=> INSERT INTO test VALUES (1);
INSERT 0 1
student$ psql -p 5433 -d replica_physical

Мы хотим, чтобы в журнал сервера выводилось сообщение, если из-за конфликта с выполняющимся запросом применение журнальной записи на реплике откладывается более чем на deadlock_timeout.

β=> \dconfig deadlock_timeout
List of configuration parameters
    Parameter     | Value
------------------+-------
 deadlock_timeout | 1s
(1 row)

Для этого установим параметр log_recovery_conflict_waits.

β=> ALTER SYSTEM SET log_recovery_conflict_waits = on;
ALTER SYSTEM

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

β=> ALTER SYSTEM SET max_standby_streaming_delay = '9s';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta reload

Начинаем транзакцию с уровнем изоляции Repeatable Read: первый оператор создаст снимок данных, который будут

Page 30: Page 30

использовать все операторы транзакции.

β=> BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
β=> SELECT * FROM test;
 id
----
  1
(1 row)

На мастере изменяем единственную строку и выполняем очистку. При этом первая версия строки будет удалена.

α=> UPDATE test SET id = 2;
UPDATE 1
α=> VACUUM test;
VACUUM

Через 5 секунд запрос на реплике еще сработает — реплика задерживает применение конфликтующей журнальной записи:

β=> SELECT * FROM test;
 id
----
  1
(1 row)

А в журнале отчета сервера уже есть сообщение об ожидании разрешения конфликта:

postgres$ tail -3 /var/log/postgresql/postgresql-16-beta.log
2026-04-10 18:17:57.155 MSK [94207] LOG:  recovery still waiting after 1024.273 ms:
recovery conflict on snapshot
2026-04-10 18:17:57.155 MSK [94207] DETAIL:  Conflicting process: 94293.
2026-04-10 18:17:57.155 MSK [94207] CONTEXT:  WAL redo at 0/301B8A0 for Heap2/PRUNE:
snapshotConflictHorizon: 738, nredirected: 1, ndead: 0, nunused: 0, redirected: [1->2],
dead: [], unused: []; blkref #0: rel 1663/16409/16410, blk 0

Еще через 5 секунд такой же запрос уже будет аварийно прерван, поскольку версия строки, входящая в снимок, больше не существует.

β=> SELECT * FROM test;
FATAL:  terminating connection due to conflict with recovery
DETAIL:  User query might have needed to see row versions that must be removed.
HINT:  In a moment you should be able to reconnect to the database and repeat your
command.
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

4. Запрет откладывания применения конфликтующей записи

Выставим нулевое время задержки применения конфликтующих изменений.

student$ psql -p 5433 -d replica_physical
β=> ALTER SYSTEM SET max_standby_streaming_delay = 0;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta reload

Снова начинаем транзакцию…

β=> BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
β=> SELECT * FROM test;

Page 31: Page 31

 id
----
  2
(1 row)

На мастере изменяем строку и выполняем очистку…

α=> UPDATE test SET id = 3;
UPDATE 1
α=> VACUUM test;
VACUUM

Запрос на реплике прерывается тут же:

β=> SELECT * FROM test;
FATAL:  terminating connection due to conflict with recovery
DETAIL:  User query might have needed to see row versions that must be removed.
HINT:  In a moment you should be able to reconnect to the database and repeat your
command.
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

5. Обратная связь

Установим обратную связь и, чтобы не ждать долго, небольшой интервал оповещений.

student$ psql -p 5433 -d replica_physical
β=> ALTER SYSTEM SET hot_standby_feedback = on;
ALTER SYSTEM
β=> ALTER SYSTEM SET wal_receiver_status_interval = '1s';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta reload

Снова начинаем транзакцию…

β=> BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
β=> SELECT * FROM test;
 id
----
  3
(1 row)

На мастере изменяем строку и выполняем очистку…

α=> UPDATE test SET id = 4;
UPDATE 1
α=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 0 removed, 2 remain, 1 are dead but not yet removable
removable cutoff: 740, which was 1 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 10 hits, 0 misses, 0 dirtied
WAL usage: 1 records, 0 full page images, 188 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Благодаря обратной связи, очистка не может удалить старую версию строки (tuples: 0 removed, 2 remain, 1 are dead but not

Page 32: Page 32

yet removable).

β=> SELECT * FROM test;
 id
----
  3
(1 row)
β=> COMMIT;
COMMIT
α=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 1 removed, 1 remain, 0 are dead but not yet removable
removable cutoff: 741, which was 0 XIDs old when operation ended
new relfrozenxid: 740, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 11 hits, 0 misses, 0 dirtied
WAL usage: 3 records, 0 full page images, 308 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

После завершения транзакции на реплике мастер может выполнить очистку (tuples: 1 removed, 1 remain, 0 are dead but not yet removable). Журнальные записи реплицируются, но ничего не сломают.

6. Влияние слота на очистку при остановленной реплике

Теперь отменим синхронизацию и остановим реплику:

α=> ALTER SYSTEM RESET synchronous_standby_names;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload
student$ sudo pg_ctlcluster 16 beta stop

Еще раз изменим строку таблицы:

α=> UPDATE test SET id = 5;
UPDATE 1

Слот неактивен, но помнит минимальный xmin снимков реплики:

α=> SELECT active, xmin FROM pg_replication_slots;
 active | xmin
--------+------
 f      |  741
(1 row)

Это значение xmin меньше, чем минимальный xmin локальных снимков:

α=> SELECT min(backend_xmin::text::numeric) FROM pg_stat_activity;
 min
-----
 742
(1 row)

Поэтому слот будет задерживать очистку (tuples: 0 removed, 2 remain, 1 are dead but not yet removable).

α=> VACUUM VERBOSE test;

Page 33: Page 33

INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 0 removed, 2 remain, 1 are dead but not yet removable
removable cutoff: 741, which was 1 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 10 hits, 0 misses, 0 dirtied
WAL usage: 1 records, 0 full page images, 188 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Удалим слот.

α=> SELECT pg_drop_replication_slot('replica');
 pg_drop_replication_slot
--------------------------
(1 row)

Теперь очистка сработает (tuples: 1 removed).

α=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_physical.public.test"
INFO:  finished vacuuming "replica_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 1 removed, 1 remain, 0 are dead but not yet removable
removable cutoff: 742, which was 0 XIDs old when operation ended
new relfrozenxid: 741, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 11 hits, 0 misses, 0 dirtied
WAL usage: 3 records, 0 full page images, 308 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Уборка

Удалим созданную базу данных и сбросим настройки параметров конфигурации.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_physical;
DROP DATABASE
α=> ALTER SYSTEM RESET synchronous_commit;
ALTER SYSTEM
α=> ALTER SYSTEM RESET synchronous_standby_names;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Page 34: Практика+

  1. Настройте потоковую физическую репликацию вместе с архивацией сегментов WAL, использовав для потока репликации сетевой интерфейс виртуальной машины. 2. Проверьте работу потоковой репликации через этот сетевой интерфейс. 3. Сымитируйте отказ сети, выключив сетевой интерфейс виртуальной машины. 4. Убедитесь, что потоковая репликация остановилась, при этом реплика получает изменения благодаря архивации сегментов WAL, но с задержками. 5. Заново запустите сетевой интерфейс виртуальной машины, проверив затем, что потоковая репликация автоматически восстановилась.

Комментарии преподавателя

  1. Проверить состояние сетевых интерфейсов можно командой: ip a

Проверить доступность сетевого адреса можно командой: ping -c1 <IP адрес>

Внесите изменения в файл pg_hba.conf для разрешения подключений к серверу alpha через любые сетевые интерфейсы, а также измените конфигурацию этого сервера так, чтобы он прослушивал все сетевые интерфейсы.

Процесс wal_receiver завершается, если сетевое соединение отсутствует в течение времени wal_receiver_timeout. Задайте небольшое значение этого параметра.

  1. Отказ сети смоделируйте, выключив сетевой интерфейс: sudo ip l set enp0s3 down

  2. Убедитесь, что после переключения сегмента WAL реплика получает изменения, сделанные до переключения.

  3. Включить сетевой интерфейс можно командой: sudo ip l set enp0s3 up

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

Page 35: 1. Физическая потоковая репликация + архив WAL

Имя основного сетевого интерфейса виртуальной машины enp0s3:

student$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen
1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group
default qlen 1000
    link/ether 08:00:27:62:bf:96 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 69519sec preferred_lft 69519sec
    inet6 fd17:625c:f037:2:f831:c519:5f7e:335c/64 scope global temporary dynamic
       valid_lft 86345sec preferred_lft 14345sec
    inet6 fd17:625c:f037:2:a00:27ff:fe62:bf96/64 scope global dynamic mngtmpaddr
       valid_lft 86345sec preferred_lft 14345sec
    inet6 fe80::a00:27ff:fe62:bf96/64 scope link
       valid_lft forever preferred_lft forever

Проверим доступность основного сетевого интерфейса виртуальной машины.

student$ ping -c1 10.0.2.15
PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.
64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.027 ms
--- 10.0.2.15 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.027/0.027/0.027/0.000 ms

Разрешим подключения к серверу alpha с адресов IPv4, принадлежащих сетям интерфейсов сервера.

postgres$ sed -i.bak 's/127\.0\.0\.1\/32/samenet/' /etc/postgresql/16/alpha/pg_hba.conf

Включим прослушивание всех интерфейсов сервером и настроим архивирование WAL.

α=> ALTER SYSTEM SET listen_addresses = '*';
ALTER SYSTEM
α=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM
α=> ALTER SYSTEM SET archive_command =
'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f';
ALTER SYSTEM

Каталог для архива WAL.

postgres$ mkdir /var/lib/postgresql/archive
student$ sudo pg_ctlcluster 16 alpha restart

Резервная копия:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup --pgdata=/var/lib/postgresql/16/beta --checkpoint=fast

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

postgres$ cat << EOF | tee /etc/postgresql/16/beta/conf.d/rest.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
primary_conninfo = 'user=postgres password=postgres host=10.0.2.15 port=5432'
wal_receiver_timeout = '2s'
EOF
restore_command = 'cp /var/lib/postgresql/archive/%f %p'

Page 36: 2. Проверка репликации

primary_conninfo = 'user=postgres password=postgres host=10.0.2.15 port=5432'
wal_receiver_timeout = '2s'
postgres$ touch /var/lib/postgresql/16/beta/standby.signal
student$ sudo pg_ctlcluster 16 beta start
student$ psql
α=> CREATE DATABASE replica_physical;
CREATE DATABASE
α=> \c replica_physical
You are now connected to database "replica_physical" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Привет, мир!');
INSERT 0 1

Реплика получает записи WAL через поток…

student$ psql -p 5433
β=> SELECT pid, status, written_lsn, flushed_lsn, latest_end_lsn, sender_host
FROM pg_stat_wal_receiver \gx
-[ RECORD 1 ]--+----------
pid            | 96630
status         | streaming
written_lsn    | 0/3486580
flushed_lsn    | 0/3486580
latest_end_lsn | 0/3486580
sender_host    | 10.0.2.15

…и сообщает мастеру о принятых записях без задержек:

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 96631
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      | 10.0.2.15
client_hostname  |
client_port      | 59474
backend_start    | 2026-04-10 18:18:50.909683+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3486580
write_lsn        | 0/3486580
flush_lsn        | 0/3486580
replay_lsn       | 0/3486580
write_lag        | 00:00:00.00072
flush_lag        | 00:00:00.008024
replay_lag       | 00:00:00.008634
sync_priority    | 0
sync_state       | async
reply_time       | 2026-04-10 18:18:57.480665+03
β=> \c replica_physical
You are now connected to database "replica_physical" as user "student".

Таблица реплицирована:

β=> SELECT * FROM test;
      s
--------------
 Привет, мир!
(1 row)

Page 37: 3. Отказ сети

Выключаем сетевой интерфейс — авария сети.

student$ sudo ip l set enp0s3 down

Сеть недоступна.

student$ ping -c1 10.0.2.15
ping: connect: Network is unreachable

Процесс wal receiver остановлен:

β=> SELECT * FROM pg_stat_wal_receiver \gx
(0 rows)

На мастере добавляем строку…

α=> INSERT INTO test VALUES ('Отказ сети.');
INSERT 0 1

…но реплика ее не получает:

β=> SELECT * FROM test;
      s
--------------
 Привет, мир!
(1 row)

Потоковая репликация остановилась:

α=> SELECT sent_lsn, write_lsn FROM pg_stat_replication \gx
-[ RECORD 1 ]--------
sent_lsn  | 0/34865F8
write_lsn | 0/3486580

4. Архивация

Однако архивация на мастере продолжает работать. Переключим сегмент WAL:

α=> SELECT pg_walfile_name(pg_current_wal_lsn()), pg_switch_wal();
     pg_walfile_name      | pg_switch_wal
--------------------------+---------------
 000000010000000000000003 | 0/3486660
(1 row)
α=> INSERT INTO test VALUES ('Сегмент WAL переключен.');
INSERT 0 1

Сегмент попал в архив:

α=> SELECT * FROM pg_stat_archiver \gx
-[ RECORD 1 ]------+------------------------------
archived_count     | 4
last_archived_wal  | 000000010000000000000003
last_archived_time | 2026-04-10 18:19:07.410852+03
failed_count       | 0
last_failed_wal    |
last_failed_time   |
stats_reset        | 2025-11-25 18:23:08.681015+03

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

β=> SELECT * FROM test;

Page 38: 5. Восстановление сети

      s
--------------
 Привет, мир!
 Отказ сети.
(2 rows)

Вставим еще одну строку в таблицу на мастере.

α=> INSERT INTO test VALUES ('Сеть еще неисправна...');
INSERT 0 1

На реплику она не попадает.

β=> SELECT * FROM test;
      s
--------------
 Привет, мир!
 Отказ сети.
(2 rows)

Теперь восстановим сетевой интерфейс — авария ликвидирована.

student$ sudo ip l set enp0s3 up
student$ ping -c1 10.0.2.15
PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.
64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.018 ms
--- 10.0.2.15 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.018/0.018/0.018/0.000 ms

Опять работает потоковая репликация:

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 98407
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      | 10.0.2.15
client_hostname  |
client_port      | 57020
backend_start    | 2026-04-10 18:19:16.503782+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/4000170
write_lsn        | 0/4000170
flush_lsn        | 0/4000170
replay_lsn       | 0/4000170
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
reply_time       | 2026-04-10 18:19:16.632099+03

На мастере добавляем еще строку.

α=> INSERT INTO test VALUES ('Сеть восстановлена!');
INSERT 0 1

Реплика получила все строки.

β=> SELECT * FROM test;

Page 39: Уборка

Source slide page 39: Уборка{#source-figure-039 caption=“Source slide page 39: Уборка”}

            s
-------------------------
 Привет, мир!
 Отказ сети.
 Сегмент WAL переключен.
 Сеть еще неисправна...
 Сеть восстановлена!
(5 rows)

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

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_physical;
DROP DATABASE
β=> \c postgres
You are now connected to database "postgres" as user "student".
β=> ALTER SYSTEM RESET listen_addresses;
ALTER SYSTEM
β=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
β=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop -m immediate --skip-systemctl-redirect
α=> ALTER SYSTEM RESET listen_addresses;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_mode;
ALTER SYSTEM
α=> ALTER SYSTEM RESET archive_command;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart
postgres$ rm -f /etc/postgresql/16/beta/conf.d/rest.conf

05 Занятие 05: Репликация / Переключение на реплику

Исходная страница в нативном Markdown: Репликация / Переключение на реплику
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_05_replica_switchover.html Archive: DBA3-handouts-16.zip Страниц PDF: 34

Page 1: Репликация / Переключение на реплику

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Переключение на реплику
  • Возвращение в строй бывшего мастера
  • Особенности, связанные с файловым архивом

Page 3: Переключение на реплику

Причины

  • плановое переключение (switchover):
  • останов основного сервера для проведения технических работ
  • аварийное переключение (failover):
  • переход на реплику из-за сбоя основного сервера
  • Процедура
    • убедиться, что мастер остановлен
    • $ pg_ctl promote или
    • pg_promote()
    • автоматическое переключение: отсутствует
    • (для автоматизации требуется стороннее кластерное ПО)

Комментарии преподавателя

Причиной перехода на резервный сервер может быть необходимость проведения технических работ на основном сервере — тогда переход выполняется в удобное время в штатном режиме (switchover). А в случае сбоя переходить на резервный сервер нужно как можно быстрее, чтобы сократить время простоя системы (failover).

Чтобы переключить реплику в режим самостоятельного сервера, ей посылают команду, вызывая либо pg_ctl promote из операционной системы, либо функцию pg_promote из SQL. Переключение с помощью триггерного файла (параметр promote_trigger_file) начиная с 16-й версии не поддерживается.

Перед этим важно убедиться, что мастер остановлен. Если он продолжает работу и к нему еще подключены клиенты, данные на разных серверах могут «разойтись», и свести их потом воедино — нетривиальная и неавтоматизируемая задача. Скорее всего, часть данных придется просто потерять.

Для перехода на резервный сервер необходимо переключить на него клиентов. Автоматизация такого переключения требует стороннего кластерного программного обеспечения (это обсуждается в модуле «Кластерные технологии»), так как в PostgreSQL собственных средств для этого нет.

https://postgrespro.ru/docs/postgresql/16/warm-standby-failover

Page 4: Повышение реплики

Применяет журнальные записи

  • уже полученные wal receiver, но еще не примененные startup
  • Завершает процессы
    • wal receiver
    • startup
  • Запускает процессы
    • wal writer
    • autovacuum launcher
    • archiver (зависит от настройки)
  • Переходит на новую линию времени

Комментарии преподавателя

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

Для этого процесс startup применяет журнальные записи, которые уже были получены процессом wal receiver, но еще не применены. На этом wal receiver и startup завершают свою работу — на основном сервере они не нужны.

А вот процессы wal writer и autovacuum launcher, наоборот, запускаются. Может запускаться и процесс archiver.

Выйдя из режима восстановления, сервер переходит на новую линию времени.

С точностью до некоторых деталей, все происходит так же, как при окончании восстановления из резервной копии.

Page 5: Настройка потоковой репликации

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

α=> ALTER SYSTEM SET checkpoint_timeout = '1d';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload
α=> CREATE DATABASE replica_switchover;
CREATE DATABASE

Подключим расширение pg_walinspect. Оно потребуется для исследования записей о контрольных точках в WAL. Нужны права суперпользователя.

α=> \c replica_switchover postgres
You are now connected to database "replica_switchover" as user "postgres".
α=> CREATE EXTENSION pg_walinspect;
CREATE EXTENSION
α=> \c - student
You are now connected to database "replica_switchover" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Исходное состояние. Сервер alpha — единственный.');
INSERT 0 1

Мы будем использовать автономную резервную копию несколько раз, поэтому создаем ее в формате tar.

student$ pg_basebackup \
  --format=tar \
  --compress=gzip \
  --checkpoint=fast \
  --pgdata=/home/student/tmp/backup

Выкладываем копию в каталог PGDATA сервера beta:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

Нам потребуется файл standby.signal.

postgres$ touch /var/lib/postgresql/16/beta/standby.signal

Настроим подключение к мастеру.

postgres$ cat << EOF >> /var/lib/postgresql/16/beta/postgresql.auto.conf
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'
EOF

Создадим на сервере alpha слот репликации и запустим реплику beta.

student$ pg_receivewal --create-slot --slot=replica
student$ sudo pg_ctlcluster 16 beta start

Проверим реплику:

α=> INSERT INTO test VALUES ('Проверка репликации: alpha — мастер, beta — реплика.');
INSERT 0 1

Page 6: Page 6

student$ psql -p 5433 -d replica_switchover
β=> SELECT * FROM test;
                          s
------------------------------------------------------
 Исходное состояние. Сервер alpha — единственный.
 Проверка репликации: alpha — мастер, beta — реплика.
(2 rows)

Останов мастера и контрольная точка

Добавим в таблицу еще одну строку и корректно остановим мастер. При этом выполнится контрольная точка.

α=> INSERT INTO test VALUES ('Перед корректным остановом мастера — сервера alpha.');
INSERT 0 1
student$ sudo pg_ctlcluster 16 alpha stop

Информация о контрольной точке записалась в управляющий файл:

postgres$ /usr/lib/postgresql/16/bin/pg_controldata -D ~postgres/16/alpha | \
  grep -E '^.*(cluster|checkpoint.*location).*:'
Database cluster state:               shut down
Latest checkpoint location:           0/30002B8
Latest checkpoint's REDO location:    0/30002B8

Получила ли реплика эту запись о контрольной точке? Исследуем WAL на реплике, начиная с LSN записи о контрольной точке.

β=> SELECT start_lsn, prev_lsn, resource_manager, record_type, record_length,
  left(description, 46) || '...' description
FROM pg_get_wal_records_info('0/30002B8', 'FFFFFFFF/FFFFFFFF') \gx
-[ RECORD 1 ]----+--------------------------------------------------
start_lsn        | 0/30002B8
prev_lsn         | 0/3000290
resource_manager | XLOG
record_type      | CHECKPOINT_SHUTDOWN
record_length    | 114
description      | redo 0/30002B8; tli 1; prev tli 1; fpw true; x...

В поле record_length показана длина записи в байтах. Последняя позиция, полученная репликой, больше LSN записи о контрольной точке:

β=> SELECT pg_last_wal_receive_lsn(), pg_wal_lsn_diff(pg_last_wal_receive_lsn(), '0/30002B8' );
 pg_last_wal_receive_lsn | pg_wal_lsn_diff
-------------------------+-----------------
 0/3000330               |             120
(1 row)

Разница немного превышает длину записи (114) из-за выравнивания записей WAL: на 64-разрядной платформе LSN записей кратны восьми байтам.

Повышение реплики

Сейчас сервер beta является репликой (находится в режиме восстановления):

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

Повышаем реплику. Можно использовать функцию pg_promote(), которая выполняет то же действие.

student$ sudo pg_ctlcluster 16 beta promote

Есть еще один вариант выполнить повышение: удалить файл standby.signal и перезапустить резервный сервер. Это не вполне «честный» способ, поскольку в этом случае реплика не поймет, что восстановление завершено, и не перейдет на

Page 7: Page 7

новую линию времени. Теперь бывшая реплика beta стала полноценным экземпляром.

β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

Page 8: Возвращение в строй

Source slide page 8: Возвращение в строй{#source-figure-008 caption=“Source slide page 8: Возвращение в строй”}

Комментарии преподавателя

После повышения бывшая реплика выступает в качестве самостоятельного сервера. Она обслуживает запросы клиентов и формирует архив журнальных записей — уже с новой линией времени.

Если переключение не было вызвано выходом сервера из строя, то нужен способ быстро вернуть старый мастер в строй — теперь уже в качестве реплики (failback).

Page 9: Простое подключение

Source slide page 9: Простое подключение{#source-figure-009 caption=“Source slide page 9: Простое подключение”}

Бывший мастер подключается к новому как реплика

  • допустимо, только если перед переключением реплика получила
  • от мастера все журнальные записи

Комментарии преподавателя

В случае аккуратной остановки мастера (в режиме fast или smart) все журнальные записи мастера скорее всего дойдут до реплики, хотя это и не гарантируется. Процесс останова организован таким образом, что сначала отключаются все обслуживающие процессы, затем выполняется контрольная точка, и только в самую последнюю очередь останавливается процесс wal sender, чтобы реплика получила запись WAL о контрольной точке.

Позицию в журнале мастера можно проверить с помощью утилиты pg_controldata (запись latest checkpoint location), а позицию на реплике покажет функция pg_last_wal_receive_lsn. Эта функция возвращает текущую позицию в WAL, поэтому ее значение должно опережать latest checkpoint location на длину записи о контрольной точке.

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

В случае останова мастера без выполнения контрольной точки (сбой или режим immediate), такое подключение на нагруженном сервере, скорее всего, будет невозможно. При этом сервер не сможет стартовать, а в журнале сообщений будет фиксироваться ошибка.

Page 10: Возвращение в строй: прямое подключение

Введем сервер alpha в строй, но теперь уже в качестве реплики. Для этого создадим файл standby.signal:

postgres$ touch /var/lib/postgresql/16/alpha/standby.signal

Настроим подключение к мастеру.

postgres$ cat << EOF >> /var/lib/postgresql/16/alpha/postgresql.auto.conf
primary_conninfo = 'user=student port=5433'
EOF

Запустим alpha и убедимся, что этот сервер стал репликой.

student$ sudo pg_ctlcluster 16 alpha start
student$ psql -d replica_switchover
α=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

На сервере beta мы можем изменять данные:

β=> INSERT INTO test VALUES ('Сейчас сервер beta — мастер, alpha запущена как реплика.');
INSERT 0 1

А реплика alpha их принимает.

α=> SELECT * FROM test;
                            s
----------------------------------------------------------
 Исходное состояние. Сервер alpha — единственный.
 Проверка репликации: alpha — мастер, beta — реплика.
 Перед корректным остановом мастера — сервера
alpha.
 Сейчас сервер beta — мастер, alpha запущена как
реплика.
(4 rows)

Page 11: Резервная копия

Source slide page 11: Резервная копия{#source-figure-011 caption=“Source slide page 11: Резервная копия”}

Бывший мастер восстанавливается из резервной копии

  • абсолютно новая реплика, процесс занимает много времени
  • можно ускорить rsync, если с момента сбоя прошло немного времени
  • (но все равно долго для больших баз данных)

Комментарии преподавателя

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

Простой и надежный вариант — создать абсолютно новую реплику путем изготовления и развертывания базовой резервной копии. Однако для больших баз данных этот процесс может занимать много времени.

Вариант такого подхода — не использовать утилиту pg_basebackup, а сделать копию с помощью API резервирования с использованием утилиты rsync. Если выполнять копирование сразу после перехода на реплику, то большая часть файлов не успеет поменяться и процесс может пройти существенно быстрее. Но это, конечно, усложняет процедуру.

Page 12: Утилита pg_rewind

Source slide page 12: Утилита pg_rewind{#source-figure-012 caption=“Source slide page 12: Утилита pg_rewind”}

Подготовка к восстановлению

  • «откатывает» потерянные записи WAL, заменяя соответствующие
  • страницы целевого сервера страницами с сервера-источника
  • копирует с сервера-источника все служебные файлы

Комментарии преподавателя

Еще более быстрый вариант состоит в использовании штатной утилиты pg_rewind.

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

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

Дальше применяются все необходимые записи WAL с нового мастера. Фактически, это выполняет уже не утилита, а обычный процесс восстановления после запуска сервера. Чтобы восстановление началось с нужного момента, утилита создает управляющий файл backup_label.

https://postgrespro.ru/docs/postgresql/16/app-pgrewind

Page 13: Особенности pg_rewind

Целевой сервер (бывший мастер)

  • все необходимые журнальные файлы должны сохраниться в pg_wal
  • или в архиве
  • должны быть включены контрольные суммы или wal_log_hints = on
  • должен быть включен параметр full_page_writes = on
  • Сервер-источник (текущий мастер)
    • клиент-серверное подключение:
    • -source-server = строка подключения
    • корректно остановленный сервер:
    • -source-pgdata = каталог

Комментарии преподавателя

Для сервера-источника, если он запущен, ключом –source-server задается строка клиент-серверного подключения libpq (утилита не использует протокол репликации). Роль для подключения должна иметь право выполнения функций pg_ls_dir, pg_stat_file, pg_read_binary_file.

Если же сервер-источник остановлен корректно, можно указать его каталог данных ключом –source-pgdata.

Утилита имеет ряд особенностей:

● Все сегменты WAL, содержащие записи от последней общей контрольной точки до текущего момента, должны находиться в каталоге pg_wal целевого сервера или быть доступны для получения из архива. Во втором случае надо задать параметр restore_command и ключ –restore-target-wal.

● Из-за ошибок при записи возможно рассогласование данных внутри страницы (проблема неатомарной записи, обсуждается в теме «Базовая резервная копия»). Чтобы решить проблему, на целевом сервере первое изменение страницы после контрольной точки должно вызывать запись в WAL полного ее образа. Для этого должен быть установлен full_page_writes = on. Этот режим не учитывает «незначительные» изменения страниц (hint bits), поэтому дополнительно требуется, чтобы в кластере либо были включены контрольные суммы страниц, либо был установлен параметр wal_log_hints = on.

● Если целевой сервер был остановлен нештатно, без контрольной точки, утилита запустит его в однопользовательском режиме и тут же остановит корректно.

Page 14: Возвращение в строй: утилита pg_rewind

Воспроизведем теперь аварийную ситуацию появления двух пишущих серверов — разделение сети (мы подробно обсуждаем этот и другие виды сбоев в теме «Обзор кластерных технологий» этого курса). Выведем сервер alpha из режима реплики, не останавливая основной сервер beta.

student$ sudo pg_ctlcluster 16 alpha promote

Теперь и на alpha, и на beta можно изменять данные:

β=> INSERT INTO test VALUES ('Die hard');
INSERT 0 1

В реальности такой ситуации необходимо всячески избегать, поскольку теперь непонятно, какому серверу верить. Придется либо полностью потерять изменения на одном из серверов, либо придумывать, как объединить данные. Наш выбор — потерять изменения, сделанные на сервере beta. Откатим на beta изменения, которые противоречат изменениям, сделанным на alpha. Для этого мы используем утилиту pg_rewind, поэтому убедимся, что на целевом сервере (beta) заданы необходимые значения параметров:

β=> \dconfig data_checksums|full_page_writes
List of configuration parameters
    Parameter     | Value
------------------+-------
 data_checksums   | on
 full_page_writes | on
(2 rows)

Остановим целевой сервер некорректно.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/beta/postmaster.pid)

В ключах утилиты pg_rewind надо указать каталог PGDATA целевого сервера и способ обращения к серверу-источнику (alpha): либо строку подключения (если сервер работает), либо местоположение его каталога PGDATA (если он выключен). В нашем случае сервер alpha запущен. Поскольку целевой сервер beta не был остановлен корректно, утилита запустит его в однопользовательском режиме и остановит с выполнением контрольной точки. Для запуска нужно указать расположение файла конфигурации. Ключ -R обеспечит запуск целевого сервера как реплики после завершения работы утилиты: восстановив согласованность, он останется в режиме восстановления.

postgres$ /usr/lib/postgresql/16/bin/pg_rewind \
  -D /var/lib/postgresql/16/beta \
  --config-file=/etc/postgresql/16/beta/postgresql.conf \
  -R \
  --source-server='user=student port=5432'
pg_rewind: executing "/usr/lib/postgresql/16/bin/postgres" for target server to complete
crash recovery
2026-04-10 18:06:02.619 MSK [74028] LOG:  database system was interrupted; last known up
at 2026-04-10 18:05:54 MSK
2026-04-10 18:06:02.619 MSK [74028] LOG:  database system was not properly shut down;
automatic recovery in progress
2026-04-10 18:06:02.621 MSK [74028] LOG:  redo starts at 0/3000360
2026-04-10 18:06:02.622 MSK [74028] LOG:  invalid record length at 0/3006BF0: expected at
least 24, got 0
2026-04-10 18:06:02.622 MSK [74028] LOG:  redo done at 0/3006BB0 system usage: CPU: user:
0.00 s, system: 0.00 s, elapsed: 0.00 s
2026-04-10 18:06:02.624 MSK [74028] LOG:  checkpoint starting: end-of-recovery immediate
wait
2026-04-10 18:06:02.625 MSK [74028] LOG:  checkpoint complete: wrote 8 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.002 s;
sync files=0, longest=0.000 s, average=0.000 s; distance=26 kB, estimate=26 kB;
lsn=0/3006BF0, redo lsn=0/3006BF0
PostgreSQL stand-alone backend 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
backend> 2026-04-10 18:06:02.633 MSK [74028] LOG:  checkpoint starting: shutdown immediate
2026-04-10 18:06:02.634 MSK [74028] LOG:  checkpoint complete: wrote 0 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.002 s;
sync files=0, longest=0.000 s, average=0.000 s; distance=0 kB, estimate=23 kB;
lsn=0/3006C68, redo lsn=0/3006C68

Page 15: Page 15

pg_rewind: servers diverged at WAL location 0/3002C38 on timeline 2
pg_rewind: rewinding from last common checkpoint at 0/3002968 on timeline 2
pg_rewind: Done!

В результате pg_rewind «откатила» страницы данных к последней контрольной точке перед моментом, когда изменения серверов разошлись. Утилита также создала файл backup_label с информацией о позиции WAL, с которой нужно начинать восстановление. Заглянем в него:

postgres$ cat /var/lib/postgresql/16/beta/backup_label
START WAL LOCATION: 0/3000360 (file 000000020000000000000003)
CHECKPOINT LOCATION: 0/3002968
BACKUP METHOD: pg_rewind
BACKUP FROM: standby
START TIME: 2026-04-10 18:06:03 MSK

Используя параметр -R, мы попросили утилиту создать сигнальный файл standby.signal и задать в конфигурационном файле строку соединения.

postgres$ ls -l /var/lib/postgresql/16/beta/standby.signal
-rw------- 1 postgres postgres 0 апр 10 18:06
/var/lib/postgresql/16/beta/standby.signal

На сервере alpha заранее создан слот репликации, задействуем его.

postgres$ echo primary_slot_name = 'replica' >> /var/lib/postgresql/16/beta/postgresql.auto.conf
postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
checkpoint_timeout = '1d'
primary_conninfo = 'user=student port=5433'
primary_conninfo = 'user=student passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = replica

Теперь beta запустится в режиме реплики.

student$ sudo pg_ctlcluster 16 beta start

Слот репликации инициализировался и используется:

α=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+----------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | t
active_pid          | 74281
xmin                |
catalog_xmin        |
restart_lsn         | 0/305E000
confirmed_flush_lsn |
wal_status          | reserved
safe_wal_size       |
two_phase           | f
conflicting         |

Данные, измененные на новом мастере (alpha), получены репликой (beta):

student$ psql -p 5433 -d replica_switchover
β=> SELECT * FROM test;

Page 16: Page 16

                            s
----------------------------------------------------------
 Исходное состояние. Сервер alpha — единственный.
 Проверка репликации: alpha — мастер, beta — реплика.
 Перед корректным остановом мастера — сервера
alpha.
 Сейчас сервер beta — мастер, alpha запущена как
реплика.
(4 rows)

Добавим еще одну строку:

α=> INSERT INTO test VALUES ('Еще строка с сервера alpha.');
INSERT 0 1
β=> SELECT * FROM test;
                            s
----------------------------------------------------------
 Исходное состояние. Сервер alpha — единственный.
 Проверка репликации: alpha — мастер, beta — реплика.
 Перед корректным остановом мастера — сервера
alpha.
 Сейчас сервер beta — мастер, alpha запущена как
реплика.
 Еще строка с сервера alpha.
(5 rows)

Итак, мы ввели бывший мастер (beta) в строй в качестве реплики.

Page 17: Переключение и архив

Source slide page 17: Переключение и архив{#source-figure-017 caption=“Source slide page 17: Переключение и архив”}

Комментарии преподавателя

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

Page 18: Переключение и архив

Source slide page 18: Переключение и архив{#source-figure-018 caption=“Source slide page 18: Переключение и архив”}

Комментарии преподавателя

Но реплика не в курсе настроек архивирования на мастере. Когда бывшая реплика займет место мастера, она не запишет недостающие сегменты в архив (хотя они у нее есть), потому что рассчитывает на то, что архив работал без сбоев. В результате архив будет неполным.

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

Потоковый архив лишен этого недостатка, поскольку утилита pg_receivewal отслеживает содержимое каталога и запрашивает у сервера недостающие журнальные записи. Но, как отмечалось в модуле «Резервное копирование», использование этой утилиты сопряжено с поддержанием дополнительной инфраструктуры.

Page 19: Проблемы с файловым архивом

Восстановим серверы alpha и beta из резервной копии и настроим на обоих файловую архивацию в общий архив.

student$ sudo pg_ctlcluster 16 alpha stop
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/16/alpha
postgres$ mkdir /var/lib/postgresql/16/alpha
postgres$ chmod 700 /var/lib/postgresql/16/alpha
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/alpha
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/alpha/pg_wal
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

Архив сегментов WAL.

postgres$ rm -rf /var/lib/postgresql/archive
postgres$ mkdir /var/lib/postgresql/archive

Одинаковые настройки архивации на серверах alpha и beta.

postgres$ cat << EOF | tee -a \
  /var/lib/postgresql/16/alpha/postgresql.auto.conf \
  /var/lib/postgresql/16/beta/postgresql.auto.conf
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f'
EOF
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p
/var/lib/postgresql/archive/%f'

На реплике (beta) создадим файл standby.signal и настроим подключение к мастеру.

postgres$ touch /var/lib/postgresql/16/beta/standby.signal
postgres$ cat << EOF >> /var/lib/postgresql/16/beta/postgresql.auto.conf
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'
EOF

Запускаем мастер (alpha) и создаем слот репликации.

student$ sudo pg_ctlcluster 16 alpha start
student$ pg_receivewal --create-slot --slot=replica

Теперь стартуем реплику.

student$ sudo pg_ctlcluster 16 beta start

Текущий сегмент журнала:

student$ psql -d replica_switchover
α=> SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000010000000000000003
(1 row)

Принудительно переключим сегмент WAL. Чтобы переключение произошло, нужно гарантировать, что текущий и следующий сегменты содержат какие-либо записи.

α=> SELECT pg_switch_wal();

Page 20: Page 20

 pg_switch_wal
---------------
 0/3006658
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

Теперь журнальные записи пишутся в следующий сегмент, а предыдущий попал в архив:

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     | last_failed_wal
--------------------------+--------------------------+-----------------
 000000010000000000000004 | 000000010000000000000003 |
(1 row)

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

α=> ALTER SYSTEM SET archive_command = 'exit 1';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

Опять переключим сегмент WAL.

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/4002C08
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

Сегмент не архивируется.

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     |     last_failed_wal
--------------------------+--------------------------+--------------------------
 000000010000000000000005 | 000000010000000000000003 | 000000010000000000000004
(1 row)

Процесс archiver будет продолжать попытки, но безуспешно.

student$ tail -n 4 /var/log/postgresql/postgresql-16-alpha.log
2026-04-10 18:07:21.223 MSK [75035] LOG:  archive command failed with exit code 1
2026-04-10 18:07:21.223 MSK [75035] DETAIL:  The failed archive command was: exit 1
2026-04-10 18:07:22.224 MSK [75035] LOG:  archive command failed with exit code 1
2026-04-10 18:07:22.224 MSK [75035] DETAIL:  The failed archive command was: exit 1

Сервер beta в режиме реплики не выполнял архивацию, а после повышения не будет архивировать пропущенный сегмент. Остановим alpha, повышаем beta.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)
student$ sudo pg_ctlcluster 16 beta promote

Еще раз принудительно переключим сегмент, теперь уже на beta.

student$ psql -p 5433 -d replica_switchover
β=> SELECT pg_switch_wal();

Page 21: Page 21

 pg_switch_wal
---------------
 0/5002158
(1 row)
β=> INSERT INTO test SELECT now();
INSERT 0 1

Что с архивом?

student$ ls -l /var/lib/postgresql/archive
total 49156
-rw------- 1 postgres postgres 16777216 апр 10 18:07 000000010000000000000003
-rw------- 1 postgres postgres 16777216 апр 10 18:07 000000010000000000000005.partial
-rw------- 1 postgres postgres 16777216 апр 10 18:07 000000020000000000000005
-rw------- 1 postgres postgres       41 апр 10 18:07 00000002.history

Сегмент 000000010000000000000004 отсутствует, архив теперь непригоден для восстановления и репликации.

Page 22: Переключение и архив

Source slide page 22: Переключение и архив{#source-figure-022 caption=“Source slide page 22: Переключение и архив”}

Комментарии преподавателя

При установке archive_mode = always на реплике запускается процесс archiver, который записывает сегменты в архив наравне с мастером. Таким образом, один и тот же файл будет записан два раза с двух серверов. Это накладывает на команду archive_command серьезные требования:

● она не должна перезаписывать существующий файл, но должна сообщать об успехе, если файл с тем же содержимым уже есть в архиве;

● она должна корректно обрабатывать одновременный вызов с двух серверов.

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

(Разумеется, можно настроить archive_command и так, чтобы мастер и реплика сохраняли сегменты журнала в разные архивы, но вряд ли это практично.)

Page 23: Архивация с реплики

Чтобы при переключении на реплику архив не пострадал, на реплике нужно использовать значение archive_mode = always. При этом команда архивации должна корректно обрабатывать одновременную запись сегмента мастером и репликой. Еще раз восстановим alpha и beta из резервной копии и настроим на обоих файловую архивацию в общий архив.

student$ sudo pg_ctlcluster 16 alpha stop
Cluster is not running.
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -rf /var/lib/postgresql/16/alpha
postgres$ mkdir /var/lib/postgresql/16/alpha
postgres$ chmod 700 /var/lib/postgresql/16/alpha
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ mkdir /var/lib/postgresql/16/beta
postgres$ chmod 700 /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/alpha
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/alpha/pg_wal
student$ sudo tar xzf /home/student/tmp/backup/base.tar.gz -C /var/lib/postgresql/16/beta
student$ sudo tar xzf /home/student/tmp/backup/pg_wal.tar.gz -C /var/lib/postgresql/16/beta/pg_wal

Архив сегментов WAL.

postgres$ rm -rf /var/lib/postgresql/archive
postgres$ mkdir /var/lib/postgresql/archive

Настройка архивации на мастере (alpha). Мы заменили логическую связку в команде архивации, чтобы при наличии файла она сразу возвращала 0.

postgres$ cat << EOF | tee -a /var/lib/postgresql/16/alpha/postgresql.auto.conf
archive_mode = on
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p /var/lib/postgresql/archive/%f'
EOF
archive_mode = on
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p
/var/lib/postgresql/archive/%f'

Настройка архивации на реплике (beta). Используем режим always и ту же команду архивации.

postgres$ cat << EOF | tee -a /var/lib/postgresql/16/beta/postgresql.auto.conf
archive_mode = always
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p /var/lib/postgresql/archive/%f'
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'
EOF
archive_mode = always
archive_command = 'test -f /var/lib/postgresql/archive/%f || cp %p
/var/lib/postgresql/archive/%f'
primary_conninfo = 'user=student port=5432'
primary_slot_name = 'replica'

На реплике нужен еще standby.signal.

postgres$ touch /var/lib/postgresql/16/beta/standby.signal

Запускаем мастер и создаем слот репликации.

student$ sudo pg_ctlcluster 16 alpha start
student$ pg_receivewal --create-slot --slot=replica

Теперь стартуем реплику.

student$ sudo pg_ctlcluster 16 beta start

Page 24: Page 24

student$ psql -d replica_switchover

Повторим опыт. Переключаем сегмент:

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/3006658
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

Проверяем состояние архивации:

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     | last_failed_wal
--------------------------+--------------------------+-----------------
 000000010000000000000004 | 000000010000000000000003 |
(1 row)

Заполненный сегмент попал в архив. На сервере alpha возникли проблемы с архивацией, команда возвращает 1:

α=> ALTER SYSTEM SET archive_command = 'exit 1';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha reload

Alpha продолжает генерировать сегменты WAL.

α=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/4002C08
(1 row)
α=> INSERT INTO test SELECT now();
INSERT 0 1

Но основной сервер их не архивирует:

α=> SELECT
  pg_walfile_name(pg_current_wal_lsn()) current_wal,
  last_archived_wal,
  last_failed_wal
FROM pg_stat_archiver;
       current_wal        |    last_archived_wal     |     last_failed_wal
--------------------------+--------------------------+--------------------------
 000000010000000000000005 | 000000010000000000000003 | 000000010000000000000004
(1 row)

Однако архивация с реплики срабатывает и сегмент оказывается в архиве:

student$ ls -l /var/lib/postgresql/archive
total 32768
-rw------- 1 postgres postgres 16777216 апр 10 18:08 000000010000000000000003
-rw------- 1 postgres postgres 16777216 апр 10 18:08 000000010000000000000004

Выполняем переключение на реплику.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)
student$ sudo pg_ctlcluster 16 beta promote

Beta стала основным сервером и генерирует файлы WAL.

student$ psql -p 5433 -d replica_switchover

Page 25: Page 25

β=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/5002158
(1 row)
β=> INSERT INTO test SELECT now();
INSERT 0 1

Еще раз заглянем в архив:

student$ ls -l /var/lib/postgresql/archive
total 65540
-rw------- 1 postgres postgres 16777216 апр 10 18:08 000000010000000000000003
-rw------- 1 postgres postgres 16777216 апр 10 18:08 000000010000000000000004
-rw------- 1 postgres postgres 16777216 апр 10 18:08 000000010000000000000005.partial
-rw------- 1 postgres postgres 16777216 апр 10 18:08 000000020000000000000005
-rw------- 1 postgres postgres       41 апр 10 18:08 00000002.history

В архиве появились файлы реплики, пропусков в нем нет, проблема решена.

Page 26: Итоги

Переключение используется как в штатных, так и в нештатных ситуациях

  • После переключения бывший мастер надо вернуть в строй
  • Обе процедуры должны быть заранее отработаны
  • Файловый архив журнала предзаписи требует внимания

Page 27: Практика

  1. Выполните необходимую настройку потоковой репликации с сервера alpha на beta с использованием слота, без непрерывного архивирования. 2. Сымитируйте сбой сервера alpha и переключитесь на сервер beta. 3. Верните в строй сервер alpha в качестве реплики, выполнив резервную копию с нового мастера beta и настроив необходимые параметры.
  • Убедитесь, что репликация работает и использует слот. 4. Переключитесь на сервер alpha, чтобы бывший мастер снова стал основным сервером.

Комментарии преподавателя

  1. Для имитации сбоя можно остановить сервер в режиме immediate:

pg_ctlcluster 16 имя_кластера stop -m immediate –skip-systemctl-redirect

или завершить основной процесс postgres:

kill -QUIT номер_процесса

Номер процесса можно найти в первой строке файла PGDATA/postmaster.pid.

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

Page 28: 1. Настройка репликации без архива

Настраиваем репликацию. Остановим сервер beta и очистим его каталог данных.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta

Формируем автономную резервную копию в каталоге данных сервера beta, предварительно создав слот replica.

postgres$ pg_basebackup \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R \
  --create-slot \
  --slot=replica
postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer host=''/var/run/postgresql'' port=5432 sslmode=prefer
sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0
target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'

Запускаем сервер в режиме реплики.

student$ sudo pg_ctlcluster 16 beta start

Проверим настройки. Выполним несколько команд на мастере:

α=> CREATE DATABASE replica_switchover;
CREATE DATABASE
α=> \c replica_switchover
You are now connected to database "replica_switchover" as user "student".
α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Привет, мир!');
INSERT 0 1

Проверим реплику:

student$ psql -p 5433
β=> \c replica_switchover
You are now connected to database "replica_switchover" as user "student".
β=> SELECT * FROM test;
      s
--------------
 Привет, мир!
(1 row)

2. Сбой основного сервера и переход на реплику

Имитируем аварийную остановку мастера.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)

Повышаем реплику.

student$ sudo pg_ctlcluster 16 beta promote

Page 29: 3. Возвращение в строй бывшего мастера

Создаем автономную резервную копию сервера beta в каталоге данных сервера alpha. Перед этим удалим конфигурационный файл, иначе pg_basebackup его скопирует и допишет параметры.

postgres$ rm -f /var/lib/postgresql/16/beta/postgresql.auto.conf

Очистим каталог данных на сервере alpha.

postgres$ rm -rf /var/lib/postgresql/16/alpha

Запускаем утилиту резервного копирования, попросив ее создать слот и настройки репликации.

postgres$ pg_basebackup \
  --port=5433 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/alpha \
  -R \
  --create-slot \
  --slot=replica

Параметры конфигурации и сигнальный файл подготовлены утилитой:

postgres$ cat /var/lib/postgresql/16/alpha/postgresql.auto.conf
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5433 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
postgres$ ls -l /var/lib/postgresql/16/alpha/standby.signal
-rw------- 1 postgres postgres 0 апр 10 18:20
/var/lib/postgresql/16/alpha/standby.signal
student$ sudo pg_ctlcluster 16 alpha start

Слот репликации инициализировался и используется:

β=> SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+----------
slot_name           | replica
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | t
active_pid          | 99939
xmin                |
catalog_xmin        |
restart_lsn         | 0/5000060
confirmed_flush_lsn |
wal_status          | reserved
safe_wal_size       |
two_phase           | f
conflicting         |

Проверим репликацию:

β=> INSERT INTO test VALUES ('Я — бывшая реплика (новый мастер).');
INSERT 0 1
student$ psql -d replica_switchover
α=> SELECT * FROM test;
                 s
------------------------------------
 Привет, мир!
 Я — бывшая реплика (новый мастер).
(2 rows)

4. Переключение на новую реплику

Повышаем реплику.

Page 30: Page 30

student$ sudo pg_ctlcluster 16 alpha promote
α=> select pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

В итоге прежний мастер снова стал основным сервером.

Уборка

Удалим слот и базу данных, сбросим измененные параметры.

α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_switchover;
DROP DATABASE
student$ sudo pg_ctlcluster 16 alpha restart
β=> \c postgres
You are now connected to database "postgres" as user "student".
β=> SELECT pg_drop_replication_slot('replica');
 pg_drop_replication_slot
--------------------------
(1 row)
student$ sudo pg_ctlcluster 16 beta stop

Page 31: Практика+

  1. Настройте сервер alpha, чтобы после контрольной точки сохранялись два дополнительных сегмента WAL, и запустите физическую репликацию с alpha на сервер beta без слота репликации. 2. Аварийно остановите сервер alpha и выполните повышение beta. Запустите alpha и измените данные на нем. Убедитесь, что данные на alpha и beta не совпадают. 3. Остановите сервер alpha и запустите его в качестве реплики сервера beta, используя утилиту pg_rewind.

Комментарии преподавателя

  1. Настройте репликацию с простейшими настройками и без слота репликации. Для гарантии сохранения двух дополнительных сегментов WAL задайте подходящее значение wal_keep_size.

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

Page 32: 1. Репликация

Мы не используем слоты репликации, поэтому потребуем сохранять два дополнительных сегмента WAL.

α=> ALTER SYSTEM SET wal_keep_size = 32;
ALTER SYSTEM

Остановим сервер beta и сделаем его репликой для сервера alpha.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup --checkpoint=fast -R --pgdata=/var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 beta start

Создадим базу данных и таблицу в ней.

α=> CREATE DATABASE replica_switchover;
CREATE DATABASE
α=> \c replica_switchover
You are now connected to database "replica_switchover" as user "student".
α=> CREATE TABLE test AS SELECT 'Сервер alpha — мастер.' txt;
SELECT 1

Изменения реплицированы на сервер beta.

student$ psql -p 5433 -d replica_switchover
β=> SELECT * FROM test;
          txt
------------------------
 Сервер alpha — мастер.
(1 row)

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

α=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 100787
usesysid         | 10
usename          | postgres
application_name | 16/beta
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2026-04-10 18:21:16.297541+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3432440
write_lsn        | 0/3432440
flush_lsn        | 0/3432440
replay_lsn       | 0/3432440
write_lag        | 00:00:00.000407
flush_lag        | 00:00:00.007474
replay_lag       | 00:00:00.008946
sync_priority    | 0
sync_state       | async
reply_time       | 2026-04-10 18:21:23.621078+03

На реплике можно посмотреть статистику приемника WAL:

β=> SELECT pid, status, written_lsn FROM pg_stat_wal_receiver;

Page 33: 2. Разрыв репликации

  pid   |  status   | written_lsn
--------+-----------+-------------
 100786 | streaming | 0/3432440
(1 row)

На мастер-сервере отключено питание.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/alpha/postmaster.pid)

Повышаем beta.

student$ sudo pg_ctlcluster 16 beta promote

Изменяем данные на новом мастере.

β=> INSERT INTO test VALUES('Сервер beta повышен, он выполняет пишущие транзакции.');
INSERT 0 1

На сервере alpha питание восстановлено.

student$ sudo pg_ctlcluster 16 alpha start
student$ psql -d replica_switchover
α=> INSERT INTO test VALUES('Die Hard...');
INSERT 0 1

Данные на серверах разошлись.

α=> SELECT * FROM test;
          txt
------------------------
 Сервер alpha — мастер.
 Die Hard...
(2 rows)
β=> SELECT * FROM test;
                          txt
-------------------------------------------------------
 Сервер alpha — мастер.
 Сервер beta повышен, он выполняет пишущие
транзакции.
(2 rows)

Остановим сервер alpha (бывший мастер).

student$ sudo pg_ctlcluster 16 alpha stop

3. Ввод в строй бывшего мастера

Мы хотим запустить сервер alpha как реплику. Для этого нужно вернуть его к состоянию, совместимому с beta. Используем штатную утилиту.

postgres$ /usr/lib/postgresql/16/bin/pg_rewind \
  -D /var/lib/postgresql/16/alpha \
  --config-file=/etc/postgresql/16/alpha/postgresql.conf \
  --source-server='user=postgres port=5433' \
  -R
pg_rewind: servers diverged at WAL location 0/3432440 on timeline 1
pg_rewind: rewinding from last common checkpoint at 0/2000060 on timeline 1
pg_rewind: Done!

На сервере beta выполняются пишущие транзакции.

β=> INSERT INTO test VALUES('На сервере beta продолжаются изменения.');
INSERT 0 1

Данные, измененные на сервере beta, получены сервером alpha:

student$ sudo pg_ctlcluster 16 alpha start

Page 34: Page 34

Source slide page 34: Page 34{#source-figure-034 caption=“Source slide page 34: Page 34”}

student$ psql -d replica_switchover
α=> SELECT * FROM test;
                          txt
-------------------------------------------------------
 Сервер alpha — мастер.
 Сервер beta повышен, он выполняет пишущие
транзакции.
 На сервере beta продолжаются изменения.
(3 rows)

Уборка

Удалим созданную базу данных и сбросим настройки конфигурационных параметров.

β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> DROP DATABASE replica_switchover;
DROP DATABASE
β=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop
α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> SELECT pg_promote();
 pg_promote
------------
 t
(1 row)
α=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

06 Занятие 06: Репликация / Логическая репликация

Исходная страница в нативном Markdown: Репликация / Логическая репликация
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_06_replica_logical.html Archive: DBA3-handouts-16.zip Страниц PDF: 43

Page 1: Репликация / Логическая репликация

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Сравнение логической и физической репликации
  • Публикации и подписки
  • Начальная синхронизация
  • Идентификация и фильтрация строк
  • Логическое декодирование
  • Большие транзакции
  • Конфликты
  • Источники репликации

Page 3: Сравнение

Физическая

  • мастер-реплика: поток данных только в одну сторону
  • передача потока журнальных записей или файлов журнала
  • требуется двоичная совместимость серверов
  • реплицируется только весь кластер
  • Логическая
    • публикация-подписки: у сервера нет выделенной роли
    • передача изменений табличных строк
    • требуется совместимость на уровне протокола
    • возможна выборочная репликация отдельных таблиц

Комментарии преподавателя

При физической репликации серверы имеют назначенные роли: мастер и реплика. Мастер передает журнальные записи на реплику, которая применяет эти записи к своим файлам данных. Применение происходит чисто механически, без «понимания смысла» изменений, поэтому важна двоичная совместимость между серверами (одинаковые платформы и основные версии PostgreSQL). Поскольку журнал общий для всего кластера, то и реплицировать можно только кластер целиком.

При логической репликации на одном сервере создается публикация, другие серверы могут на нее подписаться. У сервера нет выделенной роли: один и тот же сервер может как публиковать изменения, так и подписываться на другие (или даже свои) публикации.

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

https://postgrespro.ru/docs/postgresql/16/logical-replication

Page 4: Схема репликации

Source slide page 4: Схема репликации{#source-figure-004 caption=“Source slide page 4: Схема репликации”}

  • max_wal_senders = 10
  • max_logical_replication_workers = 4
  • max_replication_slots = 10
  • max_worker_processes = 8

Комментарии преподавателя

Логическая репликация использует модель «публикация–подписка».

При старте экземпляра запускается фоновый процесс logical replication launcher, периодически проверяющий таблицу pg_subscription системного каталога на предмет появления новой подписки. Как только подписка появляется, для нее запускается фоновый процесс logical replication apply worker. Он соединяется с сервером публикации, на котором, как при любом подключении по протоколу репликации, запускается процесс wal sender. Этот процесс читает журнал предзаписи, отбирает нужные и преобразует их в платформонезависимые сообщения протокола. При этом обязательно используется слот логической репликации.

На стороне подписки рабочий процесс принимает сообщения и применяет изменения. В это же время сервер-подписчик может принимать обычные запросы на чтение и запись.

Обратите внимание, что на публикующем сервере может быть запущено много процессов wal sender — по одному на каждую подписку. Значения параметров max_wal_senders и max_replication_slots должны быть не меньше этого количества.

На сервере подписки необходимо установить параметры max_logical_replication_workers (для процессов, принимающих изменения по подписке) и в целом max_worker_processes (как минимум на единицу больше, учитывая logical replication launcher, но этот пул используется и для других нужд).

https://postgrespro.ru/docs/postgresql/16/logical-replication-architecture

Page 5: Публикация

Объект базы данных

  • выдает изменения данных построчно в порядке фиксации транзакций
  • только базовые и секционированные таблицы
  • wal_level = logical
  • Публикуются операции
    • insert, update, delete, truncate
  • Права
    • создать публикацию — привилегия CREATE на базу данных
    • добавить таблицу — владелец или суперпользователь
    • добавить все таблицы схемы или базы данных — суперпользователь

Комментарии преподавателя

На одном сервере создается публикация, выделяющая изменения в одной или нескольких таблицах (базовых или секционированных) одной базы данных. Чтобы создать публикацию, нужна привилегия CREATE на базу данных. Для репликации из нескольких баз данных потребуется создать несколько публикаций.

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

Для работы логической репликации в журнале публикующего сервера необходима дополнительная информация (параметр wal_level = logical).

Публикация включает в себя изменения строк, происходящие в таблицах в результате выполнения команд DML. Команда MERGE, выполненная на публикующем сервере, применяется как INSERT, UPDATE или DELETE.

По умолчанию изменения передаются не сразу, а только при фиксации транзакции.

https://postgrespro.ru/docs/postgresql/16/logical-replication-publication

https://postgrespro.ru/docs/postgresql/16/sql-createpublication

Page 6: Подписка

Объект базы данных

  • получает сообщения от одной или нескольких публикаций
  • возможна подписка в двоичном формате
  • применяет изменения построчно
  • таблицы и столбцы сопоставляются по полным именам
  • Права
    • создание подписки — роль pg_create_subscription и суперпользователи

Комментарии преподавателя

Определив в базе данных подписку на публикации, можно получать и применять изменения. По сути, подписка — это подключение к публикующему серверу, созданное для репликации данных.

Создавать подписки могут члены роли pg_create_subscription и суперпользователи.

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

Таблицы идентифицируются по полным именам (включая схему), столбцы также идентифицируются по именам. Это позволяет подписке использовать отличающуюся схему данных (например, иметь в таблице дополнительные столбцы).

При создании подписки можно указать параметр binary, устанавливающий передачу данных в двоичном формате. Двоичный формат может ускорить обработку если определены функции двоичного получения и отправки для требуемых типов данных. Однако при использовании двоичного формата важна совместимость машинного представления данных на разных платформах.

https://postgrespro.ru/docs/postgresql/16/logical-replication-subscription

Page 7: Публикации и подписки

Source slide page 7: Публикации и подписки{#source-figure-007 caption=“Source slide page 7: Публикации и подписки”}

Комментарии преподавателя

На публикующем сервере можно распределять объекты по публикациям любым образом. Можно включить все таблицы в одну публикацию, можно разделить их между несколькими публикациями — это не влияет на производительность.

Однако каждая подписка создает отдельное подключение к публикующему серверу, нагружая его лишней работой. На публикующем сервере в таком случае будет запущено несколько процессов wal sender, каждый из которых будет самостоятельно читать WAL и выполнять логическое декодирование (рассматривается ниже) в своей локальной памяти. Поэтому если необходимо подписаться на несколько публикаций, это лучше сделать в одной общей подписке.

Напомним, что публикации и подписки — объекты базы данных, поэтому для каждой базы потребуется отдельное соединение.

Мониторинг логической репликации на публикующем сервере идентичен мониторингу физической репликации. Здесь важнейшие инструменты — представления pg_stat_replication и pg_replication_slots.

Информация о состоянии подписок находится в pg_subscription.

https://postgrespro.ru/docs/postgresql/16/catalog-pg-subscription#CATALO G-PG-SUBSCRIPTION

Данные о применении изменений в представлении pg_stat_subscription.

https://postgrespro.ru/docs/postgresql/16/monitoring-stats#MONITORING-P G-STAT-SUBSCRIPTION

Данные об ошибках в процессе репликации в pg_stat_subscription_stats.

https://postgrespro.ru/docs/postgresql/16/monitoring-stats#MONITORING-P G-STAT-SUBSCRIPTION-STATS

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

Мы собираемся настроить логическую репликацию таблицы test с сервера alpha на сервер beta. Для начала создадим базу данных.

α=> CREATE DATABASE replica_logical;
CREATE DATABASE
α=> \c replica_logical
You are now connected to database "replica_logical" as user "student".

Для работы логической репликации понадобится изменить уровень журнала.

α=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Второй кластер изначально будет копией первого, поэтому выполним резервное копирование в каталог PGDATA второго сервера.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup --checkpoint=fast --pgdata=/var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 beta start
student$ psql -d replica_logical

Теперь на первом сервере создадим таблицу и заполним ее данными.

α=> CREATE TABLE test(
  id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text,
  cluster_name text DEFAULT 'alpha'
);
CREATE TABLE

На втором сервере таблицы test нет. Поскольку команды DDL не реплицируются, таблицу необходимо создать вручную. Структура ее может отличаться, но таблица должна содержать все публикуемые столбцы.

student$ psql -p 5433 -d replica_logical
β=> CREATE TABLE test(
  id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text,
  cluster_name text DEFAULT 'beta',
  additional text
);
CREATE TABLE

На первом сервере создаем публикацию.

α=> CREATE PUBLICATION test_pub;
CREATE PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
(1 row)

Заметьте, что публикация создана, хотя в ней нет ни одной таблицы. Обычно публикацию создают, указав: список таблиц, причем в этом случае можно указать публикуемые столбцы; все таблицы в заданных схемах (требует прав суперпользователя);

Page 9: Page 9

все таблицы базы данных (требует прав суперпользователя). Добавим в публикацию таблицу.

α=> ALTER PUBLICATION test_pub ADD TABLE test;
ALTER PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test"

На втором сервере подписываемся на публикацию.

β=> CREATE SUBSCRIPTION test_sub
CONNECTION 'host=localhost port=5432 user=student password=student dbname=replica_logical'
PUBLICATION test_pub;
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION
β=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)

Проверим созданную подписку.

α=> INSERT INTO test(descr) VALUES ('Раз');
INSERT 0 1
β=> SELECT * FROM test;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | Раз   | alpha        |
(1 row)

Значения столбцов id, descr и cluster_name были получены из публикации. Начиная с PostgreSQL 15, можно публиковать не все, а только некоторые столбцы:

α=> ALTER PUBLICATION test_pub SET TABLE test (id, descr);
ALTER PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test" (id, descr)

Проверим:

α=> INSERT INTO test(descr) VALUES ('Два');
INSERT 0 1
β=> SELECT * FROM test;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | Раз   | alpha        |
  2 | Два   | beta         |
(2 rows)

Обратите внимание на столбец cluster_name — теперь он получил значение по умолчанию (beta) на подписчике.

Page 10: Page 10

Состояние подписки можно посмотреть в представлении:

β=> SELECT * FROM pg_stat_subscription \gx
-[ RECORD 1 ]---------+------------------------------
subid                 | 16419
subname               | test_sub
pid                   | 78025
leader_pid            |
relid                 |
received_lsn          | 0/3084B70
last_msg_send_time    | 2026-04-10 18:09:10.378621+03
last_msg_receipt_time | 2026-04-10 18:09:10.379748+03
latest_end_lsn        | 0/3084B70
latest_end_time       | 2026-04-10 18:09:10.378621+03

received_lsn — позиция в журнале, до которой получены изменения; latest_end_lsn — позиция в журнале, подтвержденная процессу wal sender. К процессам сервера добавился logical replication worker (его номер указан в pg_stat_subscription.pid):

student$ ps f -C postgres -o pid,command | grep beta
  77670 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/beta -c
config_file=/etc/postgresql/16/beta/postgresql.conf
  77671  \_ postgres: 16/beta: checkpointer
  77672  \_ postgres: 16/beta: background writer
  77674  \_ postgres: 16/beta: walwriter
  77675  \_ postgres: 16/beta: autovacuum launcher
  77676  \_ postgres: 16/beta: logical replication launcher
  77796  \_ postgres: 16/beta: student replica_logical [local] idle
  78025  \_ postgres: 16/beta: logical replication apply worker for subscription 16419

Page 11: Ограничения

Не реплицируются

  • последовательности материализованные представления временные и нежурналируемые таблицы внешние таблицы большие объекты DDL Требование к TRUNCATE
    • таблицы, связанные внешними ключами, должны быть в одной
    • подписке

Комментарии преподавателя

Реплицируются только изменения содержимого базовых и секционированных таблиц, вызванные командами DML.

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

Команды DDL также не реплицируются, поэтому нужно предварительно создать таблицы на стороне подписки.

Команда TRUNCATE реплицируется успешно только в случае, если все таблицы подписчика, внешние ключи которых ссылаются на опустошаемые таблицы, включены в подписку.

https://postgrespro.ru/docs/postgresql/16/logical-replication-restrictions

Page 12: Начальная синхронизация

Source slide page 12: Начальная синхронизация{#source-figure-012 caption=“Source slide page 12: Начальная синхронизация”}

Один процесс синхронизации для каждой таблицы

  • но не более max_sync_workers_per_subscription = 2

Комментарии преподавателя

По умолчанию (если не задано значение параметра copy_data = false) при создании подписки выполняется начальная синхронизация содержимого таблиц. Она происходит бесшовно благодаря использованию механизма экспорта снимка данных.

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

Максимальное количество параллельных процессов синхронизации ограничено параметром max_sync_workers_per_subscription (по умолчанию — два процесса).

Состояние репликации для каждой таблицы в подписке показывает представление pg_subscription_rel.

https://postgrespro.ru/docs/postgresql/16/catalog-pg-subscription-rel#CATAL OG-PG-SUBSCRIPTION-REL

Если для подписки указать параметр binary, то в двоичном формате будет выполняться не только репликация, но и начальная синхронизация.

Page 13: Начальная синхронизация

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

α=> CREATE TABLE t1 AS
  SELECT id, 'Это строка номер '||id descr FROM generate_series(1,1_000_000) id;
SELECT 1000000
α=> CREATE TABLE t2 AS
  SELECT id, 'Это строка номер '||id descr FROM generate_series(1,1_000_000) id;
SELECT 1000000
α=> CREATE TABLE t3 AS
  SELECT id, 'Это строка номер '||id descr FROM generate_series(1,1_000_000) id;
SELECT 1000000
α=> CREATE PUBLICATION t123_pub FOR TABLE t1, TABLE t2, TABLE t3;
CREATE PUBLICATION

На подписчике создадим пустые таблицы той же структуры:

β=> CREATE TABLE t1( id integer, descr text );
CREATE TABLE
β=> CREATE TABLE t2( id integer, descr text );
CREATE TABLE
β=> CREATE TABLE t3( id integer, descr text );
CREATE TABLE

Подпишемся на публикацию и сразу же заглянем в системную таблицу, показывающую состояние репликации каждой таблицы, и в список процессов.

β=> CREATE SUBSCRIPTION t123_sub
CONNECTION 'host=localhost port=5432 user=student password=student dbname=replica_logical'
PUBLICATION t123_pub;
NOTICE:  created replication slot "t123_sub" on publisher
CREATE SUBSCRIPTION
β=> SELECT *, srrelid::regclass::text tablename FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate | srsublsn  | tablename
---------+---------+------------+-----------+-----------
   16419 |   16411 | r          | 0/3084058 | test
   16435 |   16430 | i          |           | t3
   16435 |   16420 | i          |           | t1
   16435 |   16425 | i          |           | t2
(4 rows)

Здесь мы видим, что таблица test реплицируется (r), а три новые таблицы пока еще синхронизируются (i).

student$ ps f -C postgres -o pid,command | grep beta
  77670 /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/beta -c
config_file=/etc/postgresql/16/beta/postgresql.conf
  77671  \_ postgres: 16/beta: checkpointer
  77672  \_ postgres: 16/beta: background writer
  77674  \_ postgres: 16/beta: walwriter
  77675  \_ postgres: 16/beta: autovacuum launcher
  77676  \_ postgres: 16/beta: logical replication launcher
  77796  \_ postgres: 16/beta: student replica_logical [local] idle
  78025  \_ postgres: 16/beta: logical replication apply worker for subscription 16419
  78721  \_ postgres: 16/beta: logical replication apply worker for subscription 16435
  78739  \_ postgres: 16/beta: logical replication worker for subscription 16435 sync
16430
  78740  \_ postgres: 16/beta: logical replication worker for subscription 16435 sync
16420

Добавился процесс apply worker для новой подписки и два процесса синхронизации. Таблиц три, но количество

Page 14: Page 14

Source slide page 14: Page 14{#source-figure-014 caption=“Source slide page 14: Page 14”}

одновременно работающих процессов ограничено параметром:

β=> SHOW max_sync_workers_per_subscription;
 max_sync_workers_per_subscription
-----------------------------------
 2
(1 row)

Дождемся окончания синхронизации. Теперь все таблицы реплицируются:

β=> SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   16419 |   16411 | r          | 0/3084058
   16435 |   16430 | r          | 0/158C0760
   16435 |   16420 | r          | 0/158C0760
   16435 |   16425 | r          | 0/158C07D0
(4 rows)
β=> SELECT count(*) FROM t1;
  count
---------
 1000000
(1 row)

Удалим публикацию и подписку, они нам больше не нужны:

β=> DROP SUBSCRIPTION t123_sub;
NOTICE:  dropped replication slot "t123_sub" on publisher
DROP SUBSCRIPTION
α=> DROP PUBLICATION t123_pub;
DROP PUBLICATION

Page 15: Идентификация строк

Для операций изменения и удаления необходимо выбрать подходящую строку

  • Логический идентификатор строк
ALTER TABLE ... REPLICA IDENTITY ...
  • DEFAULT
  • столбцы первичного ключа (по умолчанию)
USING INDEX
столбцы уникального индекса с ограничением NOT NULL
  • FULL
  • все столбцы
  • NOTHING
  • без идентификации
  • (по умолчанию для системного каталога)

Комментарии преподавателя

Вставка новых строк на стороне подписки происходит однозначно. Сложнее при изменениях и удалениях — в этих случаях надо определить, к какой строке на подписчике применять операцию. Поэтому для каждой таблицы определяется логический идентификатор строк. По умолчанию строки идентифицируются столбцами первичного ключа, но идентификатор можно изменить командой ALTER TABLE … REPLICA IDENTITY.

Строки можно идентифицировать по другому уникальному индексу, в котором все столбцы имеют ограничение NOT NULL.

Еще один вариант — идентификация по всем столбцам. Начиная с версии 16, для поиска строк в этом случае может использоваться полный индекс на основе B-дерева, первое поле которого должно быть столбцом из публикации. Версии 17+ могут использовать и хеш-индексы. Если подходящего индекса нет, выполняется полное сканирование, что крайне неэффективно для больших таблиц.

Можно вообще отказаться от поддержки репликации для некоторых таблиц (по умолчанию так настроены таблицы системного каталога).

При изменении или удалении в WAL уровня replica не попадают значения столбцов удаляемой версии строки — это одна из причин, по которой на публикующем сервере необходим уровень журнала logical. На этом уровне в журнал дополнительно записываются старые значения столбцов, входящих в логический идентификатор. По этой причине при идентификации по всем столбцам объем журнала может существенно увеличиться по сравнению с уровнем replica.

https://postgrespro.ru/docs/postgresql/16/sql-altertable#SQL-ALTERTABLE-REPLICA-IDENTITY

Page 16: Фильтрация строк

Фильтр в публикации

  • Возможна фильтрация
    • insert
    • по любым публикуемым столбцам
    • delete
    • по столбцам логического идентификатора
    • update
    • по столбцам логического идентификатора;
    • в результате может получиться insert или delete
    • truncate
    • фильтр игнорируется
  • Только простые выражения

Комментарии преподавателя

По умолчанию публикующий сервер отправляет все строки опубликованных таблиц.

В PostgreSQL 15 появилась возможность указать выражение фильтра; строки, для которых выражение имеет значение false или NULL, опубликованы не будут.

Фильтрация не распространяется на операцию truncate. Для операций insert в выражение фильтра можно включать любые публикуемые столбцы, а для update и delete — только столбцы, входящие в логический идентификатор строк.

Для операции update выражение фильтра вычисляется и для старой, и для новой версий строки. Если для обеих версий выражение фильтра дает false или NULL, строка не реплицируется. Если старая строка не соответствует фильтру, а новая — соответствует, то выполняется вставка новой строки. Если, наоборот, старая версия соответствует, а новая — нет, строка удаляется. Если обе соответствуют условию фильтра, выполняется обновление.

Фильтровать можно лишь с помощью простых выражений: запрещены пользовательские функции, операторы, типы и правила сортировки; нельзя ссылаться на системные столбцы; нельзя обращаться ко встроенным функциям с категориями изменчивости volatile и stable.

https://postgrespro.ru/docs/postgresql/16/logical-replication-row-filter.html

Page 17: Идентификация и фильтрация

По умолчанию логический идентификатор строк определяется столбцами первичного ключа:

α=> SELECT relreplident FROM pg_class WHERE relname = 'test';
 relreplident
--------------
 d
(1 row)

d — по умолчанию (первичный ключ, если есть); i — индекс; f — все столбцы; n — никакие. Допустим, нужно реплицировать только те строки, у которых в поле cluster_name находится значение alpha. Столбец cluster_name участвует в выражении фильтра, поэтому его нужно опубликовать (сейчас он не опубликован). Публикуем все столбцы, не указывая их список.

α=> ALTER PUBLICATION test_pub SET TABLE test WHERE (cluster_name = 'alpha');
ALTER PUBLICATION
α=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test" WHERE (cluster_name = 'alpha'::text)

Проверим, как работает репликация изменений.

α=> INSERT INTO test(descr) VALUES ('Три');
INSERT 0 1
α=> INSERT INTO test(descr, cluster_name) VALUES ('Четыре', 'gamma');
INSERT 0 1
α=> SELECT * FROM test;
 id | descr  | cluster_name
----+--------+--------------
  1 | Раз    | alpha
  2 | Два    | alpha
  3 | Три    | alpha
  4 | Четыре | gamma
(4 rows)
β=> SELECT * FROM test;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | Раз   | alpha        |
  2 | Два   | beta         |
  3 | Три   | alpha        |
(3 rows)

Строка со значением gamma не реплицировалась. Попробуем изменить строку.

α=> UPDATE test SET cluster_name = 'delta' WHERE id = 2;
ERROR:  cannot update table "test"
DETAIL:  Column used in the publication WHERE expression is not part of the replica
identity.

Для update и delete в выражении фильтра допускаются только столбцы, входящие в логический идентификатор строк, а для insert — любые столбцы.

Page 18: Page 18

Изменим условие фильтрации.

α=> ALTER PUBLICATION test_pub SET TABLE test WHERE (id > 0);
ALTER PUBLICATION

Теперь мы можем изменить строку:

α=> UPDATE test SET cluster_name = 'delta' WHERE id = 2;
UPDATE 1
β=> SELECT * FROM test WHERE id = 2;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  2 | Два   | delta        |
(1 row)

Строка изменилась и на подписчике. Новое условие фильтрации работает:

α=> INSERT INTO test(id, descr) VALUES (0, 'Ноль');
INSERT 0 1
α=> SELECT * FROM test ORDER BY id;
 id | descr  | cluster_name
----+--------+--------------
  0 | Ноль   | alpha
  1 | Раз    | alpha
  2 | Два    | delta
  3 | Три    | alpha
  4 | Четыре | gamma
(5 rows)
β=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | Раз   | alpha        |
  2 | Два   | delta        |
  3 | Три   | alpha        |
(3 rows)

Строка с id = 0 не была опубликована и поэтому не реплицировалась.

Page 19: Логическое декодирование

Source slide page 19: Логическое декодирование{#source-figure-019 caption=“Source slide page 19: Логическое декодирование”}

Комментарии преподавателя

При потоковой физической репликации процесс wal sender читает записи WAL и передает их реплике в неизменном виде.

При логической репликации записи преобразуются. Модуль логического декодирования отбирает из них те, которые соответствуют публикуемым операциям и таблицам, учитывая столбцы и условия фильтров. Отобранные записи преобразуется в информацию о логических операциях, направляются в переупорядочивающий буфер и сохраняются в нем в соответствии с номерами транзакций.

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

● для update — новые значения всех столбцов, а не только измененных;

● для update и delete — старые значения столбцов, входящих в логический идентификатор;

● для commit — OID базы данных.

Переупорядочивающий буфер находится в локальной памяти процесса wal sender. Если объем данных в нем приблизится к значению параметра logical_decoding_work_mem, самая большая транзакция сбрасывается в слот репликации (файл в PGDATA/pg_replslots/имя).

При фиксации транзакции все ее операции направляются в модуль вывода, который преобразует их в сообщения протокола репликации.

https://postgrespro.ru/docs/postgresql/16/logicaldecoding

https://postgrespro.ru/docs/postgresql/16/runtime-config-resource#GUC-LO GICAL-DECODING-WORK-MEM

Page 20: Слот и логическое декодирование

Очистим содержимое таблицы и посмотрим, как работает логическое декодирование.

α=> TRUNCATE test;
TRUNCATE TABLE
β=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name | additional
----+-------+--------------+------------
(0 rows)

Создадим вручную слот логической репликации. Для передачи изменений подписчику используется модуль вывода pgoutput, а для наблюдения удобнее модуль вывода test_decoding:

α=> SELECT pg_create_logical_replication_slot('test_slot','test_decoding');
 pg_create_logical_replication_slot
------------------------------------
 (test_slot,0/158DFF40)
(1 row)

Теперь на сервере alpha два слота:

α=> SELECT slot_name, plugin, slot_type, active FROM pg_replication_slots;
 slot_name |    plugin     | slot_type | active
-----------+---------------+-----------+--------
 test_sub  | pgoutput      | logical   | t
 test_slot | test_decoding | logical   | f
(2 rows)

В отдельном сеансе начнем транзакцию и вставим строку в таблицу:

student$ psql -d replica_logical
α=> BEGIN;
BEGIN
α=> INSERT INTO test(descr) VALUES ('Πέντε');
INSERT 0 1

Модуль вывода может запросить у слота изменения:

α=> SELECT * FROM pg_logical_slot_get_changes('test_slot', NULL, NULL);
 lsn | xid | data
-----+-----+------
(0 rows)

Транзакция не завершена, модуль вывода пока ничего не получил. Сделаем еще несколько изменений и завершим транзакцию.

α=> UPDATE test SET descr = 'Пять' WHERE id = 5;
UPDATE 1
α=> UPDATE test SET id = 1, descr='Раз' WHERE id = 5;
UPDATE 1
α=> COMMIT;
COMMIT
α=> SELECT * FROM pg_logical_slot_get_changes('test_slot', NULL, NULL) \gx (xheader_width=90)

Page 21: Page 21

Source slide page 21: Page 21{#source-figure-021 caption=“Source slide page 21: Page 21”}

-[ RECORD 1 ]-----------------------------------------------------------------------------
lsn  | 0/158F41D0
xid  | 766
data | BEGIN 766
-[ RECORD 2 ]-----------------------------------------------------------------------------
lsn  | 0/158F41D0
xid  | 766
data | table public.test: INSERT: id[integer]:5 descr[text]:'Πέντε'
cluster_name[text]:'alpha'
-[ RECORD 3 ]-----------------------------------------------------------------------------
lsn  | 0/158FA308
xid  | 766
data | table public.test: UPDATE: id[integer]:5 descr[text]:'Пять'
cluster_name[text]:'alpha'
-[ RECORD 4 ]-----------------------------------------------------------------------------
lsn  | 0/158FB360
xid  | 766
data | table public.test: UPDATE: old-key: id[integer]:5 new-tuple: id[integer]:1
descr[text]:'Раз' cluster_name[text]:'alpha'
-[ RECORD 5 ]-----------------------------------------------------------------------------
lsn  | 0/158FCC48
xid  | 766
data | COMMIT 766

Теперь модуль вывода получил изменения. Мы видим, что подписчику передаются публикуемые столбцы. При изменении логического идентификатора также передается его старое значение. Удалим тестовый слот, иначе он будет препятствовать удалению сегментов WAL.

α=> SELECT pg_drop_replication_slot('test_slot');
 pg_drop_replication_slot
--------------------------
(1 row)
α=> \q

Page 22: Большие транзакции

Source slide page 22: Большие транзакции{#source-figure-022 caption=“Source slide page 22: Большие транзакции”}

Комментарии преподавателя

По умолчанию (параметр подписки streaming = off) изменения накапливаются в переупорядочивающем буфере и отправляются подписчику после фиксации транзакции.

Если транзакция изменяет много данных, могут возникать проблемы:

● расход памяти wal sender на переупорядочивающий буфер;

● запись файлов при превышении памяти, выделенной под переупорядочивающий буфер (logical_decoding_work_mem);

● отставание реплики. Если задать streaming = on, публикующий сервер будет передавать изменяемые данные подписчику во время работы транзакции. Когда общий объем изменений превышает значение параметра logical_decoding_work_mem, накопленные изменения самой большой по объему транзакции не сбрасываются в файл, а сразу передаются в модуль вывода. При этом подписчик сохраняет такие изменения в своих временных файлах и применяет при фиксации транзакции.

Начиная с PostgreSQL 16, если задано streaming = parallel и есть доступные рабочие процессы, изменения на подписчике не сохраняются, а сразу же применяются дополнительными процессами. Число таких процессов для одной подписки ограничено параметром max_parallel_apply_workers_per_subscription.

https://postgrespro.ru/docs/postgresql/16/sql-createsubscription#SQL-CREA TESUBSCRIPTION-WITH-STREAMING

https://postgrespro.ru/docs/postgresql/16/runtime-config-replication#GUC-M AX-PARALLEL-APPLY-WORKERS-PER-SUBSCRIPTION

Page 23: Конфликты

Приводят к ошибке

  • нарушение ограничений целостности,
  • недостаточные права на целевую таблицу или политика защиты строк
  • репликация выдает ошибку,
  • подписка с параметром disable_on_error отключается
  • можно вручную исправить данные на стороне подписки
  • или пропустить конфликтующую транзакцию
  • Игнорируются
    • обновляемая или удаляемая строка отсутствует на подписчике
    • репликация продолжается, но согласованность может быть нарушена

Комментарии преподавателя

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

Часто конфликтом называют ситуацию, когда операция не может быть выполнена, обычно из-за нарушения ограничения целостности, или отсутствия прав у владельца подписки на целевую таблицу, или ограничений на уровне строк (RLS, row level security).

В этом случае применение операции завершается ошибкой. Подписчик периодически повторяет попытки, но до разрешения конфликта они будут неудачными. Конфликты можно разрешать только вручную, автоматическое разрешение пока не реализовано. Есть два варианта: исправить данные на подписчике или пропустить конфликтующую транзакцию. Во втором случае нужно отключить репликацию, вызвать функцию pg_replication_origin_advance или выполнить команду ALTER SUBSCRIPTION … SKIP …, и снова включить репликацию. Можно также заранее настроить автоматическое отключение репликации, указав параметр подписки disable_on_error.

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

https://postgrespro.ru/docs/postgresql/16/logical-replication-conflicts

Page 24: Конфликты

Source slide page 24: Конфликты{#source-figure-024 caption=“Source slide page 24: Конфликты”}

Заметим, что последовательности не реплицируются. На втором сервере создалась своя собственная последовательность:

β=> INSERT INTO test(descr) VALUES ('Шесть — локально');
ERROR:  duplicate key value violates unique constraint "test_pkey"
DETAIL:  Key (id)=(1) already exists.

А вот так получится:

β=> INSERT INTO test VALUES (6, 'Шесть — локально');
INSERT 0 1

Что произойдет, если значение с таким же ключом (6) появится на публикующем сервере?

α=> INSERT INTO test(descr) VALUES ('Шесть');
INSERT 0 1
α=> INSERT INTO test(descr) VALUES ('Семь');
INSERT 0 1

При репликации возникнет конфликт, и она будет приостановлена.

β=> SELECT * FROM test ORDER BY id;
 id |      descr       | cluster_name | additional
----+------------------+--------------+------------
  1 | Раз              | alpha        |
  6 | Шесть — локально | beta         |
(2 rows)

Фактически, процесс logical replication apply worker будет периодически перезапускаться, проверяя, не устранен ли конфликт. Поэтому информация в pg_stat_subscription пропадает:

β=> SELECT * FROM pg_stat_subscription \gx
-[ RECORD 1 ]---------+---------
subid                 | 16419
subname               | test_sub
pid                   |
leader_pid            |
relid                 |
received_lsn          |
last_msg_send_time    |
last_msg_receipt_time |
latest_end_lsn        |
latest_end_time       |

В журнал сообщений будут попадать записи о нарушении ограничений целостности:

student$ sudo tail -n 3 /var/log/postgresql/postgresql-16-beta.log
2026-04-10 18:09:54.050 MSK [81032] DETAIL:  Key (id)=(6) already exists.
2026-04-10 18:09:54.050 MSK [81032] CONTEXT:  processing remote data for replication
origin "pg_16419" during message type "INSERT" for replication target relation
"public.test" in transaction 767, finished at 0/15918120
2026-04-10 18:09:54.064 MSK [77670] LOG:  background worker "logical replication worker"
(PID 81032) exited with exit code 1

А в таблице pg_stat_subscription_stats при каждой очередной попытке выполнить операцию будет расти счетчик ошибок:

β=> SELECT * FROM pg_stat_subscription_stats;
 subid | subname  | apply_error_count | sync_error_count | stats_reset
-------+----------+-------------------+------------------+-------------
 16419 | test_sub |                 2 |                0 |
(1 row)

(В PostgreSQL 18 в эту таблицу добавлены отдельные счетчики для каждого типа конфликта.) Чтобы разрешить конфликт, удалим конфликтующую строку из таблицы и немного подождем:

β=> DELETE FROM test WHERE id = 6;

Page 25: Page 25

DELETE 1

Проверим:

β=> SELECT * FROM test ORDER BY id;
 id | descr | cluster_name | additional
----+-------+--------------+------------
  1 | Раз   | alpha        |
  6 | Шесть | alpha        |
  7 | Семь  | alpha        |
(3 rows)

Данные появились, репликация восстановлена.

Пропуск конфликтующей транзакции

Можно разрешить конфликт по-другому: пропустить транзакцию, приводящую к конфликту. Конфликтующая транзакция:

β=> INSERT INTO test(id,descr) VALUES (8,'Восемь — локально');
INSERT 0 1
α=> INSERT INTO test(descr) VALUES ('Восемь');
INSERT 0 1

Перед пропуском транзакции подписка должна быть отключена:

β=> ALTER SUBSCRIPTION test_sub DISABLE;
ALTER SUBSCRIPTION

Вместо отключения вручную можно было установить параметр подписки disable_on_error. Подписка остановлена:

β=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | f       | {test_pub}
(1 row)

Посмотрим в журнал сообщений:

β=> \! tail -4 /var/log/postgresql/postgresql-16-beta.log
2026-04-10 18:09:59.698 MSK [81497] ERROR:  duplicate key value violates unique
constraint "test_pkey"
2026-04-10 18:09:59.698 MSK [81497] DETAIL:  Key (id)=(8) already exists.
2026-04-10 18:09:59.698 MSK [81497] CONTEXT:  processing remote data for replication
origin "pg_16419" during message type "INSERT" for replication target relation
"public.test" in transaction 770, finished at 0/159D17D0
2026-04-10 18:09:59.708 MSK [77670] LOG:  background worker "logical replication worker"
(PID 81497) exited with exit code 1

Сообщение в журнале содержит LSN записи о фиксации конфликтующей транзакции. Транзакцию можно пропустить командой:

β=> ALTER SUBSCRIPTION test_sub SKIP (lsn = '0/159D17D0');
ALTER SUBSCRIPTION

Вместо предыдущей команды суперпользователь мог бы использовать функцию pg_replication_origin_advance. Включим репликацию.

β=> ALTER SUBSCRIPTION test_sub ENABLE;
ALTER SUBSCRIPTION
β=> SELECT * FROM pg_stat_subscription \gx

Page 26: Page 26

-[ RECORD 1 ]---------+------------------------------
subid                 | 16419
subname               | test_sub
pid                   | 81976
leader_pid            |
relid                 |
received_lsn          | 0/15A61060
last_msg_send_time    | 2026-04-10 18:10:04.279423+03
last_msg_receipt_time | 2026-04-10 18:10:04.279523+03
latest_end_lsn        | 0/15A61060
latest_end_time       | 2026-04-10 18:10:04.279423+03

Репликация работает, но так как мы пропустили транзакцию, данные рассогласованы:

α=> SELECT * FROM test ORDER BY id;
 id | descr  | cluster_name
----+--------+--------------
  1 | Раз    | alpha
  6 | Шесть  | alpha
  7 | Семь   | alpha
  8 | Восемь | alpha
(4 rows)
β=> SELECT * FROM test ORDER BY id;
 id |       descr       | cluster_name | additional
----+-------------------+--------------+------------
  1 | Раз               | alpha        |
  6 | Шесть             | alpha        |
  7 | Семь              | alpha        |
  8 | Восемь — локально | beta         |
(4 rows)

Опустошим таблицу:

α=> TRUNCATE test;
TRUNCATE TABLE

Триггеры на подписчике

На подписчике могут выполняться триггеры; по умолчанию триггер будет срабатывать при локальных изменениях, но не при репликации. Это удобно, если на обоих серверах созданы одинаковые таблицы с одинаковым набором триггеров: в таком случае триггер уже отработал на публикующем сервере, его не надо выполнять на подписчике. Попробуем.

β=> CREATE OR REPLACE FUNCTION change_descr() RETURNS trigger AS $$
BEGIN
  NEW.additional := 'из публикации';
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
β=> CREATE TRIGGER test_before_row
BEFORE INSERT OR UPDATE ON test
FOR EACH ROW
EXECUTE FUNCTION change_descr();
CREATE TRIGGER
α=> INSERT INTO test(descr) VALUES ('Девять');
INSERT 0 1
β=> SELECT * FROM test ORDER BY id;
 id | descr  | cluster_name | additional
----+--------+--------------+------------
  9 | Девять | alpha        |
(1 row)

Триггер не сработал. Можно изменить таблицу, чтобы триггер срабатывал только при репликации:

Page 27: Page 27

Source slide page 27: Page 27{#source-figure-027 caption=“Source slide page 27: Page 27”}

β=> ALTER TABLE test ENABLE REPLICA TRIGGER test_before_row;
ALTER TABLE

Или в обоих случаях: и при репликации, и при локальных изменениях.

β=> ALTER TABLE test ENABLE ALWAYS TRIGGER test_before_row;
ALTER TABLE

Различить эти ситуации можно с помощью параметра session_replication_role:

β=> CREATE OR REPLACE FUNCTION change_descr() RETURNS trigger AS $$
BEGIN
  NEW.additional := current_setting('session_replication_role');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Добавляем по одной строке на каждом сервере:

α=> INSERT INTO test(descr) VALUES ('Десять');
INSERT 0 1
β=> INSERT INTO test(id, descr) VALUES (11, 'Одиннадцать');
INSERT 0 1
β=> SELECT * FROM test ORDER BY id;
 id |    descr    | cluster_name | additional
----+-------------+--------------+------------
  9 | Девять      | alpha        |
 10 | Десять      | alpha        | replica
 11 | Одиннадцать | beta         | origin
(3 rows)

Триггер сработал в обоих случаях.

Page 28: Источник репликации

Source slide page 28: Источник репликации{#source-figure-028 caption=“Source slide page 28: Источник репликации”}

Параметр подписки origin

  • any — передавать все изменения (по умолчанию)
  • none — передавать только локальные изменения

Комментарии преподавателя

Если определить на одном сервере и публикацию, и подписку, и передавать все изменения, то реплицированные изменения будут реплицироваться и возникнет «снежный ком».

Начиная с PostgreSQL 16, публикующий сервер в начале транзакции, которая была получена с другого узла, передает по протоколу сообщение об источнике этой транзакции. Это также важно при каскадной репликации.

https://postgrespro.ru/docs/postgresql/16/protocol-logical-replication#PROT OCOL-LOGICAL-MESSAGES-FLOW

По умолчанию подписка имеет параметр origin = any и публикация передает ей все транзакции: и локальные, и полученные с другого узла.

В топологиях, где могут появиться циклы, например, при двунаправленной репликации, следует устанавливать origin = none. Тогда публикация будет передавать только локальные изменения.

https://postgrespro.ru/docs/postgresql/16/sql-createsubscription#SQL-CREA TESUBSCRIPTION-WITH-ORIGIN

Информацию об источниках можно увидеть в pg_replication_origin.

https://postgrespro.ru/docs/postgresql/16/catalog-pg-replication-origin#CATA LOG-PG-REPLICATION-ORIGIN

Текущая позиция воспроизведения для конкретного источника репликации выводится в представлении pg_replication_origin_status.

https://postgrespro.ru/docs/postgresql/16/view-pg-replication-origin-status#V IEW-PG-REPLICATION-ORIGIN-STATUS

Page 29: Источник репликации

Удалим строки таблицы:

α=> TRUNCATE test;
TRUNCATE TABLE

Опубликуем несколько столбцов таблицы test на сервере beta.

β=> CREATE PUBLICATION test_pub FOR TABLE test(id, descr, cluster_name);
CREATE PUBLICATION

Теперь публикации есть на обоих серверах. Если сейчас на alpha создать подписку, она будет принимать от beta все изменения, в том числе и те, которые были отправлены публикацией сервера alpha. Применить такие изменения alpha не сможет из-за нарушения ограничения целостности, репликация остановится. Чтобы beta получала только изменения, сделанные локально на alpha, зададим параметр подписки origin:

β=> ALTER SUBSCRIPTION test_sub SET (origin = none);
ALTER SUBSCRIPTION

Для тех изменений, которые произошли в результате репликации, источник будет непуст, и публикация не отправит их подписке. Пусть и alpha станет подписчиком beta. Начальную синхронизацию отключим, поскольку таблица пуста. Также используем параметр подписки origin, чтобы получать только изменения, сделанные на beta:

α=> CREATE SUBSCRIPTION test_sub
CONNECTION 'host=localhost port=5433 user=student password=student dbname=replica_logical'
PUBLICATION test_pub WITH (copy_data = false, origin = none);
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION

Подписки теперь есть на обоих серверах.

α=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)
β=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)

Вставим строку на сервере alpha.

α=> INSERT INTO test VALUES (1, 'Эх, раз...');
INSERT 0 1

Проверяем на beta.

β=> SELECT * FROM test;
 id |   descr    | cluster_name | additional
----+------------+--------------+------------
  1 | Эх, раз... | alpha        | replica
(1 row)

Вставляем строку на сервере beta.

β=> INSERT INTO test VALUES (2, 'Да еще раз...');
INSERT 0 1

Проверяем на alpha.

Page 30: Page 30

α=> SELECT * FROM test;
 id |     descr     | cluster_name
----+---------------+--------------
  1 | Эх, раз...    | alpha
  2 | Да еще раз... | beta
(2 rows)

Проверим изменение данных.

α=> UPDATE test SET descr = 'Еще много, много раз!';
UPDATE 2
α=> SELECT * FROM test;
 id |         descr         | cluster_name
----+-----------------------+--------------
  1 | Еще много, много раз! | alpha
  2 | Еще много, много раз! | beta
(2 rows)
β=> SELECT * FROM test;
 id |         descr         | cluster_name | additional
----+-----------------------+--------------+------------
  1 | Еще много, много раз! | alpha        | replica
  2 | Еще много, много раз! | beta         | replica
(2 rows)

Если репликация больше не нужна, надо удалить подписки — иначе на публикующем сервере останутся открытыми репликационные слоты.

α=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION
β=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION

Page 31: Итоги

Логическая репликация использует модель «публикация—подписка»

  • Передаются изменения табличных строк
  • Строки сопоставляются по логическим идентификаторам
  • Несовпадение данных на серверах может приводить к конфликтам
  • Передача информации об источнике позволяет отслеживать ход каскадной репликации и исключить зацикливание

Page 32: Практика

  1. Создайте две базы данных на одном сервере.
  • В первой базе данных создайте таблицу с первичным ключом и добавьте в нее несколько строк. Настройте логическую репликацию таблицы из первой базы во вторую, проверьте работу репликации и удалите подписку. 2. Настройте логическую репликацию таблицы с сервера alpha на сервер beta. Выполните транзакцию, изменяющую значительный объем данных; перед ее фиксацией убедитесь, что данные помещены на диск. Повторите то же самое, но в потоковом режиме. Сохранялись ли данные на диске?

Комментарии преподавателя

  1. Воспользуйтесь утилитой pg_dump с ключом –schema-only.

Если попробовать выполнить это обычным образом, команда создания подписки «повиснет» из-за того, что она должна дождаться завершения активных транзакций на публикующем сервере, то есть и самой себя в том числе. В таком случае необходимо заранее создать слот логической репликации, как описано в документации: https://postgrespro.ru/docs/postgresql/16/sql-createsubscription

  1. Чтобы публикация не попала в логическую копию, используйте ключ –no-publications.

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

Page 33: 1. Логическая репликация на одном сервере

Сначала установим уровень журнала logical.

α=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Базы данных:

student$ psql
α=> CREATE DATABASE replica_logical;
CREATE DATABASE
α=> CREATE DATABASE replica_logical_bis;
CREATE DATABASE

Таблица в первой базе:

α=> \c replica_logical
You are now connected to database "replica_logical" as user "student".
α=> CREATE TABLE test(
  id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text
);
CREATE TABLE
α=> INSERT INTO test(descr) VALUES ('Раз'), ('Два'), ('Три');
INSERT 0 3

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

student$ pg_dump --no-acl --schema-only replica_logical | psql -d replica_logical_bis
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE

Переносить данные с помощью pg_dump не имеет смысла, поскольку в процессе переноса таблицы могут измениться.

student$ psql -d replica_logical_bis
α=> \d test
                            Table "public.test"
 Column |  Type   | Collation | Nullable |             Default
--------+---------+-----------+----------+----------------------------------
 id     | integer |           | not null | generated by default as identity
 descr  | text    |           |          |
Indexes:
    "test_pkey" PRIMARY KEY, btree (id)

Публикация:

Page 34: Page 34

α=> CREATE PUBLICATION test_pub FOR TABLE test;
CREATE PUBLICATION

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

α=> SELECT pg_create_logical_replication_slot('testslot','pgoutput');
 pg_create_logical_replication_slot
------------------------------------
 (testslot,0/23EF3B8)
(1 row)

И затем создаем подписку:

α=> CREATE SUBSCRIPTION test_sub
CONNECTION 'user=student password=student dbname=replica_logical host=localhost'
PUBLICATION test_pub
WITH (create_slot = false, slot_name = testslot);
CREATE SUBSCRIPTION

Проверим работу репликации:

α=> INSERT INTO test(descr) VALUES ('Четыре');
INSERT 0 1
α=> SELECT * FROM test;
 id | descr
----+--------
  1 | Раз
  2 | Два
  3 | Три
  4 | Четыре
(4 rows)

Удалим подписку и слот.

α=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "testslot" on publisher
DROP SUBSCRIPTION
α=> \q

2. Логическая репликация с сервера alpha на сервер beta

Запустим сервер beta и скопируем команды DDL для таблицы test. Команды создания публикации на подписчике не нужны.

student$ sudo pg_ctlcluster 16 beta start
student$ pg_dump \
  --create \
  --schema-only \
  --no-publications \
  --no-acl \
  --dbname=replica_logical \
  | psql -p 5433
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "replica_logical" as user "student".

Page 35: Page 35

SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE

Создадим подписку на сервере beta, слот на alpha создается автоматически.

student$ psql -p 5433 -d replica_logical
β=> CREATE SUBSCRIPTION test_sub
CONNECTION 'user=student password=student dbname=replica_logical host=localhost'
PUBLICATION test_pub;
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION
β=> SELECT count(*) FROM test;
 count
-------
     4
(1 row)

Репликация работает. Опустошим таблицу test на сервере публикации (alpha):

α=> TRUNCATE test;
TRUNCATE TABLE
β=> SELECT count(*) FROM test;
 count
-------
     0
(1 row)

Подписчик получил изменение. Начнем транзакцию, в которой вставим в таблицу значительное число строк.

α=> BEGIN;
BEGIN
α=> INSERT INTO test(descr)
SELECT now()::text FROM generate_series(1,1_000_000) AS g(id);
INSERT 0 1000000

Параметр logical_decoding_work_mem определяет максимальный размер переупорядочивающего буфера.

α=> SHOW logical_decoding_work_mem;
 logical_decoding_work_mem
---------------------------
 64MB
(1 row)

При превышении этого ограничения данные помещаются на диск в каталог PGDATA/pg_replslot/имя-слота. Сколько места на диске занимает каталог?

postgres$ du -sh /var/lib/postgresql/16/alpha/pg_replslot/test_sub

Page 36: Page 36

135M
/var/lib/postgresql/16/alpha/pg_replslot/test_sub

По умолчанию, пока транзакция не зафиксирована, никакие данные подписке не передаются.

β=> SELECT count(*) FROM test;
 count
-------
     0
(1 row)

Зафиксируем транзакцию…

α=> COMMIT;
COMMIT

…и немного подождем: Подписчик получил все строки:

β=> SELECT count(*) FROM test;
  count
---------
 1000000
(1 row)

А сохраненные на диске данные удалены:

postgres$ du -sh /var/lib/postgresql/16/alpha/pg_replslot/test_sub
8,0K
/var/lib/postgresql/16/alpha/pg_replslot/test_sub

Заглянем в представление pg_stat_replication_slots:

α=> SELECT * FROM pg_stat_replication_slots \gx
-[ RECORD 1 ]+----------
slot_name    | test_sub
spill_txns   | 1
spill_count  | 3
spill_bytes  | 162000000
stream_txns  | 0
stream_count | 0
stream_bytes | 0
total_txns   | 2
total_bytes  | 162001984
stats_reset  |

spill_txns — количество вытесненных на диск транзакций; spill_count — сколько раз данные вытеснялись на диск из-за превышения logical_decoding_work_mem; spill_bytes — сколько байт было вытеснено на диск. Повторим эксперимент в потоковом режиме. Сбросим статистику и снова опустошим таблицу.

α=> SELECT pg_stat_reset_replication_slot('test_sub');
 pg_stat_reset_replication_slot
--------------------------------
(1 row)
α=> TRUNCATE test;
TRUNCATE TABLE

Теперь зададим для подписки потоковый режим:

β=> ALTER SUBSCRIPTION test_sub SET (streaming = on);
ALTER SUBSCRIPTION

И снова запустим большую транзакцию.

α=> BEGIN;
BEGIN

Page 37: Page 37

α=> INSERT INTO test(descr)
SELECT now()::text FROM generate_series(1,1_000_000) AS g(id);
INSERT 0 1000000

Теперь данных в каталоге слота гораздо меньше, часть их уже отправлена подписчику:

postgres$ du -sh /var/lib/postgresql/16/alpha/pg_replslot/test_sub
8,0K
/var/lib/postgresql/16/alpha/pg_replslot/test_sub

Зафиксируем транзакцию…

α=> COMMIT;
COMMIT

…и немного подождем:

β=> SELECT count(*) FROM test;
  count
---------
 1000000
(1 row)

Заглянем в представление pg_stat_replication_slots:

α=> SELECT * FROM pg_stat_replication_slots \gx
-[ RECORD 1 ]+------------------------------
slot_name    | test_sub
spill_txns   | 0
spill_count  | 0
spill_bytes  | 0
stream_txns  | 1
stream_count | 3
stream_bytes | 161000000
total_txns   | 2
total_bytes  | 161001984
stats_reset  | 2026-04-10 18:23:59.806794+03

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

Уборка

Удалим подписку, слот и базы данных, сбросим параметры конфигурации.

β=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION
β=> \c postgres
You are now connected to database "postgres" as user "student".
β=> DROP DATABASE replica_logical;
DROP DATABASE
student$ sudo pg_ctlcluster 16 beta stop
α=> \c postgres
You are now connected to database "postgres" as user "student".
α=> DROP DATABASE replica_logical;
DROP DATABASE
α=> DROP DATABASE replica_logical_bis;
DROP DATABASE
α=> ALTER SYSTEM RESET wal_level;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Page 38: Page 38

Page 39: Практика+

  1. Создайте и опубликуйте на сервере alpha таблицу со столбцом типа bytea. Вставьте в таблицу несколько миллионов строк. Сравните время начальной синхронизации для подписки в обычном и двоичном режимах, используя для этого сервер beta. 2. Включите для подписки параллельную синхронизацию. Добавляя в таблицу строки, изучите процессы, обслуживающие подписку.

Комментарии преподавателя

  1. Оценить время начальной синхронизации можно с помощью мониторинга таблицы pg_subscription_rel. Его можно организовать на стороне клиента с помощью метакоманды \watch или на стороне сервера с помощью функции, написанной, например, на PL/pgSQL.

https://postgrespro.ru/docs/postgresql/16/catalog-pg-subscription-rel#CATAL OG-PG-SUBSCRIPTION-REL

https://postgrespro.ru/docs/postgresql/16/plpgsql

  1. Задайте подходящее значение параметра подписки streaming. Для мониторинга процессов подписки используйте представление pg_stat_subscription.

Page 40: 1. Начальная синхронизация данных при логической репликации

Создадим базу данных.

α=> CREATE DATABASE replica_logical;
CREATE DATABASE
α=> \c replica_logical
You are now connected to database "replica_logical" as user "student".

Изменим уровень журнала WAL и перезапустим сервер.

α=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart
student$ psql -d replica_logical

Создадим таблицу с большим количеством строк.

α=> CREATE TABLE t AS
SELECT n::text::bytea
FROM generate_series(1, 5_000_000) AS g(n);
SELECT 5000000

Запустим сервер beta и скопируем на него базу со структурой таблиц, но без данных.

student$ sudo pg_ctlcluster 16 beta start
student$ pg_dump -d replica_logical --schema-only --create --no-acl | psql -p 5433
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "replica_logical" as user "student".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
student$ psql -p 5433 -d replica_logical

Создадим публикацию на сервере alpha.

α=> CREATE PUBLICATION pub FOR TABLE t;
CREATE PUBLICATION

Page 41: Page 41

Source slide page 41: Page 41{#source-figure-041 caption=“Source slide page 41: Page 41”}

Подписка на beta исходно будет неактивна.

β=> CREATE SUBSCRIPTION sub
CONNECTION 'dbname=replica_logical host=localhost port=5432 user=postgres password=postgres'
PUBLICATION pub
WITH (enabled = false);
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION

Состояние таблицы в подписке можно увидеть в представлении:

β=> SELECT srsubid, srrelid::regclass, srsubstate, srsublsn
FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate | srsublsn
---------+---------+------------+----------
   16397 | t       | i          |
(1 row)

Значения кодов состояния в поле srsubstate: i — инициализация; d — копирование данных; f — копирование таблицы завершено; s — синхронизация выполнена; r — нормальная репликация. Время синхронизации будем измерять с помощью функцию на PL/pgSQL:

β=> CREATE OR REPLACE FUNCTION time4sync() RETURNS interval AS
$$
DECLARE
  st timestamp := clock_timestamp();
BEGIN
  WHILE EXISTS(SELECT 1 FROM pg_subscription_rel WHERE srsubstate <> 'r') LOOP
    PERFORM pg_sleep(0.01); -- достаточная точность
  END LOOP;
  RETURN clock_timestamp() - st;
END;
$$
LANGUAGE plpgsql;
CREATE FUNCTION

Активируем подписку и измерим время начальной синхронизации.

β=> ALTER SUBSCRIPTION sub ENABLE; SELECT time4sync();
ALTER SUBSCRIPTION
    time4sync
-----------------
 00:00:08.664681
(1 row)

Теперь настроим синхронизацию в двоичном режиме. Удалим подписку, опустошим таблицу и создадим новую подписку, указав параметр binary.

β=> DROP SUBSCRIPTION sub;
NOTICE:  dropped replication slot "sub" on publisher
DROP SUBSCRIPTION
β=> TRUNCATE TABLE t;
TRUNCATE TABLE
β=> CREATE SUBSCRIPTION sub
CONNECTION 'dbname=replica_logical host=localhost port=5432 user=postgres password=postgres'
PUBLICATION pub
WITH (enabled = false, binary = true);
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION

Включаем подписку и измеряем время синхронизации.

β=> ALTER SUBSCRIPTION sub ENABLE; SELECT time4sync();

Page 42: Page 42

ALTER SUBSCRIPTION
    time4sync
-----------------
 00:00:06.661815
(1 row)

Видим, что начальная синхронизация заметно ускорилась. Параметр binary действует и на последующую синхронизацию. Однако двоичный режим возможен не для всех типов данных; нужно также учитывать переносимость между разными архитектурами и основными версиями PostgreSQL. Двоичный режим реализован в версии 14, а в версиях до 16 начальная синхронизация всегда выполняется в текстовом режиме.

2. Параллельное применение изменений

Зададим для подписки параллельный режим.

β=> ALTER SUBSCRIPTION sub SET (streaming = parallel);
ALTER SUBSCRIPTION

Процессы, обслуживающие подписку:

β=> SELECT
  a.pid, a.leader_pid, a.backend_type,
  r.srsubstate, s.received_lsn
FROM pg_stat_subscription s
  LEFT JOIN pg_subscription_rel r ON r.srsubid=s.subid AND r.srrelid='t'::regclass
  LEFT JOIN pg_stat_activity a ON s.pid=a.pid
WHERE s.subname = 'sub';
  pid   | leader_pid |        backend_type        | srsubstate | received_lsn
--------+------------+----------------------------+------------+--------------
 107953 |            | logical replication worker | r          | 0/15368FA8
(1 row)

Опустошим таблицу, вставим в нее большое число строк и повторим запрос к представлениям системного каталога:

α=> TRUNCATE t;
TRUNCATE TABLE
α=> INSERT INTO t SELECT g.i::text::bytea FROM generate_series(1, 1_000_000) AS g(i);
β=> SELECT
  a.pid, a.leader_pid, a.backend_type,
  r.srsubstate, s.received_lsn
FROM pg_stat_subscription s
  LEFT JOIN pg_subscription_rel r ON r.srsubid=s.subid AND r.srrelid='t'::regclass
  LEFT JOIN pg_stat_activity a ON s.pid=a.pid
WHERE s.subname = 'sub';
  pid   | leader_pid |            backend_type             | srsubstate | received_lsn
--------+------------+-------------------------------------+------------+--------------
 107953 |            | logical replication worker          | r          | 0/16FFFFE0
 108149 |     107953 | logical replication parallel worker | r          |
(2 rows)

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

INSERT 0 1000000

Вставка строк окончена. Подождем еще немного…

β=> select count(*) from t;
  count
---------
 1000000
(1 row)

Теперь подписчик получил все строки.

Уборка

Page 43: Page 43

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

β=> DROP SUBSCRIPTION sub;
NOTICE:  dropped replication slot "sub" on publisher
DROP SUBSCRIPTION

Удалим базы данных.

β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> DROP DATABASE replica_logical;
DROP DATABASE

Сбросим изменения в конфигурации.

α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> ALTER SYSTEM RESET synchronous_standby_names;
ALTER SYSTEM
α=> ALTER SYSTEM RESET wal_level;
ALTER SYSTEM

Перезагрузим сервер alpha.

student$ sudo pg_ctlcluster 16 alpha restart
student$ psql
α=> DROP DATABASE replica_logical;
DROP DATABASE

Остановим сервер beta.

student$ sudo pg_ctlcluster 16 beta stop

07 Занятие 07: Репликация / Сценарии использования

Исходная страница в нативном Markdown: Репликация / Сценарии использования
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_07_replica_usecases.html Archive: DBA3-handouts-16.zip Страниц PDF: 33

Page 1: Репликация / Сценарии использования

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Использование физической репликации
  • Использование логической репликации

Page 3: Физическая репликация

Горячий резерв для высокой доступности

  • Балансировка OLTP-нагрузки
  • Реплика для отчетов
  • Несколько реплик и каскадная репликация
  • Отложенная репликация

Page 4: Горячий резерв для HA

Source slide page 4: Горячий резерв для HA{#source-figure-004 caption=“Source slide page 4: Горячий резерв для HA”}

синхронная репликация: реплика максимально соответствует мастеру запросы к реплике возможны, но не приоритетны: они будут прерваны при любом конфликте

Комментарии преподавателя

Реплика может создаваться для решения разных задач, и настройка зависит от ее предназначения.

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

Надежность обеспечивает синхронная репликация в режиме synchronous_commit = on (значение write не гарантирует надежность; значение apply может привести к неоправданным задержкам).

Для синхронной репликации значение параметра synchronous_standby_names должно содержать список резервных серверов (реплик).

Чтобы реплика как можно меньше отставала от основного сервера, нужно применять журнальные записи сразу же. Для этого выставляем задержку max_standby_streaming_delay в небольшое значение.

Чтобы запросы к реплике не могли негативно повлиять на основной сервер, выключаем обратную связь (hot_standby_feedback = off).

С такими настройками запросы к реплике возможны, но в случае конфликтов они будут прерваны. Если запросы к реплике не предполагаются, она может работать в режиме «теплого резерва» (hot_standby = off).

Заметим, что репликация — базовый механизм для обеспечения высокой доступности, но далеко не все, что нужно. Более подробнее это обсуждается в модуле «Кластерные технологии».

Page 5: Балансировка OLTP

Source slide page 5: Балансировка OLTP{#source-figure-005 caption=“Source slide page 5: Балансировка OLTP”}

запросы на реплике должны отрабатывать долгие запросы повлияют на мастер, но в OLTP-нагрузке их не должно быть согласованность данных должна обеспечиваться отдельно

Комментарии преподавателя

Другой вариант — реплика используется для балансировки OLTP-нагрузки на чтение.

OLTP-нагрузка характеризуется небольшими по длительности запросами. Это позволяет достаточно тесно связать реплику с основным сервером, чтобы запросы гарантированно не прерывались из-за конфликтов, и в то же время не беспокоиться о негативном влиянии на основной сервер.

Включаем обратную связь (hot_standby_feedback = on) и выставляем достаточно большую задержку применения конфликтных записей (max_standby_streaming_delay).

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

Для обеспечения согласованности нужно, чтобы изменения появлялись на обоих серверах одновременно. Режим synchronous_commit = apply позволяет дождаться применения журнальной записи на реплике (ценой сильного падения производительности основного сервера), но тогда можно увидеть данные на реплике раньше, чем на мастере. Надежное и прозрачное для приложений решение можно получить, используя специальные протоколы распределенных транзакций (см. модуль «Кластерные технологии»).

Page 6: Реплика для отчетов

Source slide page 6: Реплика для отчетов{#source-figure-006 caption=“Source slide page 6: Реплика для отчетов”}

запросы на реплике должны отрабатывать, в том числе и долгие (но не требуются самые актуальные данные) мастер не должен испытывать негативного влияния обратной связи чтобы совсем развязать серверы — убрать слот и использовать архив WAL

Комментарии преподавателя

Еще один возможный вариант — разделение по серверам нагрузки разного типа. Основной сервер может брать на себя OLTP-нагрузку, а длительные читающие запросы (отчеты) можно вынести на реплику.

Кроме распределения нагрузки между серверами, такое решение позволит избежать проблемы с тем, что длительные запросы на основном сервере могут задерживать выполнение очистки и приводить к раздуванию (bloat).

Из этих соображений «отчетная» реплика должна быть максимально отделена от основного сервера. Должна быть отключена обратная связь (hot_standby_feedback = off), а время откладывания конфликтующих журнальных записей нужно сильно увеличить, вплоть до бесконечности (max_standby_streaming_delay = −1).

Поскольку для длительных отчетов не требуются сиюминутные данные, можно, при наличии архива, отказаться от слота репликации (не забыв выставить параметр max_standby_archive_delay аналогично max_standby_streaming_delay). Тогда, если мастер успеет удалить необходимый реплике файл, реплика возьмет его из архива.

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

Разумеется, на практике возможны и другие сочетания параметров. Главное — четко понимать, какую задачу требуется решить, и какое влияние оказывают те или иные параметры.

Page 7: Несколько реплик

Source slide page 7: Несколько реплик{#source-figure-007 caption=“Source slide page 7: Несколько реплик”}

Комментарии преподавателя

К основному серверу можно подключить несколько реплик. Никаких специальных настроек для этого не требуется, но надо учитывать, что каждой реплике будет соответствовать отдельный процесс wal sender и отдельный слот репликации. При развертывании можно растиражировать одну базовую копию и клонировать слот репликации функцией pg_copy_physical_replication_slot.

https://postgrespro.ru/docs/postgresql/16/functions-admin#FUNCTIONS-RE PLICATION

Обычно такая схема требуется для распределения нагрузки, но собственно распределение должно решаться внешними средствами (см. модуль «Кластерные технологии»).

Page 8: Мастер и две физические реплики

Настроим конфигурацию с двумя репликами. Создадим автономную резервную копию первого сервера, одновременно создав слот.

student$ pg_basebackup \
  --checkpoint=fast \
  --pgdata=/home/student/tmp/backup \
  -R \
  --create-slot --slot=beta

Поскольку третий сервер при старте начнет применять записи WAL с той же позиции, что и второй, можно дублировать слот:

α=> SELECT pg_copy_physical_replication_slot('beta','gamma');
 pg_copy_physical_replication_slot
-----------------------------------
 (gamma,)
(1 row)

Выложим автономную копию в каталоги PG_DATA второго и третьего серверов:

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/beta
student$ sudo cp -r /home/student/tmp/backup /var/lib/postgresql/16/beta
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/beta
student$ sudo pg_ctlcluster 16 gamma stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/gamma
student$ sudo cp -r /home/student/tmp/backup /var/lib/postgresql/16/gamma
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/gamma

В конфигурации третьего сервера укажем слот gamma:

postgres$ sed 's/beta/gamma/g' -i /var/lib/postgresql/16/gamma/postgresql.auto.conf
postgres$ tail -n 1 /var/lib/postgresql/16/gamma/postgresql.auto.conf
primary_slot_name = 'gamma'

Запускаем обе реплики:

student$ sudo pg_ctlcluster 16 beta start
student$ sudo pg_ctlcluster 16 gamma start

Слоты инициализировались:

α=> SELECT slot_name, active_pid, restart_lsn FROM pg_replication_slots;
 slot_name | active_pid | restart_lsn
-----------+------------+-------------
 beta      |      83683 | 0/3048438
 gamma     |      83729 | 0/3048438
(2 rows)

Проверяем.

α=> CREATE DATABASE replica_usecases;
CREATE DATABASE
α=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
α=> CREATE TABLE revenue(city text, amount numeric);

Page 9: Page 9

Source slide page 9: Page 9{#source-figure-009 caption=“Source slide page 9: Page 9”}

CREATE TABLE
student$ psql -p 5433
student$ psql -p 5434
β=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
γ=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
β=> \d revenue
              Table "public.revenue"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 city   | text    |           |          |
 amount | numeric |           |          |
γ=> \d revenue
              Table "public.revenue"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 city   | text    |           |          |
 amount | numeric |           |          |

Мы настроили две реплики одного мастера на основе одной базовой копии.

Page 10: Каскадная репликация

Source slide page 10: Каскадная репликация{#source-figure-010 caption=“Source slide page 10: Каскадная репликация”}

позволяет снизить нагрузку на мастер и перераспределить сетевой трафик не поддерживается каскадная синхронная репликация обратная связь поступает от всех реплик

Комментарии преподавателя

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

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

Если совершится переход на реплику (ближайшую к основному серверу), то на ней увеличится номер линии времени. Значение recovery_target_timeline = ’latest’ направит восстановление по этой новой линии (это значение по умолчанию).

Заметим, что каскадная синхронная репликация не поддерживается: основной сервер может быть синхронизирован только с непосредственно подключенной к нему репликой. Поэтому на первой в каскаде реплике нужно установить synchronous_standby_names = ’’ или synchronous_commit = ’local'.

А вот обратная связь поступает основному серверу от всех реплик.

Page 11: Отложенная репликация

Source slide page 11: Отложенная репликация{#source-figure-011 caption=“Source slide page 11: Отложенная репликация”}

«машина времени» и возможность восстановиться на определенный момент в прошлом без архива

Комментарии преподавателя

Задача: иметь возможность просмотреть данные на некоторый момент в прошлом и, при необходимости, восстановить сервер на этот момент.

Восстановление из архива на момент времени (point-in-time recovery) решает эту задачу, но занимает много времени и требует подготовки.

Другое решение — создать реплику, которая применяет журнальные записи с заданной задержкой (recovery_min_apply_delay). При этом требуется синхронизация часов между серверами.

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

Типичный сценарий: на основном сервере происходит какая-либо проблема (допустим, удалены критичные данные). На реплике данные еще есть, поскольку соответствующие записи еще не применены. Дальнейшее проигрывание записей приостанавливается с помощью функции pg_wal_replay_pause, пока идет исследование проблемы. Функцией pg_get_wal_replay_pause_state можно проверить состояние приостановки.

Если принято решение продолжать, функцией pg_wal_replay_resume завершают паузу в проигрывании записей WAL и восстановление продолжается. Если решено вернуться на момент времени до удаления, нужно указать целевую точку восстановления одним из параметров recovery_target_* и перезапустить реплику.

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

Консолидация и общие справочники

  • Обновление основной версии сервера
  • Мастер-мастер
  • Логическое декодирование на физической реплике

Page 13: Консолидация

Source slide page 13: Консолидация{#source-figure-013 caption=“Source slide page 13: Консолидация”}

получение и консолидация данных нескольких филиалов

Комментарии преподавателя

Пусть имеются несколько региональных филиалов, каждый из которых работает на собственном сервере PostgreSQL. Задача состоит в консолидации части данных на центральном сервере.

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

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

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

Page 14: Общие справочники

Source slide page 14: Общие справочники{#source-figure-014 caption=“Source slide page 14: Общие справочники”}

репликация справочников

Комментарии преподавателя

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

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

Page 15: Консолидация с помощью логической репликации

Source slide page 15: Консолидация с помощью логической репликации{#source-figure-015 caption=“Source slide page 15: Консолидация с помощью логической репликации”}

Повысим обе настроенные ранее реплики и настроим консолидацию данных.

student$ sudo pg_ctlcluster 16 beta promote
student$ sudo pg_ctlcluster 16 gamma promote
β=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)
γ=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 f
(1 row)

Для логической репликации нужно повысить уровень WAL (потребуется перезапуск).

β=> ALTER SYSTEM SET wal_level = 'logical';
ALTER SYSTEM
γ=> ALTER SYSTEM SET wal_level = 'logical';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta restart
student$ sudo pg_ctlcluster 16 gamma restart

Удалим на сервере alpha слоты репликации:

α=> SELECT pg_drop_replication_slot('beta');
 pg_drop_replication_slot
--------------------------
(1 row)
α=> SELECT pg_drop_replication_slot('gamma');
 pg_drop_replication_slot
--------------------------
(1 row)

Публикуем таблицу на втором и третьем серверах:

student$ psql -p 5433 -d replica_usecases
β=> CREATE PUBLICATION revenue FOR TABLE revenue;
CREATE PUBLICATION
student$ psql -p 5434 -d replica_usecases
γ=> CREATE PUBLICATION revenue FOR TABLE revenue;
CREATE PUBLICATION

Первый сервер подписывается на обе публикации:

α=> CREATE SUBSCRIPTION msk
CONNECTION 'port=5433 dbname=replica_usecases host=localhost user=student password=student'
PUBLICATION revenue;
NOTICE:  created replication slot "msk" on publisher
CREATE SUBSCRIPTION
α=> CREATE SUBSCRIPTION spb
CONNECTION 'port=5434 dbname=replica_usecases host=localhost user=student password=student'
PUBLICATION revenue;

Page 16: Page 16

NOTICE:  created replication slot "spb" on publisher
CREATE SUBSCRIPTION

В филиалах кипит работа:

β=> INSERT INTO revenue
  SELECT 'Москва', round(random()*1_000_000)
  FROM generate_series(1,70);
INSERT 0 70
γ=> INSERT INTO revenue
  SELECT 'Санкт-Петербург', round(random()*1_000_000)
  FROM generate_series(1,10);
INSERT 0 10

А центральный офис видит работу всей компании (подождем несколько секунд, чтобы сработала репликация):

α=> SELECT city, sum(amount) FROM revenue GROUP BY city;
      city       |   sum
-----------------+----------
 Москва          | 41430874
 Санкт-Петербург |  4127614
(2 rows)

Page 17: Обновление серверов

Source slide page 17: Обновление серверов{#source-figure-017 caption=“Source slide page 17: Обновление серверов”}

мастер и физическая реплика: требуется обновить основную версию

Комментарии преподавателя

Логическая репликация может использоваться для обновления основной версии сервера без прерывания обслуживания (или с минимальным прерыванием).

Допустим, имеется основной сервер и физическая потоковая реплика.

Page 18: Обновление серверов

Source slide page 18: Обновление серверов{#source-figure-018 caption=“Source slide page 18: Обновление серверов”}

Комментарии преподавателя

Создаем новый сервер с требуемой версией PostgreSQL и переносим на него структуру всех таблиц выбранной базы данных.

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

Page 19: Обновление серверов

Source slide page 19: Обновление серверов{#source-figure-019 caption=“Source slide page 19: Обновление серверов”}

Комментарии преподавателя

Создаем физическую реплику, аналогичную реплике сервера предыдущей версии.

В итоге получаем параллельную структуру серверов с новой версией PostgreSQL. Эта структура аналогична имеющейся для старой версии.

Page 20: Обновление серверов

Source slide page 20: Обновление серверов{#source-figure-020 caption=“Source slide page 20: Обновление серверов”}

Комментарии преподавателя

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

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

Page 21: Мастер-мастер

Source slide page 21: Мастер-мастер{#source-figure-021 caption=“Source slide page 21: Мастер-мастер”}

кластер, в котором данные могут изменять несколько серверов

Комментарии преподавателя

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

Для решения можно использовать двунаправленную репликацию, передавая изменения в одних и тех же таблицах от одного сервера к другому и обратно (устанавливая ограничение на источник репликации, как было рассмотрено в теме «Логическая репликация»).

Конечно, прикладная система должна быть построена таким образом, чтобы избегать конфликтов при изменении данных в одних и тех же таблицах. Например, использовать глобальные уникальные идентификаторы или гарантировать, что разные серверы работают с разными диапазонами ключей. Также можно использовать фильтрацию строк таблиц, появившуюся в PostgreSQL 15.

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

Page 22: Декодирование на реплике

Source slide page 22: Декодирование на реплике{#source-figure-022 caption=“Source slide page 22: Декодирование на реплике”}

логический слот можно разместить на физической реплике при создании логического слота нужен список активных транзакций

Комментарии преподавателя

Начиная с PostgreSQL 16, слот логической публикации может размещаться на физической реплике, что позволяет снять часть нагрузки с мастера.

На реплике следует включить обратную связь параметром hot_standby_feedback, чтобы основной сервер не удалил нужные реплике версии строк раньше времени.

При создании слота логической репликации нужна информация о номерах активных транзакций. Эту информацию передают процессы bgwriter и checkpointer мастера в виде записи WAL, и если на мастере нет активности, создание слота задержится. Чтобы избежать ожидания, можно вызвать на мастере функцию pg_log_standby_snapshot.

https://postgrespro.ru/docs/postgresql/16/logicaldecoding-explanation

https://postgrespro.ru/docs/postgresql/16/functions-admin

Page 23: Итоги

Физическая репликация — базовый механизм для решения целого ряда задач

  • обеспечение высокой доступности
  • балансировка однотипной нагрузки
  • разделение разных видов нагрузки
  • и др.
  • Логическая репликация расширяет возможности

Page 24: Практика

  1. Настройте физическую потоковую репликацию между серверами alpha и beta. 2. Настройте каскадную репликацию на сервер gamma с задержкой применения записей WAL в десять секунд. 3. Проверьте работу репликации, убедитесь в том, что на сервере gamma данные появляются с установленной задержкой. 4. Остановите alpha и перейдите на сервер beta. 5. Проверьте, что сервер gamma продолжает работать в режиме реплики.

Комментарии преподавателя

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

Page 25: 1. Настройка репликации между серверами alpha и beta

Сначала создадим базу данных.

student$ psql
α=> CREATE DATABASE replica_usecases;
CREATE DATABASE
α=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".

Создаем слот и автономную резервную копию.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  -p 5432 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R \
  --create-slot \
  --slot=replica

Файлы postgresql.auto.conf и standby.signal подготовлены утилитой pg_basebackup.

postgres$ cat /var/lib/postgresql/16/beta/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
postgres$ ls -l /var/lib/postgresql/16/beta/standby.signal
-rw------- 1 postgres postgres 0 апр 10 18:27
/var/lib/postgresql/16/beta/standby.signal

Запускаем реплику:

student$ sudo pg_ctlcluster 16 beta start

2. Настройка репликации между серверами beta и gamma

student$ psql -p 5433 -d replica_usecases

Создадим автономную резервную копию с сервера beta, чтобы показать возможность выполнения резервного копирования с реплики. Слот создается утилитой.

student$ sudo pg_ctlcluster 16 gamma stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/gamma
postgres$ pg_basebackup \
  -p 5433 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/gamma \
  --create-slot --slot=replica \
  -R

Файл postgresql.auto.conf подготовлен утилитой pg_basebackup, добавляем задержку воспроизведения:

postgres$ echo "recovery_min_apply_delay = '10s'" | tee -a
/var/lib/postgresql/16/gamma/postgresql.auto.conf
recovery_min_apply_delay = '10s'

Вот что получилось:

Page 26: Page 26

postgres$ cat /var/lib/postgresql/16/gamma/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
primary_conninfo = 'user=postgres passfile=''/var/lib/postgresql/.pgpass''
channel_binding=prefer port=5433 sslmode=prefer sslnegotiation=postgres sslcompression=0
sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer
krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'
primary_slot_name = 'replica'
recovery_min_apply_delay = '10s'

Запускаем сервер gamma.

student$ sudo pg_ctlcluster 16 gamma start

3. Проверка работы

student$ psql -p 5434 -d replica_usecases

На alpha создадим таблицу и проверим, что она появилась сначала на beta, а через 10 секунд — и на gamma.

α=> CREATE TABLE test(s text);
CREATE TABLE
α=> INSERT INTO test VALUES ('Привет, мир!');
INSERT 0 1
β=> SELECT * FROM test;
      s
--------------
 Привет, мир!
(1 row)

Проверяем gamma:

γ=> SELECT * FROM test;
ERROR:  relation "test" does not exist
LINE 1: SELECT * FROM test;
                      ^

Таблицы пока нет. Подождем 10 секунд…

γ=> SELECT * FROM test;
      s
--------------
 Привет, мир!
(1 row)

Таблица появилась.

4. Переход на сервер beta

Текущая линия времени на gamma:

γ=> SELECT received_tli FROM pg_stat_wal_receiver;
 received_tli
--------------
            1
(1 row)

При повышении beta номер линии увеличится на единицу, процедура восстановления на gamma пойдет по новой линии благодаря значению по умолчанию:

γ=> SELECT setting, boot_val FROM pg_settings WHERE name = 'recovery_target_timeline';

Page 27: Page 27

Source slide page 27: Page 27{#source-figure-027 caption=“Source slide page 27: Page 27”}

 setting | boot_val
---------+----------
 latest  | latest
(1 row)
student$ sudo pg_ctlcluster 16 alpha stop
student$ sudo pg_ctlcluster 16 beta promote

Линия времени на gamma сменилась.

γ=> SELECT received_tli FROM pg_stat_wal_receiver;
 received_tli
--------------
            2
(1 row)

5. Проверка работы

На beta добавим в таблицу строку и проверим, что она появляется на gamma через 10 секунд.

β=> INSERT INTO test VALUES ('После перехода на второй сервер');
INSERT 0 1

Проверяем:

γ=> SELECT * FROM test;
      s
--------------
 Привет, мир!
(1 row)

Второй строки пока нет. Ждем 10 секунд…

γ=> SELECT * FROM test;
                s
---------------------------------
 Привет, мир!
 После перехода на второй сервер
(2 rows)

Строка появилась.

Уборка

Удалим созданные базы данных и слоты репликации, сбросим настройки параметров конфигурации.

γ=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
γ=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 gamma restart
β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
β=> DROP DATABASE replica_usecases;
DROP DATABASE
β=> SELECT pg_drop_replication_slot('replica');

Page 28: Page 28

 pg_drop_replication_slot
--------------------------
(1 row)
student$ sudo pg_ctlcluster 16 beta stop
student$ sudo pg_ctlcluster 16 gamma stop
student$ sudo pg_ctlcluster 16 alpha start
student$ psql
α=> SELECT pg_drop_replication_slot('replica');
 pg_drop_replication_slot
--------------------------
(1 row)
α=> DROP DATABASE replica_usecases;
DROP DATABASE

Page 29: Практика+

  1. Настройте физическую репликацию между серверами alpha и beta с обратной связью. При этом учтите необходимый уровень WAL для последующей настройки логической репликации. 2. Настройте логическую репликацию таблицы с сервера alpha на сервер gamma, поместив слот на beta — физическую реплику. Чтобы ускорить создание слота логической репликации на beta, воспользуйтесь функцией pg_log_standby_snapshot.

Комментарии преподавателя

  1. Перенесите структуру публикуемой таблицы на сервер gamma любым способом, например, используя pg_dump. Данные не переносите.

Публикация должна быть создана на alpha, подписка — на gamma, а в строке соединения подписки нужно указать сервер beta.

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

Page 30: 1. Физическая репликация alpha → beta

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

α=> ALTER SYSTEM SET hot_standby_feedback TO on;
ALTER SYSTEM SET wal_level TO logical;
ALTER SYSTEM
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

Создадим базу данных и таблицу.

student$ psql
α=> CREATE DATABASE replica_usecases;
CREATE DATABASE
α=> \c replica_usecases
You are now connected to database "replica_usecases" as user "student".
α=> CREATE TABLE test(
  id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  descr text
);
CREATE TABLE

Создаем автономную резервную копию.

student$ sudo pg_ctlcluster 16 beta stop
Cluster is not running.
postgres$ rm -rf /var/lib/postgresql/16/beta
postgres$ pg_basebackup \
  -p 5432 \
  --checkpoint=fast \
  --pgdata=/var/lib/postgresql/16/beta \
  -R \
  --create-slot --slot=replica
student$ sudo pg_ctlcluster 16 beta start

Проверим работу физической репликации.

α=> INSERT INTO test(descr) VALUES('Физика');
INSERT 0 1
student$ psql -p 5433 -d replica_usecases
β=> SELECT * FROM test;
 id | descr
----+--------
  1 | Физика
(1 row)

2. Логическая репликация beta → gamma

Скопируем базу данных и структуру таблицы на сервер gamma с beta, используя pg_dump.

student$ sudo pg_ctlcluster 16 gamma start
student$ pg_dump --create --schema-only --no-acl -d replica_usecases -p 5433 | psql -p 5434
SET
SET
SET
SET
SET
 set_config
------------

Page 31: Page 31

(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "replica_usecases" as user "student".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
student$ psql -p 5434 -d replica_usecases
γ=> SELECT * FROM test;
 id | descr
----+-------
(0 rows)

Мы скопировали лишь структуру таблицы, не данные. Создадим публикацию.

α=> CREATE PUBLICATION test_pub FOR TABLE test;
CREATE PUBLICATION
β=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test"

Подпишемся на публикуемые репликой (сервером beta) изменения в таблице test.

γ=> CREATE SUBSCRIPTION test_sub
CONNECTION 'user=student password=student dbname=replica_usecases host=localhost port=5433'
PUBLICATION test_pub;

Подписка ждет создания слота на сервере beta. Чтобы сократить ожидание, попросим alpha добавить в WAL запись с номерами активных транзакций:

α=> SELECT pg_log_standby_snapshot();
 pg_log_standby_snapshot
-------------------------
 0/305ADF0
(1 row)
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION
γ=> \dRs test_sub

Page 32: Page 32

Source slide page 32: Page 32{#source-figure-032 caption=“Source slide page 32: Page 32”}

           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)

Слот и подписка созданы. Немного подождем… Строка таблицы появилась на логической реплике.

γ=> SELECT * FROM test;
 id | descr
----+--------
  1 | Физика
(1 row)

Добавим еще одну строку.

α=> INSERT INTO test(descr) VALUES('А вот и логика');
INSERT 0 1
γ=> SELECT * FROM test;
 id |     descr
----+----------------
  1 | Физика
  2 | А вот и логика
(2 rows)

Уборка

Удалим подписку, созданную базу данных и слот репликации, сбросим настройки параметров конфигурации.

γ=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION
γ=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
γ=> DROP DATABASE replica_usecases;
DROP DATABASE
γ=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 gamma stop
α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> DROP DATABASE replica_usecases;
DROP DATABASE
β=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
β=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 beta stop
postgres$ rm -f /var/lib/postgresql/16/beta/standby.signal
α=> SELECT pg_drop_replication_slot('replica');
 pg_drop_replication_slot
--------------------------
(1 row)

Page 33: Page 33

α=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
α=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 alpha restart

08 Занятие 08: Кластерные технологии / Обзор

Исходная страница в нативном Markdown: Кластерные технологии / Обзор
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba3_08_cluster_overview.html Archive: DBA3-handouts-16.zip Страниц PDF: 28

Page 1: Кластерные технологии / Обзор

Авторские права

© Postgres Professional, 2017–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Алексей Береснев Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Ожидания от кластера
  • Средства реализации
  • Компоненты, необходимые для построения кластера
  • Примеры систем

Page 3: Ожидания от кластера

Высокая доступность системы

  • в том числе отказоустойчивость
  • Масштабируемость
  • Согласованность данных

Page 4: Высокая доступность

Обеспечение минимального времени простоя системы

  • вызванного как сбоями, так и плановыми работами
  • Различные характеристики
    • отношение времени работы системы к общему времени → 100 %
    • MTBF — время наработки на отказ
    • → ∞
    • RTO — целевое время восстановления
    • → 0
    • RPO — целевая точка восстановления
    • → 0
    • и другие

Комментарии преподавателя

Доступность системы — отношение времени, в течение которого система сохраняет доступность, к общему времени эксплуатации.

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

Часто говорят о доступности:

● «одна девятка» 90 % — 36,5 дней простоя в год;

● «две девятки» 99 % — 3,65 дней простоя в год;

● «три девятки» 99,9 % — 8,76 часов простоя в год;

● «четыре девятки» 99,99 % — 52,56 минут простоя в год;

● «пять девяток» 99,999 % — 5,26 минут простоя в год и т. д.

Высокая доступность предполагает цифры, близкие к 100 %.

Для примера: сервис управляемых баз данных PostgreSQL в Yandex Cloud обещает доступность узлов на чтение 99,99 %, на запись — 99,95 % (https://yandex.cloud/ru/services/managed-postgres).

Другими характеристиками доступности могут быть:

● время наработки на отказ (Mean Time Between Failures);

● целевое время восстановления (Recovery Time Objective) — время, за которое система должна восстановиться;

● целевая точка восстановления (Recovery Point Objective) — период, за который можно потерять данные при сбое, и др.

Page 5: Масштабируемость

Масштабируемость

  • способность системы справляться с увеличением рабочей нагрузки
  • при добавлении ресурсов
  • идеальная масштабируемость по времени отклика —
  • время отклика не должно увеличиваться
  • идеальная масштабируемость по пропускной способности —
  • число обрабатываемых запросов должно расти линейно
  • Ускорение
    • отношение времени отклика на одном узле ко времени отклика
    • в кластере из N узлов

Комментарии преподавателя

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

Масштабируемость по времени отклика показывает, как меняется время отклика (то есть время выполнения запроса) с ростом нагрузки при добавлении узлов кластера. Эта характеристика особенно важна для OLTP-систем. В идеале время отклика не должно увеличиваться.

Можно говорить не только о масштабируемости по времени отклика, но и об ускорении, которое показывает, как время отклика меняется с увеличением числа узлов при фиксированной нагрузке.

Масштабируемость по пропускной способности показывает, как меняется число обрабатываемых запросов с ростом объема данных при добавлении узлов кластера. Эта характеристика важна для OLAP-систем. В идеале число обрабатываемых запросов должно увеличиваться линейно с ростом числа узлов.

Идеальная масштабируемость (увы, не достижимая на практике) позволяет решать проблемы роста простым добавлением новых узлов в кластер.

Page 6: Согласованность

Локальные транзакции

  • практически никаких гарантий при переключении между узлами
  • согласованность обеспечивает приложение
  • Глобальные (распределенные) транзакции
    • ACID-согласованность
    • проблема атомарности транзакций

Комментарии преподавателя

Чем более строгие гарантии согласованности данных предоставляет кластер, тем больше он похож на «единой целое», тем проще с ним работать, тем меньше надо учитывать особенности в прикладном коде. Но тем дороже это обходится в смысле производительности.

Если кластер основан на схеме «мастер-реплика» с одним пишущим узлом, и транзакции выполняются локально на одном из узлов, гарантии могут оказаться очень слабыми. Например, один узел мог успеть применить запись о фиксации транзакции и ее результат уже доступен клиентам, а другой узел — еще нет. В таком случае у клиента, в частности, нет гарантии, что он не прочитает старое значение (на одном узле) уже после того, как прочитал новое (на другом узле). Обработка таких ситуаций ложится на приложение, которое должно быть рассчитано на работу в этой конфигурации.

Если кластер предоставляет глобальные (распределенные) транзакции, то можно говорить о согласованности в обычном смысле ACID: глобальная транзакция должна переводить базу данных из одного согласованного состояния в другое согласованное (C), при условии, что транзакция полностью выполняется на всех узлах (A) при отсутствии помех со стороны других конкурентных транзакций (I).

В этом случае требуется обеспечить атомарность транзакций: все узлы кластера должны выполнить одинаковое действие — либо применить, либо отменить транзакцию. PostgreSQL предоставляет для этого относительно простой протокол двухфазной фиксации (2PC), который, однако, не устойчив к сбоям.

Page 7: Средства реализации

Обеспечение высокой доступности

  • Синхронизация данных между узлами
  • Виды сбоев и их обнаружение
  • Распределенные протоколы консенсуса
  • Обеспечение масштабируемости

Page 8: Высокая доступность

Плановые работы без прерывания обслуживания

  • средствами PostgreSQL
  • временный вывод узла из кластера
  • Дублирование компонентов, исключение точек отказа
    • серверы PostgreSQL (и другие необходимые системы)
    • сетевое оборудование
    • электропитание
  • Обнаружение сбоев и управление восстановлением

Комментарии преподавателя

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

Основа обеспечения отказоустойчивости — дублирование всех узлов системы. Речь не только о серверах баз данных, но и о сетевом оборудовании, обеспечении электропитания и пр.

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

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

Page 9: Синхронизация

Shared nothing

  • физическая репликация
  • логическая репликация
  • сторонние таблицы, postgres_fdw
  • Shared disk
    • синхронизация не нужна, но требуется отказоустойчивое хранилище
    • нет штатных средств работы с общим диском

Комментарии преподавателя

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

Физическая репликация в силу своей однонаправленности позволяет строить кластер с одним ведущим узлом и несколькими репликами.

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

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

Архитектура shared disk с общим дисковым хранилищем дает интересные возможности, хотя и требует изменений в ядре PostgreSQL. При общем диске синхронизация не нужна (само хранилище, конечно, может использовать блочную репликацию для отказоустойчивости, но для СУБД оно представляется единым устройством), а вычислительные узлы, не имеющие состояния, требующего надежного хранения, могут запускаться и останавливаться в любой момент. Такая схема позволяет динамически подстраиваться под нагрузку, что особенно важно при работе в облачной инфраструктуре.

Page 10: Отказ узла

Source slide page 10: Отказ узла{#source-figure-010 caption=“Source slide page 10: Отказ узла”}

обнаружение отказа: периодический обмен сообщениями (heartbeat)

Комментарии преподавателя

Прежде чем говорить об обнаружении и управлении сбоями, нужно определиться, что считать сбоем. С точки зрения программного обеспечения кластера обычно рассматривают два вида сбоев: отказ узла и разделение сети (network partitioning).

При отказе узла этот узел перестает функционировать как часть кластера. Отказ может быть вызван остановом или сбоем сервера СУБД, сбоем операционной системы, выключением сервера и т. п.

Для обнаружения сбоев узлы кластера периодически обмениваются короткими сообщениями (heartbeat). Если какой-либо из узлов не отвечает в течении определенного времени, фиксируется и отрабатывается сбой этого узла.

Процедура отработки отказа зависит от архитектуры кластера. Например, в кластере, построенном на схеме «мастер–реплика», при сбое мастера какая-то из реплик должна занять его место (должно произойти переключение ролей).

Page 11: Разделение сети

Source slide page 11: Разделение сети{#source-figure-011 caption=“Source slide page 11: Разделение сети”}

split-brain: кластер распадается на независимые, одновременно работающие части, принимающие запросы на запись

  • отказ узла, задержка или отказ сети неотличимы друг от друга

Комментарии преподавателя

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

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

Заметим, что сеть может выйти из строя, а может временно начать терять пакеты или доставлять их с задержкой (более того, в некоторых ОС обычная загруженность процессора тоже может приводить к сетевым задержкам). Все эти случаи неотличимы от сбоя узла: если от узла вовремя не поступит подтверждение, кластер должен считать такую ситуацию сбоем. Узел может сохранять работоспособность или нет, но узнать это невозможно, если с ним нет связи.

Многие системы будут нестабильно работать в условиях ненадежного сетевого подключения. Чтобы уменьшить вероятность ложных срабатываний, можно увеличить пороговое значение, но тогда увеличится и время обнаружения настоящего сбоя, а это приведет к снижению доступности. Для примера: распределенное хранилище пар ключ–значение etcd предлагает устанавливать порог в 10 раз выше, чем время нормального отклика узла.

https://github.com/etcd-io/etcd

Page 12: Проблема консенсуса

Узлы кластера должны уметь приходить к общему мнению

  • общее представление о текущем состоянии всех узлов кластера
  • атомарная фиксация глобальных транзакций
  • Консенсус через обмен сообщениями
    • нет общей памяти (shared nothing)
    • используются специальные распределенные протоколы,
    • учитывающие возможность сбоев на каждом этапе переговоров
    • построение, доказательство корректности и тестирование реализации —
    • сложная задача!

Комментарии преподавателя

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

Поскольку никакого общего устройства хранения у узлов нет (мы рассматриваем системы класса shared nothing), то единственный способ прийти к общему мнению — договориться, используя какой-либо протокол распределенного консенсуса. Протокол определяет, какими сообщениями и в каком порядке должны обменяться узлы, чтобы прийти к соглашению.

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

Page 13: Протоколы 2PC, 3PC

Двухфазная фиксация

  • prepare:
  • координатор проводит голосование;
  • commit:
  • если все «за», координатор сообщает о фиксации.
  • поддерживается PostgreSQL (PREPARE TRANSACTION + COMMIT)
  • Трехфазная фиксация
    • prepare:
    • координатор проводит голосование;
    • precommit: если все «за», координатор сообщает всем о намерении;
    • commit:
    • если все успешно, координатор сообщает о фиксации.
  • Свойства
    • возможны ситуации (например, разделение сети),
    • в которых невозможно разрешить транзакцию до устранения сбоя

Комментарии преподавателя

Простой протокол для фиксации глобальных транзакций, поддержка которого встроена в PostgreSQL, — двухфазная фиксация (2-Phase Commit). Узел, выполняющий транзакцию, считается координатором. На подготовительной фазе (prepare) координатор сообщает остальным узлам о фиксации транзакции и дожидается от них подтверждения, что они готовы выполнить фиксацию (если хотя бы один узел не готов — транзакция обрывается). На фазе фиксации (commit) координатор сообщает всем узлам о решении зафиксировать транзакцию. При получении от всех подтверждения, координатор тоже фиксирует транзакцию.

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

Это ограничение преодолевает более сложный протокол трехфазной фиксации (3-Phase Commit). В нем вводится дополнительная фаза precommit, на которой координатор сообщает всем узлам о принятом решении фиксировать транзакцию. После этого, даже если произойдет сбой координатора, оставшиеся узлы будут иметь право зафиксировать транзакцию самостоятельно (а до фазы precommit — самостоятельно отменить ее).

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

Page 14: Кворумные протоколы

Source slide page 14: Кворумные протоколы{#source-figure-014 caption=“Source slide page 14: Кворумные протоколы”}

Paxos, Raft, ZAB, E3PC и другие

  • при отказе до N/2–1 узлов связное большинство продолжает работу
  • меньшинство блокируется до устранения сбоя

Комментарии преподавателя

Алгоритмы, основанные на кворуме (большинстве голосов), позволяют в условиях разделения сети не блокировать узлы, сохранившие связь друг с другом и образующие кворум. Таким образом (если голоса всех узлов равны), из N узлов N/2+1 смогут продолжить работу.

Узлы, оставшиеся в меньшинстве, вынуждены ожидать исправления сбоя, чтобы принять решение о фиксации или откате открытых транзакций. Такие узлы могут полностью не обслуживать клиентов или как минимум не принимать запросы на запись, чтобы не допустить ситуацию split-brain.

Существует ряд алгоритмов, доказанных математически и имеющих протестированные и проверенные временем реализации.

● Paxos опубликован в 1998 году, а фактически появился в конце 80-х. Есть целое семейство этих алгоритмов для различных ситуаций.

● Raft создавался как алгоритм, эквивалентный Paxos, но более простой для понимания. В этом алгоритме один из узлов выбирается в качестве лидера, который может изменять данные; остальные узлы читающие. Этот протокол лежит в основе таких систем, как etcd и Consul (распределенные хранилища «ключ–значение»).

● ZAB используется в системе ZooKeeper.

● E3PC — Enhanced 3PC улучшает алгоритм трехфазной фиксации, разрешая фиксировать транзакцию при получении большинства (а не всех) голосов.

Существуют иные подходы, позволяющие решать проблему консенсуса, например, Virtual synchrony (используется в Corosync).

Page 15: Масштабируемость

Пропускная способность по чтению

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

Комментарии преподавателя

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

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

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

Заметим, что возможные схемы шардирования очень сильно связаны с логикой данных; какое-то универсальное решение тут невозможно.

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

Page 16: Распределение нагрузки

Средства, специфичные для PostgreSQL

  • библиотека libpq
  • PgBouncer — менеджер пула
  • Pgpool-II — балансировщик и менеджер пула
  • Одиссей — многопоточный менеджер пула
  • …
  • Другие средства
    • виртуальный IP-адрес
    • HAProxy — балансировщик TCP/HTTP
    • round-robin DNS
    • …

Комментарии преподавателя

Для распределения нагрузки уже имеется достаточно много готовых решений; обычно такие средства не входят в состав самого кластера.

● В строке подключения libpq можно указать несколько узлов, порядок подключения к которым устанавливает параметр load_balance_hosts, а параметром target_session_attrs задают свойства сеанса (например, разрешение только читающих запросов).

● PgBouncer — удобный менеджер пула соединений для PostgreSQL.

● Pgpool-II — менеджер пула и балансировщик, способный распределять пишущую нагрузку на мастер, а читающую на реплики.

● Одиссей — многопоточный менеджер пула с полной поддержкой SSL/TLS и возможностями аутентификации LDAP и PAM.

Из общих средств, не связанных с PostgreSQL, отметим:

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

● HAProxy — балансировщик и прокси-сервер, работающий на уровне TCP и HTTP. Для обновления списка узлов подходит средство управления конфигурациями confd. Или можно использовать способность HAProxy выполнять проверки (health check) узлов кластера для исключения неподходящих из списка балансировки.

● Некоторые системы (Consul), предоставляют round-robin DNS: выбор узлов и балансировка может происходить на этапе разрешения имени (но кеширование сводит на нет преимущества такого решения).

Есть и другие средства. Многие можно использовать совместно.

Page 17: Компоненты и примеры

Необходимые компоненты, отсутствующие в PostgreSQL

  • Примеры систем

Page 18: Отказоустойчивость

Агент

  • управляет одним экземпляром PostgreSQL
  • Управляющий
    • логика управления всем кластером
  • Консенсус
    • внешняя система под управлением одного из протоколов консенсуса
    • поддержка общего состояния для управляющих
  • Точка входа
    • способ направить клиентов на нужный узел кластера

Комментарии преподавателя

Если основной целью создания кластера является высокая доступность (в частности, отказоустойчивость), естественным решением является взять несколько экземпляров обычного PostgreSQL и объединить их дополнительным программным обеспечением. Синхронизация данных возлагается на физическую репликацию, то есть в кластере выделяется основной узел (мастер) и реплики.

Поскольку PostgreSQL не может сам себя конфигурировать, перезапускать, останавливать и т. п., нужна программа, которая будет этим заниматься. Эту роль выполняет агент.

Логика управления кластером сосредоточена в управляющих. На этом уровне принимается решение о том, что такой-то узел следует считать вышедшим из строя, о том, какую из реплик следует назначить новым мастером при выходе старого из строя и т. п.

Чтобы разделять общую точку зрения, управляющие опираются на систему, работающую по одному из рассмотренных ранее распределенных протоколов консенсуса.

Наконец, для клиентов требуется единая точка входа, которая направит их на нужный узел (или узлы) кластера.

В разных кластерных системах эти компоненты могут называться по-разному; некоторые могут объединяться вместе. Если какие-то компоненты отсутствуют, кластер надо дополнять другими решениями.

Page 19: Ограждение (fencing)

Отключение узла в неизвестном состоянии от клиентов

  • из-за разделения сети может возникнуть ситуация split-brain
  • нужно не только оградить узел,
  • но и не допустить в это время переход на реплику
  • Механизмы
    • отключение питания
    • отключение сетевого порта
    • завершение и перенаправление клиентских соединений

Комментарии преподавателя

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

Если этого не сделать, то мастер, оказавшись в результате разделения сети в меньшинстве, может продолжить работу (ситуация split-brain). Полагаться на то, что управляющие, обнаружив сбой, сами остановят сервер PostgreSQL, нельзя: у управляющих может не оказаться связи с агентом, может произойти сбой в агенте и т. п.

Процедура может выполняться по-разному, но не должна рассчитывать на доступность узла по сети. Например:

● программно отключать питание сервера (интерфейсы IPMI, PDU);

● программно отключать сетевой порт на коммутаторе;

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

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

Page 20: Пример: Patroni

Source slide page 20: Пример: Patroni{#source-figure-020 caption=“Source slide page 20: Пример: Patroni”}

Комментарии преподавателя

Patroni https://github.com/patroni/patroni — кластерное решение с открытым исходным кодом, рассчитанное на использование как в облаках, так и на выделенных серверах.

Здесь агент и управляющий объединены в одном компоненте. Для консенсуса используется хранилище «ключ–значение», предоставляемое одной из систем etcd (Raft), Consul (Raft) или ZooKeeper (ZAB).

Для точки входа требуется использовать отдельное решение. Обычно предлагается использовать HAProxy в связке с confd, который периодически обновляет конфигурацию HAProxy на основе информации из слоя консенсуса. Стандартная настройка предполагает перенаправление всех клиентов на узел, выполняющий роль мастера.

Чтобы HAProxy сам не стал точкой отказа, необходимо иметь несколько таких серверов и, например, виртуальный IP-адрес для клиентов. Однако это приводит к тому, что конфигурации разных серверов HAProxy нужно обновлять синхронно, и мы снова сталкиваемся с вопросом построения кластера — уже на другом уровне.

Для ограждения узла (fencing) может использоваться механизм сторожевого таймера (watchdog timer) ядра Linux. Если узел, будучи мастером, не может связаться со слоем консенсуса в течение определенного времени, таймер перезагружает операционную систему.

Page 21: Пример: стек ClusterLabs

Source slide page 21: Пример: стек ClusterLabs{#source-figure-021 caption=“Source slide page 21: Пример: стек ClusterLabs”}

Комментарии преподавателя

Компания ClusterLabs (https://clusterlabs.org/) предоставляет стек компонентов с открытым исходным кодом для построения кластеров из более или менее произвольных систем.

Агент (resource agent в терминологии этой системы) представляет конкретную систему как унифицированный «ресурс», реализуя операции останова, запуска, повышения реплики и т. п. Этот компонент должен быть реализован отдельно для каждой системы.

Управляющий компонент — Pacemaker — взаимодействует с ресурсом через агента. Для общего состояния и обнаружения сбоев используется Corosync (Virtual synchrony).

Ограждению (fencing) узлов уделено большое внимание. Используется технология STONITH (Shoot The Other Node In The Head), позволяющая надежно изолировать сбойный узел от кластера и клиентов путем программного отключения питания или сетевого порта на коммутаторе.

Точкой входа для клиентов обычно является виртуальный IP-адрес, который управляющий поднимает на узле, являющимся в данный момент мастером.

Дополнительно при необходимости можно распределить читающую нагрузку по репликам с помощью HAProxy, PgBouncer и т. п.

Для PostgreSQL имеется открытое решение PAF (PostgreSQL Automatic Failover): http://clusterlabs.github.io/PAF/

Page 22: Пример: BiHA

Source slide page 22: Пример: BiHA{#source-figure-022 caption=“Source slide page 22: Пример: BiHA”}

Комментарии преподавателя

Очевидно, что большое количество внешних компонентов сильно усложняет развертывание и обслуживание кластера. Чтобы упростить эти задачи, можно встроить все необходимое внутрь самого PostgreSQL. Эта нетривиальная задача решена в кластере BiHA (Built-in High Availability), который является составной частью Postgres Pro Enterprise.

Внешне такой кластер выглядит, как обычные узлы, связанные физической репликацией, но на самом деле в каждом узле Postgres Pro Enterprise реализован механизм обнаружения ошибок, предотвращения разделения кластера (split-brain), слой консенсуса (используется протокол Raft), логика управления и агент.

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

Дополнительное программное обеспечение для точки входа не используется, кластер полагается на способность клиента самостоятельно определять, работает ли он с основным сервером (выполняющим пишущие и читающие запросы) или с репликой. Такие средства имеются в том числе в libpq.

https://postgrespro.ru/docs/enterprise/16/biha-solution

Page 23: Согласованность

Менеджер распределенных транзакций

  • требует модификации ядра PostgreSQL
  • может быть встроен или представлен отдельным компонентом

Комментарии преподавателя

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

Для создания кластера, предоставляющего клиентам единую согласованную картину данных, необходим менеджер распределенных транзакций (Global Transaction Manager), который реализует протокол распределенного консенсуса для атомарной фиксации транзакций, охватывающих несколько узлов.

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

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

Page 24: Пример: multimaster

Source slide page 24: Пример: multimaster{#source-figure-024 caption=“Source slide page 24: Пример: multimaster”}

Комментарии преподавателя

Примером такой системы может служить расширение multimaster Postgres Pro Enterprise. В СУБД встраивается менеджер распределенных транзакций, который расширяет имеющийся двухфазный протокол 2PC до E3PC. Для этого на узлах запускаются процессы-арбитры, выполняющие дополнительные коммуникации. Используется алгоритм консенсуса Paxos.

https://postgrespro.ru/docs/enterprise/16/multimaster

Любой из узлов кластера multimaster (кроме рефери, если он используется) может принимать запросы как на чтение, так и на запись. Читающие транзакции выполняются локально. Пишущие транзакции являются распределенными; узел, фиксирующий транзакцию, становится координатором.

Репликация данных между узлами кластера основана на логической репликации, DDL реплицируется собственным алгоритмом.

Для распределения нагрузки имеет смысл балансировать соединения между узлами кластера. Например, можно указывать разные строки соединений для разных клиентов, а можно воспользоваться HAProxy.

Приложение, работающее на одном сервере, может быть запущено и на multimaster-кластере без дополнительных забот о согласованности данных. Поддерживаются уровни изоляции Read Committed и Repeatable Read. Однако, в силу ограничений реализации, некоторые нюансы приложение должно учитывать.

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

Page 25: Масштабируемость

Большое количество узлов

  • репликация или общий диск
  • шардинг
  • параллельные вычисления

Комментарии преподавателя

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

Данные между узлами могут реплицироваться полностью (получим возможность масштабирования по чтению) или частично (например, по принципу шардинга — получим возможность масштабирования и по записи). Системы, рассчитанные на облачную инфраструктуру и динамическую подстройку ресурсов под нагрузку, могут использовать вариант с общим диском и разделением узлов на вычислительные и хранящие данные.

Распределение данных по большому количеству узлов позволяет организовать и параллельное выполнение запросов.

Page 26: Пример: Citus

Source slide page 26: Пример: Citus{#source-figure-026 caption=“Source slide page 26: Пример: Citus”}

Комментарии преподавателя

Citus https://github.com/citusdata/citus — расширение PostgreSQL для распределения данных и запросов по узлам кластера.

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

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

Citus гарантирует атомарность изменений на всех узлах кластера с помощью глобальных транзакций по протоколу двухфазной фиксации 2PC.

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

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

Page 27: Пример: Neon

Source slide page 27: Пример: Neon{#source-figure-027 caption=“Source slide page 27: Пример: Neon”}

Комментарии преподавателя

Решение с открытым кодом Neon продвигает концепцию облачных бессерверных вычислений. Идея заключается в разделении СУБД на отдельные узлы, не требующие хранения состояния, и использовании большого и дешевого, но медленного хранилища данных.

Вычислительные узлы (по сути, экземпляры PostgreSQL, от которого «отрезано» журналирование и работа с диском) запускаются в контейнерах под управлением Kubernetes. Поскольку эти узлы не имеют собственного состояния, контейнеры можно запускать и останавливать в любой момент в зависимости от активности клиентов, не платя за использование лишних ресурсов.

Для сохранения данных пишущий экземпляр (он может быть только один) передает журнальные записи компоненту safekeeper. Журнальные записи зафиксированных транзакций необходимо хранить надежно; для этого компоненты safekeeper образуют отдельный отказоустойчивый кластер, работающий по проколу Paxos.

Журнальные записи асинхронно передаются от safekeeper компоненту pageserver, который применяет их к страницам данных. Pageserver возвращает страницы по запросу клиентов, при необходимости сбрасывая их в «бездонное» объектное хранилище S3 и считывая их оттуда. Таким образом, pageserver служит кешем данных, с которыми ведется активная работа, и является аналогом буферного кеша.

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

https://github.com/neondatabase/neon

Page 28: Итоги

Для создания кластера требуются средства, не входящие в стандартный PostgreSQL

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