АйТи Фреш
Главная / Статьи / 1С и базы данных
1С и базы данных

Как доказать, что CDC продолжит читать слот после переключения PostgreSQL 18

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~16 мин чтения
Как доказать, что CDC продолжит читать слот после переключения PostgreSQL 18
Иллюстрация к статье «Как доказать, что 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. Именно остановка потребителя на короткое время превращает движущуюся цель в проверяемую точку.

Не принимайте решение по одному полю `synced`. Оно доказывает происхождение слота, но не состояние WAL, не достижение нужного LSN и не работоспособность маршрута от CDC до нового primary.

Конфигурация, которую я выбираю

На 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';
Если физический слот из `synchronized_standby_slots` отсутствует или инвалидирован, логический потребитель остановится. Это осознанная плата за защиту позиции, поэтому состояние физического слота должно участвовать в мониторинге.
Как доказать, что CDC продолжит читать слот после переключения PostgreSQL 18 — схема
Схема к статье. Открыть схему в полном размере

Проверка перед переключением: мой 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;
Во время непрерывной работы LSN primary и standby не обязаны совпадать до последнего байта: slotsync асинхронен. Для планового переключения я ставлю CDC на паузу и жду совпадения контрольной позиции. Тут спешка не окупается.
Памятка: Проверка перед переключением: мой go/no-go — схема
Памятка: Проверка перед переключением: мой go/no-go. Открыть схему в полном размере

Кейс переплётной мастерской «Кожаный корешок»

Пример условный, но собран из типовой практики. В мастерской 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: загрузчик подрядчика отбрасывает дубликаты по ключу события. Я обещаю непрерывность потока, но никогда не обещаю отсутствие повторов без проверки всей цепочки.

Название «Кожаный корешок» условное и не раскрывает реального клиента. Если у вас нет standby, не заводите его ради этой статьи: сначала посчитайте, сколько стоит час простоя аналитики и повторный snapshot, и сравните с ценой второго сервера.

Как я провожу само переключение

Контрольную таблицу создаю заранее и явно добавляю в публикацию. Маркер должен пройти весь путь — 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 fast
SELECT 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/resume
INSERT 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';
После promotion поле `synced` может остаться истинным, но на primary оно уже не является критерием готовности. Смотрите на выход из recovery, отсутствие invalidation, подключение потребителя и продвижение `confirmed_flush_lsn`.
Порядок действий: Как я провожу само переключение — схема
Порядок действий: Как я провожу само переключение. Открыть схему в полном размере

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
Имена физических слотов в Patroni обычно совпадают с именами узлов. Прежде чем прописывать `synchronized_standby_slots`, сверьте фактические имена в `pg_replication_slots` на лидере, иначе логический поток встанет сразу после reload.

Что контролировать после теста и где остаётся риск

В мониторинг я вывожу не только 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 идемпотентен. Всё остальное — полезные, но недостаточные зелёные лампочки.

На микросекундную разницу LSN во время обычной работы можно не реагировать. На invalidation, исчезновение слота, исчерпание WAL и отсутствие тестового события закрывать глаза нельзя.
Порядок действий: Что контролировать после теста и где остаётся риск — схема
Порядок действий: Что контролировать после теста и где остаётся риск. Открыть схему в полном размере

Частые вопросы

Достаточно ли увидеть `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.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи