Резервный PostgreSQL уже принимает запросы. Приложение переключено, health-check зелёный, пользователи оформляют заказы. А на подписчике логической репликации, который строит витрину для аналитики или кормит соседний сервис, тишина: новые строки не появляются. Вы поменяли адрес в строке подключения, но подписка не ожила, или ожила и сообщает, что нужного слота нет. Сменить адрес оказалось недостаточно.
Эта история — не про ошибку конфигурации в одной строке. Она показывает, что у переключения есть два независимых результата: узел начал обслуживать клиентов, и поток изменений продолжился с правильного места. Первое проверяется запросом от приложения. Второе требует отдельного разговора о слотах, и именно ему посвящена статья.
Оговорка о версиях сразу, потому что она определяет всё остальное. Способ подготовки слотов на резервном узле, о котором пойдёт речь, описан в документации PostgreSQL 18 (раздел о failover логической репликации). Не стоит считать, что та же функция есть в вашей установке: сначала выясните версию сервера и откройте документацию именно для неё. Если версия старше той, где эта возможность описана, переносить на неё рассуждения про синхронизацию слотов нельзя — придётся планировать иначе, и об этом будет отдельный абзац ниже.
Два разных «переключилось»
Физическая репликация и логическая решают разные задачи, и это хорошо видно в момент аварии. Физический standby повторяет журнал предзаписи целиком: он держит байт-в-байт копию кластера и после повышения становится новым primary. Для приложения этого достаточно: таблицы те же, данные те же, меняется только адрес.
Логический подписчик устроен иначе. Он не копия кластера, а самостоятельная база, которая получает поток изменений по таблицам, перечисленным в публикации. Он подключается к publisher и говорит: продолжай с того места, где я остановился. Кто помнит это место? Не сам подписчик в полном объёме, а слот на стороне publisher. Именно там лежит договорённость «эти изменения ещё не подтверждены, не выбрасывай журнал».
Отсюда прямое следствие. Когда publisher умирает, а подписчик пытается подключиться к новому primary, ему нужен тот же слот с той же позицией. Физический standby, который повторяет журнал, сам по себе слота-то, как правило, не содержит: слоты не входят в данные, которые реплицируются как обычные изменения таблиц. Поэтому наивный сценарий «подняли standby, перенаправили подписку» упирается в отсутствие слота, и подписчику остаётся либо ошибка, либо повторная инициализация данных с нуля.
Документация формулирует базовое условие просто: чтобы логическая репликация продолжилась после отказа publisher, нужен соответствующий physical standby. Но этого условия мало. Дальше идёт вопрос о том, что именно должно быть на этом standby.
Что удерживает логический слот
Полезно разобрать слот как объект с тремя обязанностями, потому что от них зависят последствия выбора.
- Позиция подтверждения. Слот помнит, до какого места подписчик подтвердил получение изменений. Всё, что позже, должно быть доступно для повторной отправки.
- Удержание журнала. Пока позиция не продвинулась, publisher не может удалить соответствующие сегменты WAL. Поэтому забытый слот растёт в диске, как нерасплатившийся долг.
- Состояние декодирования. Чтобы превратить журнал в понятные логические изменения, serverу нужен согласованный взгляд на каталог. Слот фиксирует точку, с которой такое декодирование корректно.
Теперь представьте, что слот на новом primary создан заново командой вроде «создай слот с тем же именем». Имя совпадёт, но позиция будет другой: слот начнётся с текущего конца журнала. Транзакции, которые прошли между последним подтверждением подписчика и моментом аварии, окажутся в пропасти. Подписка внешне заработает, а данные разойдутся тихо. Это самый неприятный сценарий, потому что он не выглядит как поломка.
Поэтому правило звучит так: слот на резервном узле ценен только тогда, когда он унаследовал позицию настоящего слота, а не только имя. Всё остальное — самообман, хотя иногда и работающий на тестовой нагрузке.
Как слоты попадают на резервный узел
В описанной документации версии 18 путь такой: логические слоты, помеченные как пригодные для failover, могут синхронизироваться на standby. Сам флаг задаётся на стороне подписки: документация описывает параметр failover=true для subscription. Идея в том, что подписчик при создании сообщает publisher: этот слот должен иметь двойника на резервных узлах.
Вот как это выглядит в виде примера подписки. Строка подключения и имена здесь условные:
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=pg-primary dbname=shop user=repl'
PUBLICATION pub_orders
WITH (failover = true);Обратите внимание на то, что флаг относится к слоту, который подписка создаёт на publisher. Если подписка уже существует и была создана без него, простое желание «включить failover» не исправит ситуацию задним числом: нужно сверить с документацией вашей версии, как изменить свойство существующей подписки, и учесть, что для этого может потребоваться временно остановить её. Не придумывайте процедуру по аналогии: проверьте её по документации установленной версии.
Второй момент — сама синхронизация. Документация подчёркивает, что она асинхронна. Это значит, что копия слота на standby может отставать от оригинала. Слот на primary продвинулся вперёд вслед за подписчиком, а резервная копия ещё не догнала. Если primary упадёт именно в этот момент, на standby окажется слот, который чуть старее настоящего. Для логической репликации это на самом деле не катастрофа: старая позиция означает, что часть изменений придёт повторно, а повтор отсекается механизмами подписчика. Опасна обратная ситуация, когда слот на standby опередил подписчика, и об этом мы скажем в разделе про момент гонки.
Практический вывод из асинхронности такой: перед плановым failover нужно убедиться, что синхронизация завершилась. Не «обычно успевает», а проверено на конкретном состоянии. Документация прямо требует проверять это до переключения.
Что смотреть на standby
Проверка сводится к осмотру каталога слотов на резервном узле. Представление pg_replication_slots показывает слоты и их свойства. Для нашей задачи интересны такие вопросы: есть ли на standby слот с именем, которое использует подписка; помечен ли он как пригодный для failover; синхронизирован ли он и не помечен ли как временный или недействительный. Точные имена столбцов и их значения берите из справки по вашей версии, а не из памяти: набор полей менялся от релиза к релизу, и угадывание здесь обходится дорого.
SELECT slot_name, slot_type, active, restart_lsn, confirmed_flush_lsn
FROM pg_replication_slots;Этот запрос полезно выполнить и на primary, и на standby, а затем сравнить позиции. Если позиция подтверждения на резервном узле заметно отстаёт или слота нет вовсе, переключать ещё рано. Если же слот есть, а его позиция не меньше, чем позиция подтверждения подписчика, то условие продолжения выполняется. Заметьте, что формулировка «не меньше» относится именно к тому, что подписчику ничего не будет потеряно; в следующем разделе мы разберём, почему критично и обратное направление.
Условие, которое легко пропустить: standby должен опережать подписчика
Теперь самый тонкий пункт. Документация связывает успешное продолжение с тем, что standby опережает subscriber, и упоминает параметр synchronized_standby_slots. Смысл можно объяснить на одной картинке.
Логический подписчик получает изменения от primary по мере их появления. Но primary может отправить подписчику транзакцию, которая ещё не доехала до физического standby. Подписчик её применил и подтвердил. Если сейчас primary погибнет, а standby будет повышен, то на новом primary этой транзакции не окажется вовсе — а подписчик её уже видел. Получается, что подписчик «знает будущее», которого у нового primary нет. Продолжить поток корректно невозможно: либо новый primary породит другие изменения под теми же позициями, либо подписчик содержит данные, которых нигде больше нет.
Чтобы такого не случалось, логическая отправка должна ждать физического standby: publisher не отдаёт подписчику изменения, пока они не гарантированно получены резервным узлом. Именно эту роль в описании документации играет synchronized_standby_slots: в нём перечисляются физические слоты standby, которых логические walsender должны дожидаться. Название параметра говорит само за себя, но детали его применения — какие слоты перечислять, что произойдёт при недоступности standby — проверяйте в документации своей версии.
У такого ожидания есть цена, и честно сказать о ней нужно заранее. Если резервный узел недоступен, а он перечислен как обязательный, логическая репликация может остановиться и ждать его. Это не баг, а плата за согласованность: вы выбираете между тем, чтобы подписчик мог отстать от аварии, и тем, чтобы он не мог опередить резерв. Решение зависит от того, что для вас хуже — задержка витрины или расхождение данных после аварии.
Таблица решений: что даёт каждое условие
| Условие | Если выполнено | Если нарушено |
|---|---|---|
| Есть физический standby | Есть узел, который может стать новым primary с теми же данными | Подписчику не к чему переключаться, остаётся восстановление с нуля |
| Подписка создана с failover=true | Слот помечен как требующий копии на standby | На резервном узле нет двойника слота, подписка не найдёт позицию |
| Синхронизация слота завершена | Позиция на standby известна и близка к актуальной | Слот отсутствует или сильно отстаёт, часть изменений придётся получать заново или она потеряна |
| Standby опережает подписчика | Подписчик не видел того, чего нет у нового primary | Данные подписчика и нового primary расходятся |
Таблица показывает полезную вещь: четыре условия независимы, и зелёный статус одного ничего не говорит о трёх остальных. Наличие standby и флага failover не гарантирует ни автоматического переключения, ни отсутствия потерь. Это просто предпосылки, которые ещё нужно подтвердить наблюдением.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Когда primary уже новый, а слот ещё не готов
Вернёмся к исходной сцене. Новый primary работает, приложение подключено, подписчик молчит. Что делать, если вы оказались в этой точке?
Первое — не спешить пересоздавать слот. Соблазн велик: команда создания слота выполняется мгновенно, подписка после неё, возможно, даже подключится. Но, как мы выяснили, свежий слот начнёт с нынешнего конца журнала, и разрыв между последним подтверждением и аварией превратится в потерянные данные. Прежде чем что-либо создавать, ответьте на вопрос: есть ли на новом primary слот с нужным именем, и какова его позиция?
Возможны три ситуации.
- Слот есть и готов. Его позиция согласуется с подписчиком. Остаётся направить подписку на новый адрес и убедиться, что подключение прошло. Это идеальный исход, ради которого всё готовилось.
- Слот есть, но синхронизация не завершилась к моменту аварии. Он может быть недействителен или не пригоден к использованию. Дальнейшие действия зависят от версии и от того, что показывает каталог слотов. Если слот непригоден, то продолжение потока с гарантией невозможно, и честная дорога — повторная синхронизация данных подписчика.
- Слота нет. Это либо подписка создавалась без failover, либо синхронизация вообще не работала. Вариант тот же: поток с точной позиции восстановить нельзя.
Во втором и третьем случаях вопрос «как продолжить» заменяется вопросом «как восстановить согласованность». Для небольших таблиц проще всего заново выполнить начальную копию данных для подписки. Для больших это долгая операция, и именно поэтому подготовка слотов заранее экономит часы после аварии. Если вы в этой ситуации, обновите подписку так, чтобы она заново скопировала данные, и сверьте результат на тестовой копии — а не в первый раз на боевом подписчике.
А если версия старше
Для установок, где описанной синхронизации слотов нет, универсальной готовой команды документация 18 дать не может, и придумывать замену по аналогии нельзя. Реалистичные варианты такие: проверить в документации вашей версии, что она предлагает для логической репликации при отказе publisher; рассмотреть обновление до версии, где возможность поддерживается; либо заранее принять сценарий «после аварии подписчик заново инициализируется» и учитывать это в расчёте времени восстановления. Любой из вариантов честнее, чем надежда на то, что слот как-нибудь окажется на месте.
Контролируемое переключение: порядок и сверка
Плановый failover — единственный случай, когда можно выбирать момент, и грех этим не воспользоваться. Вот последовательность, в которой каждый шаг снимает один из рисков, описанных выше. Она представляет собой наше рабочее предложение по организации проверки, а не пошаговый документ из справочника.
- Зафиксировать состояние до начала. Выпишите с primary позиции слотов и значение
confirmed_flush_lsn, а на подписчике — последнюю применённую позицию. Это точка отсчёта для сверки. - Проверить синхронизацию на standby. Убедитесь, что слот присутствует, помечен пригодным для failover и догнал primary. Асинхронность означает, что одной проверки в начале недостаточно: повторите её ближе к моменту переключения.
- Убедиться, что standby опережает подписчика. Сравните позицию получения журнала на резервном узле с позицией подписчика. Если настроен
synchronized_standby_slots, такое опережение обеспечивается устройством отправки, но наблюдение никто не отменяет. - Остановить запись. Для контролируемого переключения разумно на время прекратить запись на primary, чтобы позиции перестали двигаться и сверка имела смысл.
- Повысить standby. Сделайте его новым primary способом, принятым в вашей топологии.
- Перенаправить подписку. Измените строку подключения подписки на адрес нового primary.
- Дождаться продолжения потока. Не считайте дело сделанным, пока подписчик реально не принял изменения.
Шаг шестой стоит комментария. Изменение адреса у существующей подписки — обычная операция, но после неё процесс-приёмник перезапускается и подключается заново. Сообщения в журнале подписчика в эти секунды говорят больше любого статуса: «слот не существует» означает проблему подготовки, «подключение отклонено» — проблему доступа или адреса, а успешный запуск процесса применения — начало нормальной работы.
Сверка данных: как убедиться, что потерь нет
Продолжение потока ещё не доказывает целостности. Подписчик мог пропустить диапазон при неудачной подготовке, и это не проявится сразу. Поэтому после переключения нужна сверка, и она должна быть выстроена от фактов, а не от ощущений.
Практичные приёмы, не требующие специальных инструментов:
- сравнить количество строк в ключевых таблицах на новом primary и на подписчике после того, как запись остановлена и поток дошёл до конца;
- сравнить агрегаты — сумму по числовому столбцу, максимальную метку времени, максимальный идентификатор;
- для таблиц с монотонным ключом проверить отсутствие пропусков в диапазоне, созданном незадолго до переключения;
- выбрать несколько строк, изменённых прямо перед аварией, и сравнить их содержимое целиком.
Если запись продолжается, точные сравнения невозможны: подписчик всегда чуть позади. Тогда сравнивайте данные, которые заведомо старше текущего отставания. Измерить отставание можно по разнице позиций на publisher и подписчике.
Обязательно проверьте и последовательности (sequences). Логическая репликация не переносит их значения так, как переносит содержимое таблиц, поэтому подписчик может отстать по счётчикам; если подписчик когда-нибудь станет рабочей базой, эту разницу придётся выровнять. Для сценария «подписчик только читает» это не критично, но знать об особенности нужно. Если не уверены в поведении своей версии, свяжитесь с документацией перед тем, как полагаться на перенос.
Ресурсы под такую схему
Схема из трёх узлов — publisher, physical standby и подписчик — требует трёх разных машин, и экономить на этом нельзя: если standby и primary живут на одном диске или одной виртуальной машине, отказоустойчивости у вас нет. Отдельные VPS сами по себе не гарантируют, что узлы размещены на разном оборудовании, поэтому вопрос размещения стоит уточнять отдельно, а не предполагать.
Теперь о нагрузке. Резервный узел постоянно принимает журнал и должен успевать его применять, а publisher держит журнал для отстающих слотов. Дисковая подсистема здесь важнее, чем кажется: на VPS MAATRIX используются NVMe-диски и процессоры AMD EPYC, с root-доступом для настройки PostgreSQL под себя. Для учебного стенда из трёх машин хватит самых младших конфигураций из каталога, например Ba с 1 vCPU, 2 ГБ RAM и 40 ГБ NVMe, а для рабочей нагрузки размер нужно подбирать по замерам: перечисленные ресурсы не являются обещанием ёмкости. Заранее оставляйте запас дискового пространства на publisher под WAL, который удерживают слоты: если подписчик отключится надолго, объём вырастет.
Общие вопросы настройки репликации баз данных на VPS разобраны в статье про репликацию PostgreSQL и MySQL; здесь мы сознательно остановились только на слотах и их судьбе при переключении.
Что считать успехом
Вернёмся к началу. Зелёный статус нового primary — это ответ на вопрос «работает ли база». Но вы переключали не просто базу, а цепочку с подписчиком на конце, и проверять нужно её конец. Поэтому критерий готовности звучит не как «primary доступен» и даже не как «подписка подключилась», а проще: на новом primary появилась свежая запись, и спустя короткое время та же запись видна на подписчике.
Сделайте это намеренно. Вставьте в таблицу, входящую в публикацию, строку с заметной меткой — например, текстом с временем переключения — и найдите её на подписчике. Если строка прошла по новой цепочке, то слот сохранил позицию, подписка продолжила поток, а сверка старых данных подтвердила отсутствие провалов. Только после этой записи можно считать переключение завершённым: до неё у вас есть лишь предположение, что всё должно было сработать.