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

Уровень 3.0

Предусловия:

  • Изучена лекция 7 «Логическая репликация»

Настройка и анализ логической репликации​

Подготовка виртуальной машины Host1​

  1. Подключитесь к виртуальной машине посредством webssh, указав ip-адрес Host1.

  2. Запустите оболочку от имени пользователя postgres:

    [student@ServerName ~]$ sudo -iu postgres
  3. В терминале подключитесь в psql к базе данных postgres от имени пользователя postgres:

    [postgres@ServerName ~]$ psql
    psql (15.5)
    Type "help" for help.
  4. Создайте роль replicator:

    postgres=# CREATE ROLE replicator WITH LOGIN REPLICATION PASSWORD 'replicator';
    CREATE ROLE

    Указанная роль будет использоваться для подключения по протоколу репликации, поэтому требуется наличие атрибутов LOGIN и REPLICATION.

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

  5. Создайте базу данных host1_db и подключитесь к ней:

    postgres=# CREATE DATABASE host1_db;
    CREATE DATABASE
    postgres=# \c host1_db
    You are now connected to database "host1_db" as user "postgres".

Подготовка виртуальной машины Host2​

  1. Подключитесь к виртуальной машине посредством webssh, указав ip-адрес Host2.

  2. Запустите оболочку от имени пользователя postgres:

[student@ServerName ~]$ sudo -iu postgres
  1. В терминале подключитесь в psql к базе данных postgres от имени пользователя postgres:

    [postgres@ServerName ~]$ psql
    psql (15.5)
    Type "help" for help.
  2. Создайте базу данных host2_db:

    postgres=# CREATE DATABASE host2_db;
    CREATE DATABASE
    postgres=# \c host2_db
    You are now connected to database "host2_db" as user "postgres".

Создание публикации​

в этом разделе все действия выполняются на виртуальной машине Host1.

  1. В сеансе 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 | postmaster
    max_wal_senders | 10 | postmaster
    wal_level | replica | postmaster
    (3 rows)

    Значения параметров max_wal_senders и max_replication_slots являются достаточным для выполнения лабораторной работы.

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

    Изменение параметра wal_level требует перезапуска экземпляра (context=postmaster).

  2. В сеансе psql задайте значение параметра wal_level=logical, выйдите из psql и перезапустите экземпляр Pangolin:

    postgres=# ALTER SYSTEM SET wal_level = logical;
    ALTER SYSTEM
    postgres=# \q
    [postgres@ServerName ~]$ pg_ctl restart -l logfile
    waiting for server to shut down.... done
    server stopped
    waiting for server to start.... done
    server started
  3. Подключитесь в psql к базе данных host1_db:

    [postgres@ServerName ~]$ psql -d host1_db
    psql (15.5)
    Type "help" for help.
  4. В сеансе psql создайте таблицу host1_table и вставьте в нее 3 строки:

    host1_db=# CREATE TABLE host1_table (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, value integer, non_published text);
    CREATE TABLE
    host1_db=# INSERT INTO host1_table(value, non_published) VALUES(100, 'сто'), (200, 'двести'), (300, 'триста');
    INSERT 0 3

    В созданной таблице значение первичного ключа (столбец id) может задаваться только автоматически с использованием последовательности (GENERATED ALWAYS AS IDENTITY).

  5. В сеансе psql выдайте права на чтение таблицы host1_table роли replicator:

    host1_db=# GRANT SELECT ON host1_table TO replicator;
    GRANT

    Таблица host1_table будет включена в публикацию, поэтому роль replicator, от имени которой будет осуществляться подключение с подписчика, должна иметь права на чтение данной таблицы.

  6. В сеансе 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) реплицироваться не будут.

  7. В сеансе psql получите информацию об имеющихся публикациях:

    host1_db=# \dRp+
    Publication host1_pub
    Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
    ----------+------------+---------+---------+---------+-----------+----------
    postgres | f | t | t | f | f | f
    Tables:
    "public.host1_table" (id, value) WHERE (id < 8)

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

Создание подписки​

в этом разделе все действия выполняются на виртуальной машине Host2.

  1. В сеансе 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 | postmaster
    max_worker_processes | 10 | postmaster
    (2 rows)

    Вспомним, что за получение и применение декодированных записей на стороне подписчика отвечает процесс logical replication worker. Соответственно, максимальное количество указанных процессов на стороне подписчика должно быть достаточным, как и общее количество рабочих процессов.

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

  2. В сеансе 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).

  3. В сеансе psql создайте подписку на публикацию host1_pub, указав в качестве параметра host ip-адрес виртуальной машины Host1:

    host2_db=# CREATE SUBSCRIPTION host2_sub
    CONNECTION 'host={IP-Address} dbname=host1_db user=replicator password=replicator'
    PUBLICATION host1_pub;
    NOTICE: created replication slot "host2_sub" on publisher
    CREATE SUBSCRIPTION

    При создании подписки указана строка подключения к базе данных с соответствующей публикацией от имени роли replicator, а также имя подписки - host1_pub.

    В выводе команды имеется сообщение о том, что при выполнении команды на стороне публикации был создан слот репликации с именем host2_sub.

  4. В сеансе psql получите информацию о текущем состоянии имеющихся подписок:

    host2_db=# \dRs
    Name | Owner | Enabled | Publication
    -----------+----------+---------+-------------
    host2_sub | postgres | t | {host1_pub}
    (1 row)
    host2_db=# SELECT * FROM pg_stat_subscription\gx
    -[ RECORD 1 ]---------+------------------------------
    subid | 16413
    subname | host2_sub
    pid | 45386
    relid |
    received_lsn | 0/BE98B70
    last_msg_send_time | 2025-11-14 05:09:24.662148+00
    last_msg_receipt_time | 2025-11-14 05:09:24.661948+00
    latest_end_lsn | 0/BE98B70
    latest_end_time | 2025-11-14 05:09:24.662148+00

    Представление pg_stat_subscription отражает текущее состояние подписок. В частности, из него можно узнать LSN, до которого подписчиком получены изменения (received_lsn), и LSN, получение записи которого подтверждено процессу wal sender.

  5. В сеансе psql выполните запрос к таблице host1_table:

    host2_db=# SELECT * FROM host1_table;
    id | value | is_local
    ----+-------+----------
    1 | 100 |
    2 | 200 |
    3 | 300 |
    (3 rows)

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

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

Проверка репликации​

  1. В сеансе psql на Host1 в таблицу host1_table вставьте одну строку и обновите одну из имеющихся строк:

    host1_db=# INSERT INTO host1_table(value, non_published) VALUES(400, 'четыреста');
    INSERT 0 1
    host1_db=# UPDATE host1_table SET value = 250 WHERE id = 2;
    UPDATE 1
  2. В сеансе psql на Host2 выполните запрос к таблице host1_table:

    host2_db=# SELECT * FROM host1_table;
    id | value | is_local
    ----+-------+----------
    1 | 100 |
    3 | 300 |
    4 | 400 |
    2 | 250 |
    (4 rows)

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

  3. В сеансе psql на Host1 в таблицу host1_table удалите одну строку и выполните запрос:

    host1_db=# DELETE FROM host1_table WHERE id = 3;
    DELETE 1
    host1_db=# SELECT * FROM host1_table;
    id | value | non_published
    ----+-------+---------------
    1 | 100 | сто
    4 | 400 | четыреста
    2 | 250 | двести
    (3 rows)
  4. В сеансе 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 для репликации.

  5. В сеансе 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 уже имеется в таблице. Она была получена от экземпляра с публикацией в результате репликации.

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

  6. В сеансе psql на Host2 вставьте новую строку в таблицу host1_table c указанием значения столбца id:

    host2_db=# INSERT INTO host1_table(id, value, is_local) VALUES(5, 500, true);
    INSERT 0 1
    host2_db=# SELECT * FROM host1_table;
    id | value | is_local
    ----+-------+----------
    1 | 100 |
    3 | 300 |
    4 | 400 |
    2 | 250 |
    5 | 500 | t
    (5 rows)

    Теперь строка на стороне подписки успешно вставлена. При этом было заполнено поле is_local, что может быть сделано только локально.

  7. В сеансе psql на Host1 в таблицу host1_table вставьте еще две строки:

    host1_db=# INSERT INTO host1_table(value, non_published) VALUES(500, 'пятьсот');
    INSERT 0 1
    host1_db=# INSERT INTO host1_table(value, non_published) VALUES(600, 'шестьсот');
    INSERT 0 1
  8. В сеансе 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 с использованием последовательности.

    Из-за конфликта логическая репликация приостановлена до устранения причин конфликта.

  9. В сеансе psql на Host2 посмотрите состояние подписки:

    host2_db=# SELECT * FROM pg_stat_subscription\gx
    -[ RECORD 1 ]---------+----------
    subid | 16413
    subname | host2_sub
    pid |
    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.

  10. В сеансе 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 подтверждает наличие конфликта.

    В чем заключается конфликт можно узнать из журнала сообщений.

  11. В сеансе psql на Host2 посмотрите несколько последних сообщений в журнале сообщений:

    host2_db=# \! tail -n 5 logfile
    2025-11-14 05:27:02.686 UTC [45628] LOG: logical replication apply worker for subscription "host2_sub" has started
    2025-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/BE99C60
    2025-11-14 05:27:02.699 UTC [36264] LOG: background worker "logical replication worker" (PID 45628) exited with exit code 1
  12. В сеансе psql на Host1 посмотрите состояние слота репликации:

    host1_db=# SELECT * FROM pg_replication_slots\gx
    -[ RECORD 1 ]-------+----------
    slot_name | host2_sub
    plugin | pgoutput
    slot_type | logical
    datoid | 32798
    database | host1_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

    При конфликте слот репликации неактивен (active=f) и блокирует сегменты WAL от удаления (wal_status=reserved).

  13. В сеансе psql на Host2 удалите локально вставленную строку и выполните запрос к таблице host1_table:

    host2_db=# DELETE FROM host1_table WHERE is_local;
    DELETE 1
    host2_db=# SELECT * FROM host1_table;
    id | value | is_local
    ----+-------+----------
    1 | 100 |
    3 | 300 |
    4 | 400 |
    2 | 250 |
    5 | 500 |
    6 | 600 |
    (6 rows)

    Конфликт разрешился, и две вставленные на стороне публикации строки успешно "добрались" до подписчика.

  14. В сеансе psql на Host2 проверьте состояние подписки:

    host2_db=# SELECT * FROM pg_stat_subscription\gx
    -[ RECORD 1 ]---------+------------------------------
    subid | 16413
    subname | host2_sub
    pid | 45804
    relid |
    received_lsn | 0/BE99F18
    last_msg_send_time | 2025-11-14 05:38:30.349567+00
    last_msg_receipt_time | 2025-11-14 05:38:30.349375+00
    latest_end_lsn | 0/BE99F18
    latest_end_time | 2025-11-14 05:38:30.349567+00

    Подписка снова работает.

Анализ логического декодирования​

  1. В сеансе 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.

  2. В сеансе 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 | reserved
    decoding_slot | test_decoding | logical | f | reserved
    (2 rows)

    Добавился созданный слот логической репликации, пока еще неактивный.

  3. В сеансе 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;
    BEGIN
    INSERT 0 1
    UPDATE 1
    INSERT 0 1
    DELETE 1
    COMMIT
  4. В сеансе psql на Host1 посмотрите сформированные декодированные сообщения:

    host1_db=# SELECT * FROM pg_logical_slot_get_changes('decoding_slot', NULL, NULL);
    lsn | xid | data
    -----------+-----+----------------------------------------------------------------------------------------------------
    0/BE9A2C0 | 868 | BEGIN 868
    0/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]:4
    0/BE9A7C0 | 868 | COMMIT 868
    (6 rows)

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

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

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

    Модуль вывода перед отправкой подписчику отфильтрует лишнюю информацию.

  5. В сеансе 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 отсутствует на стороне подписчика из-за того, что не удовлетворяет условию фильтра.

  6. В сеансе psql на Host1 удалите созданный слот репликации:

    host1_db=# SELECT pg_drop_replication_slot('decoding_slot');
    pg_drop_replication_slot
    --------------------------

    (1 row)

Завершение​

Завершение на Host2​

  1. В сеансе psql удалите подписку:

    host2_db=# DROP SUBSCRIPTION host2_sub;
    NOTICE: dropped replication slot "host2_sub" on publisher
    DROP SUBSCRIPTION

    Вместе с подпиской также удален соответствующий слот логической репликации.

  2. В сеансе psql подключитесь к базе данных postgres и удалите базу данных host2_db:

    host2_db=# \c postgres
    You are now connected to database "postgres" as user "postgres".
    postgres=# DROP DATABASE host2_db;
    DROP DATABASE
  3. Выйдите из сеанса psql и выйдите из оболочки, запущенной от имени пользователя postgres:

    postgres=# \q
    [postgres@ServerName ~]$ exit
    logout

Завершение на Host1​

  1. В сеансе psql подключитесь к базе данных postgres и удалите базу данных host1_db:

    host2_db=# \c postgres
    You are now connected to database "postgres" as user "postgres".
    postgres=# DROP DATABASE host1_db;
    DROP DATABASE
  2. В сеансе psql удалите роль replicator:

    postgres=# DROP ROLE replicator;
    DROP ROLE
  3. В сеансе psql сбросьте значения конфигурационных параметров и выйдите из psql:

    postgres=# ALTER SYSTEM RESET ALL;
    ALTER SYSTEM
    postgres=# \q
  4. Перезапустите экземпляр Pangolin:

    [postgres@ServerName ~]$ pg_ctl restart -l logfile
    waiting for server to shut down.... done
    server stopped
    waiting for server to start.... done
    server started
  5. Выйдите из оболочки, запущенной от имени пользователя postgres:

    [postgres@ServerName ~]$ exit
    logout

Самопроверка​

Вопрос 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

О чем может свидетельствовать вывод выполненной команды? Выберите все правильные варианты: