Learning Platform
Урок 14.04 · 24 мин
Продвинутый
Logical ReplicationPublicationSubscriptionLogical DecodingWAL

В прошлом уроке мы разбирали physical replication — клонирование на уровне байтов 8 KiB страниц. Это надёжно и работает «всё или ничего», но имеет ограничения:

  • Версии Postgres должны совпадать → невозможен rolling major upgrade без downtime.
  • Реплицируется вся БД → невозможно поднять replica только для пары таблиц.
  • Архитектура должна совпадать → миграция с x86 на ARM ферму = пересборка.
  • Replica всегда read-only → невозможен multi-master или гибрид «replica для аналитики + локальные write».

В Postgres 10 появилось решение этих ограничений — logical replication. Идея: вместо передачи байт страниц декодировать WAL на primary в логические изменения («строка с id=42 в orders получила status=‘paid’») и отправлять эти changes на subscriber, где они применяются как обычные INSERT/UPDATE/DELETE.

Logical decoding: WAL → row changes

В основе logical replication лежит механизм logical decoding. Это процесс, который читает WAL и преобразует физические записи (а именно — записи об изменениях heap, с информацией о tuple) в семантические события: «в таблицу X добавлена строка с этими значениями», «в таблице Y строка с такими-то primary key обновлена в такие-то значения».

Для этого нужно:

  1. wal_level = logical на primary. Это поднимает на уровень WAL такие записи, чтобы по ним можно было восстановить полные данные кортежа. На уровне replica (для physical) этого мало.
  2. REPLICA IDENTITY на таблицах. Это говорит Postgres, какие колонки писать в WAL, чтобы можно было идентифицировать строку при UPDATE/DELETE. По умолчанию — primary key. Если PK нет — нужно сделать ALTER TABLE ... REPLICA IDENTITY FULL (пишет в WAL все старые значения колонок).
  3. Logical replication slot — slot с типом logical (vs physical из прошлого урока). Хранит позицию decoding и состояние плагина.
  4. Output plugin — для встроенной logical replication это pgoutput. Для CDC-pipelines в Kafka — wal2json, decoderbufs, pgoutput + Debezium.

Publication и Subscription

Архитектура logical replication построена вокруг двух объектов:

  • Publication на publisher (старший сервер): набор таблиц + список операций (INSERT/UPDATE/DELETE/TRUNCATE), которые отдаются «вовне».
  • Subscription на subscriber: подключение к publisher + ссылка на publication. Подписавшись, subscriber получает initial copy и затем стримит inkremental changes.
Logical replication: топология

На publisher — logical slot + publication, перечисляющая 'что я отдаю'. На subscriber — subscription, перечисляющая 'что я хочу'. WAL декодируется в pgoutput-формат и применяется как обычные INSERT/UPDATE/DELETE на subscriber.

PUBLISHERwal_level = logical
CREATE PUBLICATION orders_pub FOR TABLE orders, itemsчто отдаём
logical slotrestart_lsn + plugin state
walsender (logical mode)вызывает pgoutput, декодирует WAL → tuples
pgoutputchanges →
TCPпо replication protocol
SUBSCRIBERпринимает changes как обычный backend
CREATE SUBSCRIPTION orders_sub CONNECTION '...' PUBLICATION orders_pubчто подписаны
apply workerвыполняет INSERT/UPDATE/DELETE на локальной БД
tablesync workersinitial copy при первой подписке
Не передаются: DDL (CREATE TABLE, ALTER), sequences, large objects, TRUNCATE до версии 11это нужно делать вручную или через extensions

Минимальный пример настройки (на реальной БД):

-- на publisher
ALTER SYSTEM SET wal_level = 'logical';  -- requires restart
CREATE PUBLICATION orders_pub FOR TABLE orders, order_items;
-- на subscriber
CREATE TABLE orders (...);          -- DDL руками!
CREATE TABLE order_items (...);
CREATE SUBSCRIPTION orders_sub
  CONNECTION 'host=publisher dbname=app user=replicator'
  PUBLICATION orders_pub;

CREATE SUBSCRIPTION делает три вещи: создаёт logical slot на publisher, запускает tablesync workers для initial COPY каждой таблицы, и затем запускает apply worker для incremental changes.

Синтаксис создания publication на реальной БД. В pglite logical replication не работает (нет walsender), но синтаксис разбирается. CREATE PUBLICATION — DDL операция, делается на publisher.

PostgreSQL

Просмотр published tables — какие таблицы попадают в publication. Полезно при отладке: 'почему изменение в таблице X не реплицируется?' Часто ответ — она просто не в publication.

PostgreSQL

Что НЕ реплицируется

Главное, что нужно знать заранее, — logical replication не передаёт всё:

  • DDL (CREATE/ALTER/DROP TABLE, индексы, типы). Если на publisher добавил колонку, на subscriber её нет — следующий INSERT с лишней колонкой просто упадёт с ошибкой. Сначала ALTER на subscriber (тогда лишняя колонка примет NULL), потом на publisher.
  • Sequences. Значение serial/identity колонок реплицируется, но сам sequence не движется на subscriber. После switchover придётся вручную setval() каждого sequence.
  • Large objects (LO). Реплицируются только если внутри обычной таблицы как bytea. lo_* API — нет.
  • TRUNCATE реплицируется с Postgres 11+, но требует publish_via_partition_root для партиционированных таблиц.

Это и есть главная боль logical-репликации в продакшене: рассогласование схем. Тулинг типа pglogical_create_subscriber или Bucardo раньше делал это автоматически; в современном Postgres большая часть схемы — работа человека или скриптов миграций.

Когда использовать logical вместо physical

Несколько ясных сценариев:

  1. Major version upgrade без downtime. Поднимаем Postgres 16 рядом с 14, делаем 14 publisher, 16 subscriber, ждём catch-up, переключаем трафик. Это единственный способ обновить мажорную версию без часа downtime на dump/restore.
  2. Cross-cloud / cross-region replication. Часто вместе с network compression или через managed CDC-сервисы.
  3. Selective replication. Хочу реплицировать только 5 таблиц из 200 — physical не умеет, logical делает легко.
  4. Multi-source consolidation. Десять источников (shard_001..shard_010), один analytics-target — собирает все subscriptions. Physical не поддерживает (replica может быть только от одного primary).
  5. CDC в Kafka/event-bus. Использует тот же logical decoding с другим output plugin (Debezium, wal2json).
  6. Active-active / multi-master. Через extensions (pglogical, BDR, EDB Postgres Distributed). С нативной logical replication multi-master не делается из-за conflict resolution.

Ограничения и подводные камни

  • Sequences не реплицируются → после failover нужны setval(). Иначе следующий INSERT попытается использовать существующий ID и упадёт на unique violation.
  • Conflict handling = stop. Если на subscriber UPDATE строки, которой нет (например, DELETE на subscriber сделан вручную), apply worker остановится с ERROR. Subscription стопится до ручного вмешательства (ALTER SUBSCRIPTION ... DISABLE, разрешить конфликт, ENABLE).
  • Replica identity для UPDATE/DELETE. Если у таблицы нет PK и не выставлен REPLICA IDENTITY FULL, UPDATE/DELETE не реплицируются с ошибкой «cannot update table without replica identity».
  • Initial copy блокирует другие операции на subscription. Большая таблица в initial copy = долгий tablesync.
  • WAL bloat на publisher. Logical slot держит WAL до confirmation. Если subscriber отстаёт — WAL копится. Это тот же риск, что и с physical slot.

REPLICA IDENTITY и почему он важен

При UPDATE на subscriber Postgres должен найти строку для обновления. Чтобы это сделать, в WAL должна быть информация, идентифицирующая строку: либо PK, либо unique constraint, либо все old values.

Уровни:

  • DEFAULT — пишется в WAL только PK. Требует PK на таблице.
  • USING INDEX <name> — какой-то конкретный unique index. Заменяет PK, если нет.
  • FULL — все old values. Безопасно, но дорого: каждый UPDATE/DELETE пишет полные дополнительные данные в WAL. Используется для таблиц без PK.
  • NOTHING — ничего. UPDATE/DELETE не реплицируются (только INSERT попадает в pgoutput).

Production-правило: у всех реплицируемых таблиц должен быть PK. Если PK нет — добавь, или поставь REPLICA IDENTITY FULL (с осознанием overhead).

Проверка REPLICA IDENTITY для таблиц текущей БД. На production важно убедиться, что у всех реплицируемых таблиц есть PK или выставлен FULL. Колонка relreplident: 'd'=default(PK), 'i'=USING INDEX, 'f'=FULL, 'n'=NOTHING.

PostgreSQL

Conflict resolution: что произойдёт, если apply упал

На subscriber могут возникать конфликты, нехарактерные для physical replication:

  • Конфликт по PK. Subscriber’у пришёл INSERT с id=42, но на subscriber id=42 уже есть. Apply падает с unique violation. Subscription остановилась.
  • Не нашлась строка для UPDATE/DELETE. Например, на subscriber кто-то вручную сделал DELETE этой строки. Apply падает.
  • Type mismatch. На publisher колонка integer, на subscriber тоже integer но с разным значением — обычно ок. Но если на publisher тип расширен до bigint, а на subscriber всё ещё integer и значение не помещается — падает.

Когда subscription остановилась, в pg_stat_subscription будет старый last_msg_receipt_time, в логах — текст ошибки. Разрешение конфликта обычно — руками: либо подправить subscriber-данные, либо ALTER SUBSCRIPTION ... SKIP (lsn = '0/...') — пропустить проблемную транзакцию.

В PG15+ появилось disable_on_error = true опция для subscription — апплай при ошибке автоматически отключается, чтобы не зацикливаться на retry. Это рекомендуемая настройка для production.

Параллельные apply workers

В PG16+ logical replication умеет применять транзакции параллельно (если их можно безопасно расщепить). Параметр max_parallel_apply_workers_per_subscription (default 2). До этого apply был строго последовательным — bottleneck на одной CPU. На write-heavy сценариях параллельный apply увеличивает throughput в 2-4 раза.

Row filters и column lists (PG15+)

С PG15 publication поддерживает row filter и column list:

  • CREATE PUBLICATION p FOR TABLE orders WHERE (status = 'active') — реплицировать только активные заказы.
  • CREATE PUBLICATION p FOR TABLE orders (id, customer_id, status) — реплицировать только эти колонки (sensitive колонки не покидают publisher).

Это даёт качественно новые сценарии:

  • Compliance. На analytics-кластер не идут email и phone — только агрегаты и FK.
  • Multi-tenant shard offload. Каждый shard публикует свои строки в единый analytics-cluster, отфильтрованные по tenant_id.
  • Subset replication для микросервисов. Сервис «orders» получает только колонки/строки, которые ему нужны, без широкого доступа к таблице.

Ограничения: row filter обязан быть immutable и не ссылаться на колонки, не входящие в REPLICA IDENTITY. Column list для UPDATE/DELETE требует REPLICA IDENTITY содержащую все колонки фильтра.

Logical vs physical: краткое сравнение

СвойствоPhysicalLogical
Что передаётсяБайты страниц через WALДекодированные tuple changes
Major versionДолжна совпадатьМожет отличаться (старшая → младшая или равная)
ГранулярностьВся БДPer-table
Subscriber writeableНет (read-only)Да (это обычная БД с extra apply worker)
DDLРеплицируетсяНет, делается вручную
SequencesРеплицируютсяНе реплицируются
ConflictНе бывает (replica only reads)Может быть, останавливает apply
Initial syncpg_basebackuptablesync worker per table
Use caseHA, read scalingMigration, selective copy, CDC
Проверка знанийKnowledge check
Вы делаете major upgrade Postgres 14 → 16 через logical replication. Publisher = старый 14, subscriber = новый 16. Большая таблица 'events' (PK = id BIGINT) реплицируется. Перед cutover вы хотите проверить, что данные не разъехались. Какие три проверки минимально сделать?
ОтветAnswer
(1) Подтверждение нулевого lag. Запросить на publisher: SELECT pid, sent_lsn, write_lsn, flush_lsn, replay_lsn, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag FROM pg_stat_replication; lag должен быть 0 или близко к 0. (2) Проверка sequences. Logical replication не двигает sequences — на subscriber выполнить для каждого: SELECT setval('events_id_seq', (SELECT MAX(id) FROM events)); иначе следующий INSERT после cutover упадёт на unique violation. (3) Row count и checksum по таблице. Самое простое: SELECT count(*), MIN(id), MAX(id) FROM events на обеих БД — числа должны совпадать. Точнее: SELECT md5(string_agg(t::text, '' ORDER BY id)) FROM events t — но это дорого на большой таблице, обычно достаточно sample. Дополнительно (4): проверить, что apply worker не залип на conflict (SELECT subname, srsubstate FROM pg_subscription_rel — все должны быть 'r' = ready).

Чек-лист

  • Logical replication = WAL декодируется в tuple changes, передаётся как pgoutput-сообщения, применяется как обычные DML на subscriber.
  • Требует wal_level = logical на publisher и REPLICA IDENTITY (обычно PK) на всех реплицируемых таблицах.
  • Publication = «что отдаём» (per-table, опционально per-operation). Subscription = «что подписаны» + apply worker.
  • Initial copy через tablesync, потом incremental stream через apply worker.
  • Не реплицируются: DDL, sequences, large objects. Это делается отдельно (миграции, ручной setval, отдельные тулзы).
  • REPLICA IDENTITY: DEFAULT (PK) / USING INDEX / FULL (все old values, дорого) / NOTHING (только INSERT).
  • Use cases: major upgrade без downtime, selective replication, multi-source consolidation, CDC в Kafka.
  • Logical slot удерживает WAL так же, как physical — мониторь restart_lsn.
MaterializedPostgreSQL и MaterializedMySQL Change Data Feed и Streaming в Delta Lake

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем logical replication принципиально отличается от physical?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5