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

Есть кластер PostgreSQL из двух узлов: test-psql-1 и test-psql-2, между которыми настроена физическая репликация. Этот кластер находится под управлением Patroni. Кроме этих двух узлов есть отдельно стоящий узел test-psql-3, на который должна реплицироваться только одна таблица с помощью логической репликации. PostgreSQL на test-psql-3 всегда обращается к лидеру кластера Patroni за счет того, что подключается к этому кластеру через балансировщик нагрузки кластера HAproxy. HAproxy, в свою очередь, определяет текущего лидера кластера Patroni с помощью http-запроса к API Patroni.
В этой статье:
Как Patroni управляет слотами логической репликации
Начиная с версии PostgreSQL 11, Patroni умеет управлять слотами логической репликации. Это реализовано с помощью функции Patroni «Permanent replication slots». Функционал подробно описан в первом PR , где он был реализован.
Чтобы Patroni автоматически создавал слот логической репликации на узле, принимающем роль лидера, с правильными отметками LSN, достаточно задать в конфигурации Patroni следующие настройки:
slots: permanent_logical_slot_name: type: logical database: my_db plugin: pgoutput
При добавлении этого блока в конфигурацию Patroni автоматически создаст на лидере слот логической репликации permanent_logical_slot_name, используя pg_create_logical_replication_slot.
Помимо этого, он создаст такой же слот репликации на всех репликах, для которых в конфигурации Patroni установлен тэг nofailover: false. Для этого Patroni копирует состояние слота с лидера: читает файл state из каталога pg_replslot с помощью pg_read_binary_file и создаёт идентичные файлы на репликах, после чего перезапускает на них PostgreSQL.
После создания слотов Patroni каждые loop_wait секунд (по умолчанию — 10 секунд: параметр конфигурации Patroni задаёт частоту Health Check и обмена данными с DCS) будет перемещать значение confirmed_flush_lsn на репликах в соответствии с данными из DCS, используя функцию pg_replication_slot_advance.
Возможные проблемы при таком подходе
Казалось бы, при failover, выполненном средствами Patroni, самая большая неприятность, которая может случиться со слотом логической репликации, — это повторная передача в логическую реплику некоторого количества уже переданных прежде данных из-за возможного отставания в DCS зафиксированной позиции confirmed_flush_lsn. Но это не совсем так.
Дело в том, что в продуктов��х средах при выборе между доступностью, целостностью данных и производительностью зачастую жертвуют доступностью. То есть для повышения производительности репликация настраивается асинхронной, что чревато потерей данных при автоматическом failover. Поэтому автоматический failover будет отключен, и в случае аварии на лидере кластера БД решение о целесообразности переключения лидера принимает человек.
Но раз автоматический failover отключен (на репликах установлен тэг nofailover: true), Patroni не будет заранее автоматически создавать на этих репликах слоты, перечисленные в slots.
Что же произойдет при таком управляемом failover, если предыдущий лидер недоступен, а логическая реплика в момент failover отставала? Давайте проверим.
Эксперимент с отставшей логической репликой
Исходное состояние кластера под управлением Patroni:
patronictl list + Cluster: main (7441886727573043134) ----+-----------+----+-----------+------------------+ | Member | Host | Role | State | TL | Lag in MB | Tags | +-------------+-----------------+---------+-----------+----+-----------+------------------+ | test-psql-1 | 192.168.199.69 | Leader | running | 38 | | clonefrom: true | +-------------+-----------------+---------+-----------+----+-----------+------------------+ | test-psql-2 | 192.168.199.106 | Replica | streaming | 38 | 0 | clonefrom: true | | | | | | | | nofailover: true | +-------------+-----------------+---------+-----------+----+-----------+------------------+
Лидером является test-psql-1.
Создадим на нём тестовую БД processing и в ней таблицу payment:
CREATE DATABASE processing; \c processing CREATE TABLE payment ( id INTEGER PRIMARY KEY, customer INTEGER NOT NULL, merchant INTEGER NOT NULL, sum INTEGER NOT NULL );
Добавим туда тестовую запись:
INSERT INTO payment (id, customer, merchant, sum) VALUES (1, 0, 1, 1000); SELECT * FROM payment; id | customer | merchant | sum ----+----------+----------+------ 1 | 0 | 1 | 1000
И опубликуем эту таблицу для логической репликации:
CREATE PUBLICATION payment_pub FOR TABLE payment;
На сервере test-psql-3 создадим тестовую БД test, в ней таблицу payment с такой же структурой:
CREATE DATABASE test; \c test CREATE TABLE payment ( id INTEGER PRIMARY KEY, customer INTEGER NOT NULL, merchant INTEGER NOT NULL, sum INTEGER NOT NULL );
И создадим подписку с подключением к лидеру через haproxy:
test=# CREATE SUBSCRIPTION payment_sub CONNECTION 'dbname=processing host=192.168.199.5 user=my_repl_user password=my_repl_password' PUBLICATION payment_pub; NOTICE: created replication slot "payment_sub" on publisher
После этого добавим созданный слот репликации в DCS patroni:
slots: payment_sub: database: processing plugin: pgoutput type: logical
Проверим содержимое payment:
test=# SELECT * FROM payment; id | customer | merchant | sum ----+----------+----------+------ 1 | 0 | 1 | 1000 (1 row)
На реплику test-psql-2 данные через физический слот репликации, разумеется, тоже отреплицировались.
Давайте сымитируем отставание логической реплики. Для этого просто временно остановим PostgreSQL на test-psql-3:
pg_ctlcluster 16 main stop
После чего на лидере test-psql-1 добавим запись в тестовую таблицу:
INSERT INTO payment (id, customer, merchant, sum) VALUES (2, 0, 1, 2000);
И после этого спровоцируем аварию. Для этого отрубим сетевые соединения к PostgreSQL и etcd на лидере test-psql-1:
# Блокируем доступ к PostgreSQL: iptables -A INPUT -p tcp --dport 5432 -j DROP # Блокируем доступ в Patroni со стороны haproxy: iptables -A INPUT -p tcp --dport 8008 -j DROP # Блокируем обращения от Patroni к etcd: iptables -A OUTPUT -p tcp --dport 2379 -j REJECT
Кластер потерял лидера:
test-psql-2 ~ # patronictl list + Cluster: main (7441886727573043134) ----+-----------+----+-----------+------------------+ | Member | Host | Role | State | TL | Lag in MB | Tags | +-------------+-----------------+---------+-----------+----+-----------+------------------+ | test-psql-2 | 192.168.199.106 | Replica | streaming | 38 | 0 | clonefrom: true | | | | | | | | nofailover: true | +-------------+-----------------+---------+-----------+----+-----------+------------------+
Предположим, в этот момент приходит дежурный инженер, оценивает масштаб проблемы и принимает решение, что оперативно восстановление лидера невозможно. Чтобы отдать разрешение на failover, инженер снимает тэг nofailover с реплики.
Patroni успешно выполнил failover:
test-psql-2 ~ # patronictl list + Cluster: main (7441886727573043134) ---+---------+----+-----------+-----------------+ | Member | Host | Role | State | TL | Lag in MB | Tags | +-------------+-----------------+--------+---------+----+-----------+-----------------+ | test-psql-2 | 192.168.199.106 | Leader | running | 39 | | clonefrom: true | +-------------+-----------------+--------+---------+----+-----------+-----------------+
И автоматически создал слот репликации на test-psql-2:
SELECT * FROM pg_replication_slots ; slot_name | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase | conflicting -------------+----------+-----------+--------+------------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------+------------- test_psql_1 | | physical | | | f | f | | | | D/F0033F78 | | reserved | | f | payment_sub | pgoutput | logical | 188519 | processing | f | f | | | 169319 | D/F0034DB8 | D/F0036650 | reserved | | f | f (2 rows)
Реплика test-psql-2 в момент аварии не отставала от лидера, все данные успели на нее отреплицироваться, потерь нет:
processing=# SELECT * FROM payment; id | customer | merchant | sum ----+----------+----------+------ 1 | 0 | 1 | 1000 2 | 0 | 1 | 2000 (2 rows)
Теперь вновь включим логическую реплику на test-psql-3 (она как бы «перестает отставать»):
pg_ctlcluster 16 main start
И обнаруживаем, что данные, отсутствующие из-за отставания реплики на test-psql-3 в момент аварии, до этой реплики так и не доехали:
test=# SELECT * FROM payment; id | customer | merchant | sum ----+----------+----------+------ 1 | 0 | 1 | 1000 (1 row)
И не доедут никогда.
Добавим в тестовую таблицу запись на новом лидере test-psql-2:
INSERT INTO payment (id, customer, merchant, sum) VALUES (3, 0, 1, 3000);
Состояние таблицы на лидере:
processing=# SELECT * FROM payment; id | customer | merchant | sum ----+----------+----------+------ 1 | 0 | 1 | 1000 2 | 0 | 1 | 2000 3 | 0 | 1 | 3000 (3 rows)
Состояние логической реплики:
test=# SELECT * FROM payment; id | customer | merchant | sum ----+----------+----------+------ 1 | 0 | 1 | 1000 3 | 0 | 1 | 3000 (2 rows)
Таким образом легко воспроизвести ситуацию, когда в случае ручного переключения роли лидера в кластере Patroni при отставании логической реплики будут потеряны данные, несмотря на то, что в основном кластере потери данных не было.
Если бы предыдущий лидер был доступен, то слот корректно скопировался бы. Но в данной ситуации на новом лидере Patroni создал слот просто с текущим LSN.
Решение проблемы
Решением этой проблемы может быть переход на автоматический failover средствами Patroni. Но при полностью асинхронной репликации в таком случае возникает опасность неконтролируемой потери данных в кластере при срабатывании автоматики.
К счастью, в PostgreSQL начиная с 17-й версии предложен нативный подход к управлению слотами логической репликации на репликах — синхронизация слотов репликации.
Для включения этой функции необходимо выполнить следующий условия:
1. На физической реплике должны быть включены параметры:
sync_replication_slots = on hot_standby_feedback = on
При включении синхронизации слотов PostgreSQL з��пустит специальный процесс slotsync worker, который и будет выполнять синхронизацию слотов с меткой failover.
2. Слот логической репликации должен быть создан на лидере с параметром failover.
Пример команды создания подписки на логической реплике, вызывающей создание слота с нужным параметром на лидере:
test=# CREATE SUBSCRIPTION payment_sub CONNECTION 'dbname=processing host=192.168.199.5 user=my_repl_user password=my_repl_password' PUBLICATION payment_pub WITH (failover = true);
3. Для предотвращения ситуации, когда логическая реплика может опередить физическую, необходимо добавить в конфигурацию PostgreSQL список синхронизируемых слотов:
synchronized_standby_slots = ‘payment_pub, another_pub, ...’
При установке этого параметра wal sender отдаст данные логической реплике только после того, как физические реплики подтвердят получение соответствующего WAL. Если нарушить этот порядок, возобновление логической репликации с нового лидера может стать невозможным.
Кроме изменений в конфигурации PostgreSQL необходимо также подготовить к работе с failover-слотами Patroni, поскольку при использовании встроенного механизма синхронизации слотов репликации на уровне СУБД в управлении слотами логической репликации средствами Patroni больше нет нужды.
Все слоты логической репликации следует добавить в ignore_slots в конфигурации Patroni. Пример:
ignore_slots: - name: payment_sub type: logical database: processing plugin: pgoutput
Иначе Patroni может удалить не описанные в конфигурации слоты.
Но еще лучше – обновить Patroni до версии >= 4.1.0.
Дело в том, что разработчики Patroni любезно включили в релиз 4.1.0 от 23 сентября 2025 года PR, в котором исключается взаимодействие Patroni со слотами, имеющими признак failover.
Таким образом, используя PostrgreSQL версии >= 17 и Patroni >= 4.1.0 администраторы БД теперь имеют возможность настроить отказоустойчивую логическую репликацию только встроенными средствами PostgreSQL, не внося никаких корректировок в конфигурацию Patroni.
Вместо заключения
Если вы используете логическую репликацию в связке с Patroni — проверьте, как ведут себя слоты логической репликации при ручном failover. И, по возможности, переходите на PostgreSQL 17+ и Patroni 4.1+, чтобы решить проблему раз и навсегда, используя новый функционал для синхронизации слотов.
P. S.
Читайте также в нашем блоге:
