Как доказать, что CDC продолжит читать слот после переключения PostgreSQL 18
Зелёная строка со знакомым именем в `pg_replication_slots` ещё ничего не гарантирует. Я считаю standby готовым только тогда, когда PostgreSQL синхронизировал постоянный failover-слот, сохранил нужный WAL, а остановленный потребитель после promotion прочитал контрольное событие со своей прежней позиции. Ниже — моя схема проверки для PostgreSQL 17 и 18: конфигурация, критерии go/no-go и испытание на условном примере переплётной мастерской «Кожаный корешок» (28 рабочих мест), где изменения из базы учёта заказов уходят через Debezium в аналитику подрядчика.
Почему наличия слота недостаточно
У физической и логической репликации разные позиции. Физическая реплика получает и воспроизводит WAL, чтобы стать новым primary. CDC-потребитель держит собственную логическую позицию: серверную — в confirmed_flush_lsn слота, клиентскую — во внешнем хранилище offset. У Debezium это offset Kafka Connect. Поэтому исправная физическая реплика не означает, что на ней есть пригодное состояние конкретного логического потока.
Я регулярно вижу одну и ту же ошибку: администратор создаёт на standby логический слот с тем же именем и успокаивается. Строка действительно есть, но это независимый локальный слот: у неё synced = false, другая позиция и, возможно, уже недоступный catalog_xmin. После promotion потребитель либо получает ошибку о недостающем WAL, либо начинает с неправильного места. Хуже второй вариант: сервис работает, а разрыв замечают по отчётам через несколько дней. Если же на standby слота нет вовсе, то после promotion у CDC два плохих исхода: коннектор падает с ошибкой об отсутствующем слоте либо, в зависимости от настроек, создаёт новый слот с текущей позиции нового primary. Во втором случае все изменения между последним подтверждённым offset и моментом создания слота молча выпадают из потока — без повторного snapshot их уже не вернуть.
Начиная с PostgreSQL 17 (и в 18 без изменения принципа) штатный путь такой: исходный логический слот создаётся с failover = true, а standby копирует его фоновым slotsync worker. Документация прямо говорит, что после failover пригодны только постоянные слоты, успевшие получить synced = true до отказа. Формальный критерий из раздела Logical Replication Failover — synced AND NOT temporary AND invalidation_reason IS NULL. Я добавляю ещё две проверки: нужный WAL не потерян и позиция слота после остановки CDC догнала зафиксированный confirmed_flush_lsn primary. Именно остановка потребителя на короткое время превращает движущуюся цель в проверяемую точку.
- На старом primary логический слот имеет `failover = true`.
- На standby тот же слот имеет `synced = true` и `temporary = false`.
- `invalidation_reason IS NULL`, а `wal_status` не показывает потерю WAL.
- Физическая реплика получила и воспроизвела WAL до контрольного LSN.
- После promotion CDC подключился к тому же имени слота и подтвердил новое событие.
Конфигурация, которую я выбираю
На primary я создаю постоянный физический слот для каждой реплики-кандидата и указываю именно его в synchronized_standby_slots. Не логический слот Debezium. Параметр (он есть с PostgreSQL 17) задерживает выдачу изменений логическому потребителю, пока физический слот не подтвердит получение соответствующего WAL. Это немного увеличивает задержку CDC, зато не позволяет потребителю убежать дальше WAL, находящегося на кандидате.
SELECT * FROM pg_create_physical_replication_slot('pg18_stby_1');wal_level = logical
max_wal_senders = 10
max_replication_slots = 10
max_slot_wal_keep_size = '20GB'
synchronized_standby_slots = 'pg18_stby_1'По умолчанию max_slot_wal_keep_size = -1, то есть слот удерживает WAL без ограничения; конечное значение я задаю сознательно, чтобы зависший потребитель не заполнил диск primary.
На standby обязательны primary_slot_name, hot_standby_feedback = on, sync_replication_slots = on и настоящий dbname внутри primary_conninfo. Без dbname фоновый worker не сможет подключиться к базе для синхронизации слотов. Пароль я храню в файле с правами 0600, а не в конфиге.
primary_conninfo = 'host=10.20.0.11 port=5432 user=repl dbname=postgres application_name=pg18_stby_1 passfile=/var/lib/postgresql/.pgpass'
primary_slot_name = 'pg18_stby_1'
hot_standby_feedback = on
sync_replication_slots = onНа дату статьи, 14 сентября 2026 года, актуальные минорные версии — PostgreSQL 18.6 и 17.11; на меньших минорных версиях я failover-слоты не испытываю. Debezium беру из текущей стабильной ветки с встроенным pgoutput и у нового коннектора сразу включаю свойство slot.failover: оно работает только на PostgreSQL 17 и новее, конкретную версию коннектора сверяйте с его документацией. Если существующий слот создан с failover = false, я не удаляю его посреди рабочего дня: перенос позиции или повторный snapshot оформляю отдельным изменением.
connector.class=io.debezium.connector.postgresql.PostgresConnector
database.hostname=pg-rw.internal
database.port=5432
database.dbname=orders
topic.prefix=koreshok
plugin.name=pgoutput
slot.name=koreshok_cdc
slot.failover=true
publication.name=koreshok_pub
publication.autocreate.mode=disabled
snapshot.mode=initialЕсли слот создаётся не Debezium, а вручную, флаг failover — пятый аргумент функции. Проверить результат можно сразу же в pg_replication_slots:
SELECT * FROM pg_create_logical_replication_slot(
'koreshok_cdc', 'pgoutput', false, false, true);
SELECT slot_name, failover FROM pg_replication_slots
WHERE slot_name = 'koreshok_cdc';- После изменения стартовых параметров перезапустите узлы и проверьте фактические значения через `pg_settings`.
- Заранее рассчитайте объём WAL по пиковой скорости и допустимой длительности отказа CDC; для небольшой базы учёта 20 ГБ обычно дают часы запаса.
- Держите публикацию под управлением DBA: автоматическое добавление всех таблиц я в production не разрешаю.
Проверка перед переключением: мой go/no-go
Сначала смотрю primary. Мне нужны правильный плагин, база, failover = true, отсутствие invalidation и разумный запас WAL. Поле safe_wal_size имеет смысл, когда задан конечный max_slot_wal_keep_size; при безлимитном значении оно может быть NULL.
SELECT slot_name, slot_type, plugin, database, active,
temporary, restart_lsn, confirmed_flush_lsn,
wal_status, safe_wal_size, failover, synced,
conflicting, invalidation_reason
FROM pg_replication_slots
WHERE slot_name IN ('koreshok_cdc', 'pg18_stby_1')
ORDER BY slot_name;На standby выполняю отдельную проверку — это адаптированный запрос из документации. Для koreshok_cdc должно получиться failover_ready = true. Пустой результат означает, что слот не синхронизирован; строка с synced = false означает одноимённый локальный слот, созданный кем-то вручную. Оба варианта — стоп-сигнал.
SELECT slot_name,
synced AND NOT temporary
AND invalidation_reason IS NULL AS failover_ready,
restart_lsn, confirmed_flush_lsn,
wal_status, safe_wal_size, failover
FROM pg_replication_slots
WHERE slot_name = 'koreshok_cdc';Для планового switchover я сначала останавливаю запись приложения, доставляю контрольное событие и ставлю CDC на паузу. На primary сохраняю confirmed_flush_lsn, например 0/7A31B9C0. Затем жду, пока синхронизированный слот standby достигнет этой позиции, а recovery воспроизведёт весь полученный WAL.
SELECT confirmed_flush_lsn >= '0/7A31B9C0'::pg_lsn AS slot_caught_up,
restart_lsn,
confirmed_flush_lsn
FROM pg_replication_slots
WHERE slot_name = 'koreshok_cdc'
AND synced
AND NOT temporary
AND invalidation_reason IS NULL;
SELECT pg_is_in_recovery(),
pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn(),
pg_wal_lsn_diff(pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn()) AS replay_bytes_behind;- Go: слот постоянный, синхронизированный, не инвалидирован и догнал зафиксированный LSN.
- Go: физический слот имеет сохранный WAL, а standby закончил replay контрольной записи.
- No-go: `invalidation_reason` не пустой (`wal_removed`, `rows_removed`, `wal_level_insufficient`, а в PostgreSQL 18 ещё и `idle_timeout`).
- No-go: `safe_wal_size` приближается к нулю либо физическая реплика отстаёт.
- No-go: неизвестна последняя подтверждённая клиентом позиция и нельзя проверить событие end-to-end.
Кейс переплётной мастерской «Кожаный корешок»
Пример условный, но собран из типовой практики. В мастерской 28 рабочих мест: мастера переплёта и тиснения, приёмка заказов, бухгалтерия и небольшой интернет-магазин блокнотов. Учёт заказов работает на PostgreSQL 18.6, база занимает около 14 ГБ. Два виртуальных узла по 4 vCPU и 16 ГБ RAM — primary и горячий standby — подрядчик по аналитике попросил не ради моды: его Debezium забирает изменения заказов в хранилище, где считаются сроки выполнения, загрузка мастеров и расход кожи. Нагрузка скромная: в среднем 3–5 изменений в секунду, в пике при массовой загрузке прайса — около 60, WAL до 1,5 МБ/с. Лимит max_slot_wal_keep_size = 20GB даёт несколько часов запаса даже при пике, алерт стоит на остаток 5 ГБ.
Честно скажу, для мастерской такого размера HA-пара — уже осознанный выбор, а не обязательный минимум. Если standby нет, failover-слоты не нужны: достаточно мониторинга единственного слота и регламента повторного snapshot. Но как только есть реплика и внешний потребитель CDC, риск становится реальным. Первая репетиция это показала: на standby уже лежал вручную созданный koreshok_cdc с плагином pgoutput, а synced оставался ложным и позиция отставала на 380 МБ. После тестового promotion Debezium не смог продолжить с сохранённого offset, подрядчику пришлось делать повторный snapshot базы примерно 40 минут, а в его отчётах на время появились задвоенные заказы.
Мы удалили конфликтующий слот на standby (локальный слот с synced = false удалить можно, синхронизированный — нельзя), пересоздали рабочий поток с failover = true в согласованное нерабочее окно и включили synchronized_standby_slots. В финальном испытании остановка выдачи CDC-событий заняла 40 секунд. Оба контрольных события — до и после promotion — дошли до хранилища подрядчика. Пропусков не было, но downstream повторно увидел 6 ранее обработанных событий. Это нормальная цена модели at-least-once: загрузчик подрядчика отбрасывает дубликаты по ключу события. Я обещаю непрерывность потока, но никогда не обещаю отсутствие повторов без проверки всей цепочки.
- До исправления: одноимённый локальный слот, `synced = false`, отставание 380 МБ, повторный snapshot у подрядчика.
- После исправления: failover-слот, физический защитный слот в `synchronized_standby_slots` и запас WAL 20 ГБ.
- Результат теста: пауза 40 секунд, ноль пропущенных контрольных событий, 6 безопасно дедуплицированных повторов.
- Организационно: в договоре с подрядчиком зафиксировали, кто ставит коннектор на паузу и кто подтверждает получение маркера.
Как я провожу само переключение
Контрольную таблицу создаю заранее и явно добавляю в публикацию. Маркер должен пройти весь путь — PostgreSQL, слот, Debezium, Kafka и целевой обработчик. Проверка одной SQL-строки на новом primary доказывает только работу базы.
CREATE SCHEMA IF NOT EXISTS ops;
CREATE TABLE IF NOT EXISTS ops.cdc_failover_probe (
token text PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
source_node text NOT NULL
);
ALTER PUBLICATION koreshok_pub ADD TABLE ops.cdc_failover_probe;
INSERT INTO ops.cdc_failover_probe(token, source_node)
VALUES ('switchover-2026-09-pre', 'pg-primary-1');Когда pre-маркер появился в koreshok.ops.cdc_failover_probe, блокирую запись приложения и ставлю коннектор на паузу. Убеждаюсь, что слот на primary стал неактивным, фиксирую его confirmed_flush_lsn и жду readiness на standby. Затем обязательно изолирую старый primary. Только после его остановки повышаю standby — иначе получу два сервера, считающих себя primary.
curl -X PUT http://connect-1:8083/connectors/koreshok-postgres/pause
pg_ctl -D /var/lib/postgresql/18/main stop -m fastSELECT pg_promote(wait => true, wait_seconds => 60);
SELECT NOT pg_is_in_recovery() AS is_primary;После переключения HA-адреса pg-rw.internal возобновляю коннектор, проверяю active = true и рост confirmed_flush_lsn. Затем записываю post-маркер уже на новом primary. Разрешение обычной записи приложению даю после появления маркера в конечной системе.
curl -X PUT http://connect-1:8083/connectors/koreshok-postgres/resumeINSERT INTO ops.cdc_failover_probe(token, source_node)
VALUES ('switchover-2026-09-post', 'pg-primary-2');
SELECT slot_name, active, confirmed_flush_lsn,
invalidation_reason, synced
FROM pg_replication_slots
WHERE slot_name = 'koreshok_cdc';- Заморозить прикладную запись.
- Доставить pre-маркер до конечного получателя.
- Приостановить CDC и дождаться синхронизации его LSN.
- Остановить или надёжно оградить старый primary.
- Повысить standby и переключить единый write-endpoint.
- Возобновить CDC, доставить post-маркер и только затем открыть запись.
PostgreSQL 17 или 18 и что делать, если кластером управляет Patroni
Failover-слоты, sync_replication_slots, synchronized_standby_slots и колонки failover/synced в pg_replication_slots появились в PostgreSQL 17. Поэтому схема из статьи целиком работает и на 17.x. В PostgreSQL 18 я пользуюсь дополнительно idle_replication_slot_timeout и причиной инвалидации idle_timeout. На PostgreSQL 16 и старше нативной синхронизации логических слотов нет: там позицию после failover сохраняет только внешний HA-менеджер или повторный snapshot.
Patroni умеет «постоянные» слоты: они описываются в ключе slots динамической конфигурации и сохраняются при switchover/failover. Для логических слотов Patroni с версии 3.2.0 копирует слот на реплики и продвигает его через pg_replication_slot_advance(), но сам предупреждает, что позиция на реплике может немного отставать от бывшего primary, — значит, повторы неизбежны. С версии 4.1.0 Patroni не трогает слоты, созданные с failover = true. Я выбираю один механизм на слот: на PostgreSQL 17+ — нативный failover-слот, а Patroni оставляю выбор лидера, fencing и физические слоты.
# фрагмент patronictl edit-config: физический слот для standby
slots:
pg18_stby_1:
type: physical
postgresql:
parameters:
hot_standby_feedback: on
sync_replication_slots: on
synchronized_standby_slots: pg18_stby_1- PostgreSQL 16 и ниже: нативных failover-слотов нет, планируйте повторный snapshot или слоты Patroni.
- PostgreSQL 17.x: failover-слоты, slotsync worker и `synchronized_standby_slots` уже есть.
- PostgreSQL 18.x: плюс `idle_replication_slot_timeout` и `idle_timeout` в `invalidation_reason`.
- Patroni: не объявляйте один и тот же логический слот и в `slots`, и с `failover = true`.
Что контролировать после теста и где остаётся риск
В мониторинг я вывожу не только lag. Проверяю отсутствие нужного слота, invalidation_reason, wal_status, safe_wal_size, возраст inactive_since, размер удерживаемого WAL и состояние физического receiver. В PostgreSQL 18 появился idle_replication_slot_timeout (в 17 его нет); его значение по умолчанию — ноль, то есть механизм выключен. Инвалидация по нему выполняется на checkpoint. Синхронизированные слоты на standby (synced = true) от такого timeout освобождены, но неактивный рабочий слот на primary при включённом ограничении может получить idle_timeout во время длительной остановки CDC.
pg_sync_replication_slots() я оставляю лаборатории. Документация прямо называет функцию средством тестирования и отладки: у неё нет циклических повторов, а при работающем slotsync worker вызвать её нельзя. В production мне нужна постоянная фоновая синхронизация. Также не стоит бесконечно увеличивать max_slot_wal_keep_size: так можно превратить отказ CDC в заполнение диска. Сначала измерьте пиковый WAL, затем выберите допустимое окно и настройте ранние алерты.
При аварийном failover нельзя заранее остановить запись и сравнить неподвижные LSN. synchronized_standby_slots существенно снижает риск разрыва, потому что CDC не получает изменения раньше физической реплики, но остаются fencing, клиентский offset и повторная доставка. Поэтому мой окончательный критерий прост: слот прошёл регулярное реальное promotion-испытание, а downstream идемпотентен. Всё остальное — полезные, но недостаточные зелёные лампочки.
- Алертовать `invalidation_reason IS NOT NULL` немедленно.
- Предупреждать при остатке `safe_wal_size` меньше расчётного часа пикового WAL.
- Следить за разницей receive/replay LSN и состоянием физического слота.
- Проводить контролируемый switchover не реже раза в квартал.
- Хранить идентификатор контрольного события и результат end-to-end проверки.
Частые вопросы
Достаточно ли увидеть `synced = true` на standby?
Нет. Дополнительно нужны постоянный слот, отсутствие `invalidation_reason`, доступный WAL, достижение контрольного LSN и успешное end-to-end событие после promotion.
Должны ли LSN primary и standby всегда совпадать?
Нет, фоновая синхронизация асинхронна. Для планового switchover я приостанавливаю CDC и жду, пока standby достигнет зафиксированного `confirmed_flush_lsn`.
Гарантирует ли схема нулевую потерю при аварии?
Она защищает от ухода CDC дальше WAL кандидата, если правильно настроен `synchronized_standby_slots`. Но общая гарантия зависит от fencing, фактически полученного WAL, клиентского offset и идемпотентности downstream; повторы возможны.
Можно ли периодически запускать `pg_sync_replication_slots()` по cron?
Я так не делаю. Функция предназначена преимущественно для тестирования и не работает параллельно с активным slotsync worker. В production включаю `sync_replication_slots = on`.
Решит ли эту задачу Patroni или другой HA-менеджер?
HA-менеджер автоматизирует выбор и promotion узла, но сам факт promotion не доказывает готовность CDC. Patroni умеет постоянные слоты, однако с версии 4.1.0 не вмешивается в слоты с `failover = true`. Проверку failover-слота и fencing нужно включить в runbook или pre-promote gate.
Что будет с CDC, если слот не синхронизировался на standby?
После promotion слота на новом primary нет или он чужой. Коннектор либо падает, либо создаёт новый слот с текущей позиции, и изменения между последним offset и созданием слота теряются. Выход — повторный snapshot.
Работает ли это на PostgreSQL 17?
Да, failover-слоты, `sync_replication_slots` и `synchronized_standby_slots` появились в 17. В 18 добавились `idle_replication_slot_timeout` и причина инвалидации `idle_timeout`.
Нужна ли такая схема мастерской на 28 рабочих мест?
Только если есть standby и внешний потребитель CDC, например аналитика у подрядчика. Без реплики достаточно мониторинга слота и регламента повторного snapshot.
Источники
- PostgreSQL 18 Documentation — Replication Slot Synchronization — Раздел 47.2.3 «Replication Slot Synchronization», PostgreSQL 18: https://www.postgresql.org/docs/18/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS-SYNCHRONIZATION
- PostgreSQL 18 Documentation — Logical Replication Failover — Раздел 29.3 «Logical Replication Failover», включая официальный запрос failover_ready: https://www.postgresql.org/docs/18/logical-replication-failover.html
- PostgreSQL 18 Documentation — Replication Configuration — Раздел 19.6 «Replication»: synchronized_standby_slots, primary_conninfo, primary_slot_name, sync_replication_slots, max_slot_wal_keep_size и idle_replication_slot_timeout: https://www.postgresql.org/docs/18/runtime-config-replication.html
- PostgreSQL 18 Documentation — pg_replication_slots — Раздел 53.20, поля failover, synced, wal_status, safe_wal_size, conflicting и invalidation_reason: https://www.postgresql.org/docs/18/view-pg-replication-slots.html
- PostgreSQL 18 Documentation — System Administration Functions — Раздел 9.28, функции pg_promote(), pg_create_logical_replication_slot() и pg_sync_replication_slots(): https://www.postgresql.org/docs/18/functions-admin.html
- Debezium Documentation — PostgreSQL Connector — Стабильная документация коннектора PostgreSQL, параметры slot.failover, slot.name, publication.name и хранение LSN offset: https://debezium.io/documentation/reference/stable/connectors/postgresql.html
- PostgreSQL 17 Documentation — Logical Decoding Concepts — Раздел о синхронизации слотов в PostgreSQL 17, где впервые появились failover-слоты: https://www.postgresql.org/docs/17/logicaldecoding-explanation.html
- PostgreSQL Versioning Policy — Актуальные минорные версии веток 17 и 18 и сроки поддержки: https://www.postgresql.org/support/versioning/
- Patroni Release Notes — Failover логических слотов с 3.2.0 и отказ от вмешательства в слоты с failover=true в 4.1.0: https://patroni.readthedocs.io/en/latest/releases.html
