Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Логическая репликация PostgreSQL и миграция без простоя

Логическая репликация в PostgreSQL: публикации, подписки и миграция БД

Чем логическая репликация отличается от физической: publication/subscription, выборочные таблицы, миграция на новый сервер без простоя и ограничения механизма.

Прямой ответ

Логическая репликация передаёт изменения данных (строки через декодирование WAL), а не байты страниц — отсюда её отличия от физической (streaming): подписчик может быть другой версией Postgres, реплицируются выбранные таблицы (не весь инстанс), на подписчике можно писать в свои таблицы (это не hot standby), но нет копии «всего» и PITR она не даёт (бэкапы — отдельно). Механика: на источнике ALTER SYSTEM SET wal_level = logical (рестарт) + CREATE PUBLICATION pub_orders FOR TABLE orders, order_items (или ALL TABLES), на приёмнике CREATE SUBSCRIPTION sub_orders CONNECTION 'host=... dbname=...' PUBLICATION pub_orders — подписка сама делает начальную копию таблиц (snapshot) и дальше догоняет по WAL; слот репликации на источнике держит WAL до подтверждения (отставший/мёртвый подписчик раздувает диск источника — мониторинг pg_replication_slots обязателен — xmin-якоря — логический слот держит xmin так же, как физический). Главный практический кейс — миграция/апгрейд без простоя: новый сервер (или новая мажорная версия) становится подписчиком старого, догоняет, затем короткий свитч записи (остановить запись на старом → дождаться полного догона (lag = 0) → переключить приложение → подписчик становится основной БД) — простой секунды вместо часов копирования. Ограничения, которые надо знать до старта: последовательности не реплицируются (после свитча значения sequence на новом нужно выставить вручную — иначе конфликт первичных ключей — самая частая авария таких миграций), DDL не передаётся (миграции схемы применяются на обоих — Alembic — порядок: сначала схема на подписчике, потом данные), уникальность/PK нужны для UPDATE/DELETE (без реплик-идентификации строки не найти — REPLICA IDENTITY), LARGE OBJECTS и некоторые типы не поддерживаются, конфликты на подписчике (данные уже есть) останавливают apply-воркер — чинить вручную.

Публикация и подписка: настройка и наблюдение

Шаги развёртывания: источник — wal_level = logical (только рестарт — планировать окно), max_replication_slots/max_wal_senders с запасом; публикация — на таблицу/набор/ALL TABLES (новые таблицы в ALL TABLES попадают автоматически — удобно для миграции, но «случайно опубликовали внутреннюю» — явный список безопаснее); REPLICA IDENTITY — default использует PK (нет PK — USING INDEX на уникальный или FULL — передаёт всю строку — тяжее, но работает без PK — для легаси-таблиц единственный путь); слот — создаётся подпиской автоматически (или вручную для контроля — pg_create_logical_replication_slot) — и требует присмотра: SELECT slot_name, active, restart_lsn, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag FROM pg_replication_slots — неактивный слот с растущим lag — диск источника в опасности (мёртвая подписка — удалять слот — «забытая подписка сожрала 200 ГБ WAL» — реальный сценарий). Подписчик: CREATE SUBSCRIPTION (копия+догон), состояние — pg_subscription (enabled) и pg_stat_subscription (latest_end_lsn, лаг по времени — главный индикатор «догнала ли»), ошибки apply — в логах Postgres (конфликт уникальности, отсутствующая колонка — apply останавливается, очередь изменений копится — реагировать быстро); управление: ALTER SUBSCRIPTION ... DISABLE/ENABLE (пауза/возобновление — например на время DDL на подписчике), REFRESH PUBLICATION (изменился состав публикации на источнике — подписка должна узнать — иначе новые таблицы не поедут), SKIP (lsn => ...) (пропустить проблемную транзакцию — крайняя мера с потерей изменения — осознанно и с записью в журнал). Мониторинг минимума: lag подписки (в байтах и секундах), активность слота, свободное место WAL-каталога, ошибки apply — в Prometheus через postgres_exporter (метрики) + алерты (дисциплина).

Миграция без простоя: пошаговый сценарий

Канонический план (апгрейд версии/переезд на новый хост): (1) подготовка нового сервера — та же схема (Alembic/pg_dump --schema-only — DDL не реплицируется, поэтому схема создаётся заранее и идентично — проверить версии расширений); (2) источник — wal_level logical + публикация (ALL TABLES или явный список) + слот; (3) подписка на новом — начальная копия (на больших БД — часы; трафик по сети/диску учитывать — лучше из бэкапа: базовый снимок через pg_dump/pg_basebackup + copy_data = false и старт с LSN — экономия часов на сотнях ГБ — продвинутый, но стандартный приём); (4) догон и проверка (lag ≈ 0, контрольные суммы по ключевым таблицам — count(*)/агрегаты по срезам — «данные сошлись» — до свитча); (5) свитч: приложение в режим только-чтение (или короткая пауза записи — feature flag/maintenance), дождаться lag = 0 точно, ALTER SEQUENCE ... RESTART WITH на новом для всех sequence (значение = max(id)+запас на источнике — иначе PK-конфликт сразу после свитча — шаг, который забывают чаще всего), отключить подписку, поднять приложение на новый адрес (конфиг — env-переключение — 12-factor); (6) обратная подписка (новый → старый) на время «окна отката» — если что-то пойдёт не так, откат на старый возможен с сохранением новых данных (симметричная подписка — страховка, которую делают зрелые команды); (7) проверка и только потом — удаление старого. Альтернативы по ситуации: pg_upgrade (та же машина, мажорный апгрейд — быстрее всех, но требует простоя и не переносит на другой хост — для малых БД лучший выбор), pg_dump/restore (просто и надёжно на малых объёмах — простой равен времени дампа — до десятков ГБ приемлемо), физическая репликация + promote (тот же мажор, другой хост — streaming — проще логической, но версии обязаны совпадать); логическая — когда версии разные, нужны выборочные таблицы или обратная совместимость на время свитча.

Практический сценарий: переезд с Postgres 13 на 16 в новое облако

Исходник: 240 ГБ, мажор 13 (EOL-давление), хост у старого провайдера; цель — 16 в managed-облаке, окно простоя согласовано 15 минут (бизнес работает круглосуточно). Прогон: (1) новая инстанс-16, схема — pg_dump --schema-only с 13 + ручная правка несовместимостей (пара устаревших типов — нашли на стейдже — «схема без сюрпризов» на проде недостижима без репетиции); (2) публикация ALL TABLES на 13, слот с max_slot_wal_keep_size = 100GB (защита диска источника — лучше инвалидация слота, чем лёгший прод); (3) подписка — snapshot 240 ГБ шёл 5 часов по сети (ночь, вне пика — планирование); догон — 40 минут; (4) 6 дней в режиме «подписка живёт» — lag в норме (<2 с), два сбоя сети — подписка восстановилась сама (apply продолжился с места); заодно нашли таблицу без PK (легаси-справочник) — UPDATE на ней падали на apply — добавили PK заранее (без репликации проблема бы вскрылась в ночь миграции — польза «долгого прогрева»); (5) свитч в согласованное окно: запись остановлена флагом (10:00:00), lag доведён до 0 (48 с), sequence выставлены по max(id)+10000 (скрипт генерировал ALTER'ы по всем sequence — проверка на стейдже обязательна), DNS/endpoint переключены, приложение поднято — 11 минут простоя (в бюджете 15); (6) обратная подписка 16 → 13 держалась 5 дней как страховка отката (не понадобилась; удалена после контрольной сверки — count/контрольные суммы по 20 ключевым таблицам совпали); (7) старый инстанс выключен, WAL-архив старого сохранён в бэкап-хранилище (3-2-1) на квартал. Итог: миграция в бюджете простоя, ноль потерянных транзакций (сверка подтвердила), версия 16; два урока в runbook: «sequence вручную» — в чек-лист обязательным шагом, «репетиция на стейдже с теми же данными» — сэкономила два потенциальных инцидента (схема и PK-less таблица).

FAQ

Логическая репликация против физической — что выбрать? Физическая (streaming) — точная копия инстанса: hot standby для чтения, HA/failover (Patroni-класс), PITR-основа, минимум настроек; требует одинаковой мажорной версии и копирует всё. Логическая — выборочные таблицы, разные версии, двусторонняя запись (подписчик не read-only), миграции/апгрейды и «отдать одну таблицу аналитикам»; не заменяет бэкап и HA-реплику. Типовая пара в проде: физическая — для HA/чтения, логическая — для интеграций/миграций.

Почему после свитча посыпались duplicate key? Sequence не реплицируются (логическая репликация передаёт данные строк, не состояние генераторов) — на подписчике sequence остались на значениях начального снапшота; после свитча первая же вставка берёт старый nextval и упирается в существующий id; лечение — на свитче выставить все sequence: SELECT setval('orders_id_seq', (SELECT max(id) FROM orders) + 1) (или ALTER SEQUENCE RESTART) — по всем таблицам (скрипт по pg_get_serial_sequence — генерируется автоматически); профилактика — пункт в чек-листе свитча + репетиция на стейдже.

Можно ли реплицировать в ClickHouse/другую СУБД? Напрямую подпиской — нет (подписчик обязан быть Postgres); через декодирование WAL — да: Debezium/логические слоты отдают изменения в Kafka/поток, оттуда — в любую систему (outbox-контекст — CDC как транспорт событий); для ClickHouse есть готовые коннекторы (MaterializedPostgreSQL-движок читает логическую репликацию сам — «таблицы Postgres как есть в ClickHouse» — удобно для аналитики — ClickHouse); выбор по задаче: одна-две таблицы в аналитику — MaterializedPostgreSQL/ETL (пайплайн), событийная шина для многих потребителей — CDC → Kafka.

Что делать, если apply-воркер встал с ошибкой? Диагностика: логи подписчика (типичное — конфликт уникальности «данные уже есть», отсутствующая колонка/таблица (DDL не приехал), несовместимость типа); действия по причине: конфликт строки — удалить/исправить конфликтную строку на подписчике (осознанно: чья версия правильная) и ENABLE подписку; пропущенный DDL — применить его (порядок: схема до данных — Alembic) и возобновить; «изменение невозможно применить в принципе» — ALTER SUBSCRIPTION ... SKIP (lsn) (пропуск транзакции — потеря изменения фиксируется в журнале/тикете — не молча); пока apply стоит, lag растёт и слот на источнике копит WAL — время реакции ограничено диском — алерт на «apply остановлен» должен быть page-уровня.

Источники: документация PostgreSQL (logical replication, CREATE PUBLICATION, restrictions) — ссылки в шапке.