Всем прекрасно известно, что при асинхронной репликации 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.
Читайте также в нашем блоге:
Комментарии (2)

diderevyagin
23.12.2025 14:30Интересный сценарий, действительно можно ногу прострелить ... спасибо большое !
Sleuthhound
Спасибо за статью, очень полезно!