Уровень 3.0
Предусловия:
- Изучена лекция 7 «Логическая репликация»
Настройка и анализ логической репликации
Подготовка виртуальной машины Host1
-
Подключитесь к виртуальной машине посредством
webssh, указав ip-адрес Host1. -
Запустите оболочку от имени пользователя
postgres:[student@ServerName ~]$ sudo -iu postgres -
В терминале подключитесь в
psqlк базе данныхpostgresот имени пользователяpostgres:[postgres@ServerName ~]$ psqlpsql (15.5)Type "help" for help. -
Создайте роль
replicator:postgres=# CREATE ROLE replicator WITH LOGIN REPLICATION PASSWORD 'replicator';CREATE ROLEУказанная роль будет использоваться для подключения по протоколу репликации, поэтому требуется наличие атрибутов
LOGINиREPLICATION.Экземпляр с подпиской будет использовать указанную роль для подключения к базе данных с публикацией.
-
Создайте базу данных
host1_dbи подключитесь к ней:postgres=# CREATE DATABASE host1_db;CREATE DATABASEpostgres=# \c host1_dbYou are now connected to database "host1_db" as user "postgres".
Подготовка виртуальной машины Host2
-
Подключитесь к виртуальной машине посредством
webssh, указав ip-адрес Host2. -
Запустите оболочку от имени пользователя
postgres:
[student@ServerName ~]$ sudo -iu postgres
-
В терминале подключитесь в
psqlк базе данныхpostgresот имени пользователяpostgres:[postgres@ServerName ~]$ psqlpsql (15.5)Type "help" for help. -
Создайте базу данных
host2_db:postgres=# CREATE DATABASE host2_db;CREATE DATABASEpostgres=# \c host2_dbYou are now connected to database "host2_db" as user "postgres".
Создание публикации
в этом разделе все действия выполняются на виртуальной машине Host1.
-
В сеансе
psqlпроверьте значения конфигурационных параметровwal_level,max_wal_sendersиmax_replication_slots:postgres=# SELECT name, setting, context FROM pg_settings WHERE name IN ('wal_level', 'max_wal_senders', 'max_replication_slots');name | setting | context-----------------------+---------+------------max_replication_slots | 10 | postmastermax_wal_senders | 10 | postmasterwal_level | replica | postmaster(3 rows)Значения параметров
max_wal_sendersиmax_replication_slotsявляются достаточным для выполнения лабораторной работы.В то время как значение параметра
wal_level=replicaне позволяет создавать публикации для логической репликации. Вспомним, что у публикующего экземпляра уровень журнала предзаписи должен бытьlogical.Изменение параметра
wal_levelтребует перезапуска экземпляра (context=postmaster). -
В сеансе
psqlзадайте значение параметраwal_level=logical, выйдите изpsqlи перезапустите экземпляр Pangolin:postgres=# ALTER SYSTEM SET wal_level = logical;ALTER SYSTEMpostgres=# \q[postgres@ServerName ~]$ pg_ctl restart -l logfilewaiting for server to shut down.... doneserver stoppedwaiting for server to start.... doneserver started -
Подключитесь в
psqlк базе данныхhost1_db:[postgres@ServerName ~]$ psql -d host1_dbpsql (15.5)Type "help" for help. -
В сеансе
psqlсоздайте таблицуhost1_tableи вставьте в нее 3 строки:host1_db=# CREATE TABLE host1_table (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, value integer, non_published text);CREATE TABLEhost1_db=# INSERT INTO host1_table(value, non_published) VALUES(100, 'сто'), (200, 'двести'), (300, 'триста');INSERT 0 3В созданной таблице значение первичного ключа (столбец
id) может задаваться только автоматически с использованием последовательности (GENERATED ALWAYS AS IDENTITY). -
В сеансе
psqlвыдайте права на чтение таблицыhost1_tableролиreplicator:host1_db=# GRANT SELECT ON host1_table TO replicator;GRANTТаблица
host1_tableбудет включена в публикацию, поэтому рольreplicator, от имени которой будет осуществляться подключение с подписчика, должна иметь права на чтение данной таблицы. -
В сеансе
psqlсоздайте публикацию:host1_db=# CREATE PUBLICATION host1_pub FOR TABLE host1_table (id, value) WHERE (id < 8) WITH (publish = 'insert, update');CREATE PUBLICATIONПубликация включает таблицу
host1_table. Однако, не все ее столбцы подлежат репликации. В частности, столбецnot_publishedне включен в список публикуемых столбцов.Помимо этого, в публикации указан фильтр для реплицируемых строк -
WHERE (id < 8). Реплицироваться будут изменения только семи строк.Также заданы операции, действия которых подлежат репликации -
publish = 'insert, update'. Реплицироваться будут действия операцийINSERTиUPDATE. Удаление строк (DELETE) и очистка таблицы (TRUNCATE) реплицироваться не будут. -
В сеансе
psqlполучите информацию об имеющихся публикациях:host1_db=# \dRp+Publication host1_pubOwner | All tables | Inserts | Updates | Deletes | Truncates | Via root----------+------------+---------+---------+---------+-----------+----------postgres | f | t | t | f | f | fTables:"public.host1_table" (id, value) WHERE (id < 8)Вывод команды соответствует созданной публикации. В поле
Via rootуказано значение параметраpublish_via_partition_root, отвечающего за выбор режима фильтрации для секционированных таблиц. В этой лабораторной работе его значение несущественно.
Создание подписки
в этом разделе все действия выполняются на виртуальной машине Host2.
-
В сеансе
psqlпроверьте значения конфигурационных параметровmax_logical_replication_workersиmax_worker_processes:host2_db=# SELECT name, setting, context FROM pg_settings WHERE name IN ('max_logical_replication_workers', 'max_worker_processes');name | setting | context---------------------------------+---------+------------max_logical_replication_workers | 4 | postmastermax_worker_processes | 10 | postmaster(2 rows)Вспомним, что за получение и применение декодированных записей на стороне подписчика отвечает процесс
logical replication worker. Соответственно, максимальное количество указанных процессов на стороне подписчика должно быть достаточным, как и общее количество рабочих процессов.Для выполнения данной лабораторной работы значения указанных параметров являются достаточными.
-
В сеансе
psqlсоздайте таблицуhost1_table:host2_db=# CREATE TABLE host1_table (id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, value bigint, is_local boolean);CREATE TABLEВспомним, что на стороне подписчика таблицы для применения реплицируемых данных должны быть заранее созданы и иметь те же полные имена, что и на стороне публикации.
Таблица должна содержать все столбцы, указанные в соответствующей подписке, то есть
idиvalue. При этом обратите внимание, что тип данных столбцаvalueна стороне подписчика отличается (bigintвместоinteger). Это допустимо, если возможно преобразование текстового представления типа данных на стороне публикации в целевой тип данных на стороне подписчика.Помимо реплицируемых столбцов в таблице также создан столбец
is_local.Также обратите внимание, что первичный ключ (столбец
id) может задаваться как автоматически с использованием последовательности, так и вручную (GENERATED BY DEFAULT AS IDENTITY). -
В сеансе
psqlсоздайте подписку на публикациюhost1_pub, указав в качестве параметраhostip-адрес виртуальной машины Host1:host2_db=# CREATE SUBSCRIPTION host2_subCONNECTION 'host={IP-Address} dbname=host1_db user=replicator password=replicator'PUBLICATION host1_pub;NOTICE: created replication slot "host2_sub" on publisherCREATE SUBSCRIPTIONПри создании подписки указана строка подключения к базе данных с соответствующей публикацией от имени роли
replicator, а также имя подписки -host1_pub.В выводе команды имеется сообщение о том, что при выполнении команды на стороне публикации был создан слот репликации с именем
host2_sub. -
В сеансе
psqlполучите информацию о текущем состоянии имеющихся подписок:host2_db=# \dRsName | Owner | Enabled | Publication-----------+----------+---------+-------------host2_sub | postgres | t | {host1_pub}(1 row)host2_db=# SELECT * FROM pg_stat_subscription\gx-[ RECORD 1 ]---------+------------------------------subid | 16413subname | host2_subpid | 45386relid |received_lsn | 0/BE98B70last_msg_send_time | 2025-11-14 05:09:24.662148+00last_msg_receipt_time | 2025-11-14 05:09:24.661948+00latest_end_lsn | 0/BE98B70latest_end_time | 2025-11-14 05:09:24.662148+00Представление
pg_stat_subscriptionотражает текущее состояние подписок. В частности, из него можно узнать LSN, до которого подписчиком получены изменения (received_lsn), и LSN, получение записи которого подтверждено процессуwal sender. -
В сеансе
psqlвыполните запрос к таблицеhost1_table:host2_db=# SELECT * FROM host1_table;id | value | is_local----+-------+----------1 | 100 |2 | 200 |3 | 300 |(3 rows)При создании подписки выполняется первичная синхронизация данных, поэтому данные таблицы, удовлетворяющие условию фильтра, переданы подписчику.
После первичной синхронизации запускается непосредственно логическая репликация.
Проверка репликации
-
В сеансе
psqlна Host1 в таблицуhost1_tableвставьте одну строку и обновите одну из имеющихся строк:host1_db=# INSERT INTO host1_table(value, non_published) VALUES(400, 'четыреста');INSERT 0 1host1_db=# UPDATE host1_table SET value = 250 WHERE id = 2;UPDATE 1 -
В сеансе
psqlна Host2 выполните запрос к таблицеhost1_table:host2_db=# SELECT * FROM host1_table;id | value | is_local----+-------+----------1 | 100 |3 | 300 |4 | 400 |2 | 250 |(4 rows)Операции вставки и обновление успешно применились на стороне подписчика.
-
В сеансе
psqlна Host1 в таблицуhost1_tableудалите одну строку и выполните запрос:host1_db=# DELETE FROM host1_table WHERE id = 3;DELETE 1host1_db=# SELECT * FROM host1_table;id | value | non_published----+-------+---------------1 | 100 | сто4 | 400 | четыреста2 | 250 | двести(3 rows) -
В сеансе
psqlна Host2 повторите выполнение запроса к таблицеhost1_table:host2_db=# SELECT * FROM host1_table;id | value | is_local----+-------+----------1 | 100 |3 | 300 |4 | 400 |2 | 250 |(4 rows)Операция удаления строки не применилась на подписчике. Вспомним, что при создании репликации мы не указывали операцию
DELETEдля репликации. -
В сеансе
psqlна Host2 вставьте новую строку в таблицуhost1_tableбез указания значения столбцаid:host2_db=# INSERT INTO host1_table(value, is_local) VALUES(500, true);ERROR: duplicate key value violates unique constraint "host1_table_pkey"DETAIL: Key (id)=(1) already exists.Вспомним, что при логической репликации допускаются изменения на стороне подписчика.
Однако, в этом случае операция вставки завершилась ошибкой.
Дело в том, что при вставке значение столбца
idбыло сгенерировано автоматически с использованием последовательности, созданной на стороне подписчика при определении таблицы.Соответственно, поскольку это первая вставка строки на стороне подписчика, то и значение, выданное последовательностью, равно 1. Но строка с
id=1уже имеется в таблице. Она была получена от экземпляра с публикацией в результате репликации.Это подтверждает тот факт, что в механизме логической репликации последовательности не реплицируются, а существуют независимо друг от друга на стороне публикации и подписки.
-
В сеансе
psqlна Host2 вставьте новую строку в таблицуhost1_tablec указанием значения столбцаid:host2_db=# INSERT INTO host1_table(id, value, is_local) VALUES(5, 500, true);INSERT 0 1host2_db=# SELECT * FROM host1_table;id | value | is_local----+-------+----------1 | 100 |3 | 300 |4 | 400 |2 | 250 |5 | 500 | t(5 rows)Теперь строка на стороне подписки успешно вставлена. При этом было заполнено поле
is_local, что может быть сделано только локально. -
В сеансе
psqlна Host1 в таблицуhost1_tableвставьте еще две строки:host1_db=# INSERT INTO host1_table(value, non_published) VALUES(500, 'пятьсот');INSERT 0 1host1_db=# INSERT INTO host1_table(value, non_published) VALUES(600, 'шестьсот');INSERT 0 1 -
В сеансе
psqlна Host2 повторите выполнение запроса к таблицеhost1_table:host2_db=# SELECT * FROM host1_table;id | value | is_local----+-------+----------1 | 100 |3 | 300 |4 | 400 |2 | 250 |5 | 500 | t(5 rows)Операции вставки строк не применились на стороне подписки.
Так произошло, поскольку было нарушено ограничение целостности при вставке строки с
id=5.Вспомним, что строку с
id=5на стороне подписчика мы вставили вручную.В то время как на стороне публикации было сгенерировано то же значение
idс использованием последовательности.Из-за конфликта логическая репликация приостановлена до устранения причин конфликта.
-
В сеансе
psqlна Host2 посмотрите состояние подписки:host2_db=# SELECT * FROM pg_stat_subscription\gx-[ RECORD 1 ]---------+----------subid | 16413subname | host2_subpid |relid |received_lsn |last_msg_send_time |last_msg_receipt_time |latest_end_lsn |latest_end_time |В представлении
pg_stat_subscriptionотсутствует информация о текущем состоянии подписки. Это связано с тем, что процессlogical replication workerбыл остановлен и теперь периодически перезапускается, ожидая разрешения конфликта.Отследить неудачные попытки применения конфликтующих записей можно в представлении
pg_stat_subscription_stats. -
В сеансе
psqlна Host2 выполните два раза с перерывом не менее 10 секунд запрос представлениюpg_stat_subscription_stats:host2_db=# SELECT * FROM pg_stat_subscription_stats;subid | subname | apply_error_count | sync_error_count | stats_reset-------+-----------+-------------------+------------------+-------------16413 | host2_sub | 59 | 0 |(1 row)host2_db=# SELECT * FROM pg_stat_subscription_stats;subid | subname | apply_error_count | sync_error_count | stats_reset-------+-----------+-------------------+------------------+-------------16413 | host2_sub | 61 | 0 |(1 row)Растущее значение поля
apply_error_countподтверждает наличие конфликта.В чем заключается конфликт можно узнать из журнала сообщений.
-
В сеансе
psqlна Host2 посмотрите несколько последних сообщений в журнале сообщений:host2_db=# \! tail -n 5 logfile2025-11-14 05:27:02.686 UTC [45628] LOG: logical replication apply worker for subscription "host2_sub" has started2025-11-14 05:27:02.698 UTC [45628] ERROR: duplicate key value violates unique constraint "host1_table_pkey"2025-11-14 05:27:02.698 UTC [45628] DETAIL: Key (id)=(5) already exists.2025-11-14 05:27:02.698 UTC [45628] CONTEXT: processing remote data for replication origin "pg_16413" during message type "INSERT" for replication target relation "public.host1_table" in transaction 866, finished at 0/BE99C602025-11-14 05:27:02.699 UTC [36264] LOG: background worker "logical replication worker" (PID 45628) exited with exit code 1 -
В сеансе
psqlна Host1 посмотрите состояние слота репликации:host1_db=# SELECT * FROM pg_replication_slots\gx-[ RECORD 1 ]-------+----------slot_name | host2_subplugin | pgoutputslot_type | logicaldatoid | 32798database | host1_dbtemporary | factive | factive_pid |xmin |catalog_xmin | 866restart_lsn | 0/BE99928confirmed_flush_lsn | 0/BE999D8wal_status | reservedsafe_wal_size |two_phase | fПри конфликте слот репликации неактивен (
active=f) и блокирует сегменты WAL от удаления (wal_status=reserved). -
В сеансе
psqlна Host2 удалите локально вставленную строку и выполните запрос к таблицеhost1_table:host2_db=# DELETE FROM host1_table WHERE is_local;DELETE 1host2_db=# SELECT * FROM host1_table;id | value | is_local----+-------+----------1 | 100 |3 | 300 |4 | 400 |2 | 250 |5 | 500 |6 | 600 |(6 rows)Конфликт разрешился, и две вставленные на стороне публикации строки успешно "добрались" до подписчика.
-
В сеансе
psqlна Host2 проверьте состояние подписки:host2_db=# SELECT * FROM pg_stat_subscription\gx-[ RECORD 1 ]---------+------------------------------subid | 16413subname | host2_subpid | 45804relid |received_lsn | 0/BE99F18last_msg_send_time | 2025-11-14 05:38:30.349567+00last_msg_receipt_time | 2025-11-14 05:38:30.349375+00latest_end_lsn | 0/BE99F18latest_end_time | 2025-11-14 05:38:30.349567+00Подписка снова работает.
Анализ логического декодирования
-
В сеансе
psqlна Host1 создайте слот репликации с модулем вывода для анализа декодированных сообщений:host1_db=# SELECT pg_create_logical_replication_slot('decoding_slot','test_decoding');pg_create_logical_replication_slot------------------------------------(decoding_slot,0/BE99F60)(1 row)Вспомним, что модуль вывода подготавливает декодированные сообщения для передачи подписчику.
Стандартным модулем вывода является
pgoutput.Однако, в лабораторной работе для анализа декодированных сообщений удобно использовать модуль вывода
test_decoding.Поскольку модуль вывода привязан к слоту логической репликации, то его выбор можно выполнить при создании слота функцией
pg_create_logical_replication_slot. -
В сеансе
psqlна Host1 посмотрите состояние слотов репликации:host1_db=# SELECT slot_name, plugin, slot_type, active, wal_status FROM pg_replication_slots;slot_name | plugin | slot_type | active | wal_status---------------+---------------+-----------+--------+------------host2_sub | pgoutput | logical | t | reserveddecoding_slot | test_decoding | logical | f | reserved(2 rows)Добавился созданный слот логической репликации, пока еще неактивный.
-
В сеансе
psqlна Host1 выполните несколько операций с таблицейhost1_tableвнутри транзакции:host1_db=# BEGIN;INSERT INTO host1_table(value, non_published) VALUES(700, 'семьсот');UPDATE host1_table SET value = 150 WHERE id = 1;INSERT INTO host1_table(value, non_published) VALUES(800, 'восемьсот');DELETE FROM host1_table WHERE id = 4;COMMIT;COMMIT;BEGININSERT 0 1UPDATE 1INSERT 0 1DELETE 1COMMIT -
В сеансе
psqlна Host1 посмотрите сформированные декодированные сообщения:host1_db=# SELECT * FROM pg_logical_slot_get_changes('decoding_slot', NULL, NULL);lsn | xid | data-----------+-----+----------------------------------------------------------------------------------------------------0/BE9A2C0 | 868 | BEGIN 8680/BE9A328 | 868 | table public.host1_table: INSERT: id[integer]:7 value[integer]:700 non_published[text]:'семьсот'0/BE9A648 | 868 | table public.host1_table: UPDATE: id[integer]:1 value[integer]:150 non_published[text]:'сто'0/BE9A6A8 | 868 | table public.host1_table: INSERT: id[integer]:8 value[integer]:800 non_published[text]:'восемьсот'0/BE9A748 | 868 | table public.host1_table: DELETE: id[integer]:40/BE9A7C0 | 868 | COMMIT 868(6 rows)Получить декодированные сообщения из модуля вывода можно с использованием функции
pg_logical_slot_get_changes.Видно, что записи являются осмысленным представлением произошедших изменений с каждой из строк, поэтому является платформо независимыми.
При этом в декодированные сообщения попали все изменения, произошедшие со строками, включая изменения операций и столбцов, не участвующих в публикации, а также изменения не подпадающие под условия фильтра.
Модуль вывода перед отправкой подписчику отфильтрует лишнюю информацию.
-
В сеансе
psqlна Host2 повторите выполнение запроса к таблицеhost1_table:host2_db=# SELECT * FROM host1_table;id | value | is_local----+-------+----------3 | 300 |4 | 400 |2 | 250 |7 | 700 |1 | 150 |5 | 500 |6 | 600 |(7 rows)Обратите внимание, что строка с id=
8отсутствует на стороне подписчика из-за того, что не удовлетворяет условию фильтра. -
В сеансе
psqlна Host1 удалите созданный слот репликации:host1_db=# SELECT pg_drop_replication_slot('decoding_slot');pg_drop_replication_slot--------------------------(1 row)
Завершение
Завершение на Host2
-
В сеансе
psqlудалите подписку:host2_db=# DROP SUBSCRIPTION host2_sub;NOTICE: dropped replication slot "host2_sub" on publisherDROP SUBSCRIPTIONВместе с подпиской также удален соответствующий слот логической репликации.
-
В сеансе
psqlподключитесь к базе данныхpostgresи удалите базу данныхhost2_db:host2_db=# \c postgresYou are now connected to database "postgres" as user "postgres".postgres=# DROP DATABASE host2_db;DROP DATABASE -
Выйдите из сеанса
psqlи выйдите из оболочки, запущенной от имени пользователяpostgres:postgres=# \q[postgres@ServerName ~]$ exitlogout
Завершение на Host1
-
В сеансе
psqlподключитесь к базе данныхpostgresи удалите базу данныхhost1_db:host2_db=# \c postgresYou are now connected to database "postgres" as user "postgres".postgres=# DROP DATABASE host1_db;DROP DATABASE -
В сеансе
psqlудалите рольreplicator:postgres=# DROP ROLE replicator;DROP ROLE -
В сеансе
psqlсбросьте значения конфигурационных параметров и выйдите изpsql:postgres=# ALTER SYSTEM RESET ALL;ALTER SYSTEMpostgres=# \q -
Перезапустите экземпляр Pangolin:
[postgres@ServerName ~]$ pg_ctl restart -l logfilewaiting for server to shut down.... doneserver stoppedwaiting for server to start.... doneserver started -
Выйдите из оболочки, запущенной от имени пользователя
postgres:[postgres@ServerName ~]$ exitlogout
Самопроверка
Вопрос 1
В сеансе psql выполнена следующая последовательность команд:
some_db=# \d some_table
Table "public.some_table"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | not null |
value | text | | |
Indexes:
"some_table_pkey" PRIMARY KEY, btree (id)
some_db=# \dRp+
Publication some_pub
Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
----------+------------+---------+---------+---------+-----------+----------
postgres | f | t | f | f | t | f
Tables:
"public.some_table" (id) WHERE (id > 5)
Изменения каких успешно выполненных SQL-команд будут реплицированы подписчикам публикации some_pub при их наличии? Выберите все правильные варианты
Вопрос 2
В сеансе psql успешно выполнена команда создания подписки:
some_db=# CREATE SUBSCRIPTION some_sub
CONNECTION 'host=172.29.53.200 dbname=some_db user=replicator password=replicator'
PUBLICATION some_pub;
Какие результаты были получены при выполнении указанной команды? Выберите все правильные варианты
Вопрос 3
В сеансе psql выполнена следующая команда:
some_db=# SELECT * FROM pg_replication_slots\gx
-[ RECORD 1 ]-------+----------
slot_name | some_sub
plugin | pgoutput
slot_type | logical
datoid | 32802
database | some_db
temporary | f
active | f
active_pid |
xmin |
catalog_xmin | 866
restart_lsn | 0/BE99928
confirmed_flush_lsn | 0/BE999D8
wal_status | reserved
safe_wal_size |
two_phase | f
О чем может свидетельствовать вывод выполненной команды? Выберите все правильные варианты: