Learning Platform
Урок 14.03 · 24 мин
Продвинутый
Physical ReplicationStreamingHot StandbyReplication SlotsHA

В первых двух уроках мы выяснили: WAL — это последовательный поток всех изменений в БД, разрезанный на 16 MiB сегменты, с LSN-адресацией. После crash мы умеем по WAL восстановить состояние.

Теперь сделаем следующий логический шаг: что, если этот WAL отправить на другой сервер? Если второй сервер будет всегда «находиться в recovery-режиме» и применять каждую новую WAL-запись по мере поступления, он будет физически идентичной копией primary. Это и есть physical streaming replication — старейший и самый надёжный механизм репликации в Postgres.

Свойства physical replication

Главное, что нужно понять: physical replication — это байт-в-байт копия на уровне страниц. Это не «отдельная БД, в которую льются те же запросы». Это та же физическая БД, страницы 8 KiB которой синхронизированы между primary и standby через WAL.

Следствия:

  • Версия Postgres major должна совпадать. Replica 15 не возьмёт WAL от primary 14 (формат WAL меняется). Major upgrade primary = всех replicas пересоздавать.
  • Все табличные пространства, базы, расширения, объекты — те же. Невозможно реплицировать только одну базу или одну таблицу. Всё или ничего.
  • Архитектуры совместимы. x86_64 → arm64 в general не работает (endianness и alignment отличаются в WAL FPI). Все ноды кластера обычно на одной архитектуре.
  • DDL реплицируется автоматически. Создал индекс на primary — он появится на replica без специальных команд, потому что CREATE INDEX пишет WAL.
  • Replica всегда read-only. Никакие записи невозможны (попытка вызовет ERROR), включая VACUUM. VACUUM делается только на primary, и его эффект приезжает на replica через WAL.

Архитектура: walsender и walreceiver

Streaming replication: топология

Primary держит walsender-процесс для каждой replica. Replica запускает walreceiver, который подключается к walsender и получает WAL по TCP. На replica startup process применяет WAL к страницам.

PRIMARY (master)pg_current_wal_lsn() — пишущий узел
backend processesобрабатывают INSERT/UPDATE/DELETE
walwriterflush'ит WAL на disk
walsender (по 1 на replica)читает WAL и стримит по TCP
TCP 5432WAL stream →
streamingasync/sync
STANDBY (replica)pg_last_wal_replay_lsn() — читающий узел
walreceiverпринимает WAL от walsender
startup processприменяет WAL к страницам (recovery never ends)
backend processesread-only SELECT
Слот (replication slot) на primary хранит restart_lsn — гарантия, что WAL не будет удалён, пока replica его не подтвердилаpg_replication_slots

На primary запускается walsender — отдельный backend-процесс для каждой подключённой replica. Он читает WAL по мере его записи и отправляет по TCP-соединению. На replica работает walreceiver, который принимает байты и пишет их в pg_wal/ standby. Параллельно startup process реплеит входящий WAL, применяя изменения к shared_buffers.

Это всё работает на стандартных Postgres processes — никаких внешних агентов, никакого Kafka. Только TCP-соединение и WAL.

Шаг 1: initial sync через pg_basebackup

Прежде чем стримить incremental WAL, replica должна получить базовый снимок данных. Это делается утилитой pg_basebackup — она подключается к primary по replication protocol и копирует весь $PGDATA побайтово:

# на будущей replica:
pg_basebackup \
  -h primary.example.com \
  -U replicator \
  -D /var/lib/postgresql/15/data \
  -P \
  -X stream \
  -R

Что делают флаги:

  • -D — куда положить копию.
  • -P — progress reporting.
  • -X stream — параллельно с копированием data стримить и WAL, чтобы получить консистентный снимок (без -X backup не self-contained: если на primary случится checkpoint, могут потеряться промежуточные изменения).
  • -R — автоматически записать standby.signal и primary_conninfo в postgresql.auto.conf. После pg_basebackup достаточно запустить pg_ctl start — replica подключится сама.

После pg_basebackup standby содержит данные на момент начала backup’а + WAL до конца backup’а. Запустившись, она применяет WAL до текущего LSN primary и переходит в режим «жду новых WAL-записей по TCP». Этот переход называется catch-up phase.

Hot standby: read-only нагрузка на replica

По умолчанию replica может быть в двух состояниях:

  • Warm standby — replica применяет WAL, но не принимает соединения. Видна как доступная только в момент failover.
  • Hot standby — replica применяет WAL и одновременно принимает read-only соединения. Это default в современных Postgres (hot_standby = on).

Hot standby позволяет:

  • Запускать аналитические запросы без нагрузки на primary.
  • Делать pg_dump с replica, освобождая primary.
  • Запускать read-only веб-приложения через connection pooler с маршрутизацией.

Но есть нюансы:

  • Recovery conflict. Если на primary VACUUM удалил dead tuples, чьи xmax относятся к завершённой транзакции, а на replica в этот момент работает долгий SELECT, использующий этот snapshot — replica должна либо приостановить replay, либо убить запрос. Поведение управляется max_standby_streaming_delay.
  • Read-after-write. После INSERT на primary мгновенно сделать SELECT на replica может вернуть пусто — WAL ещё не прилетел. Это replication lag, и приложение должно либо терпеть, либо использовать synchronous_commit = remote_apply, либо после write читать обратно с primary.
  • hot_standby_feedback. Replica может слать primary информацию о своих активных snapshot — тогда primary VACUUM не будет удалять нужные replica dead tuples. Это решает recovery conflict, но раздувает bloat на primary.

На реальной replica проверка состояния выглядит так. В pglite мы не в recovery, поэтому in_recovery вернёт false. На настоящей replica — true и pg_last_wal_replay_lsn покажет, докуда дотянулась replication.

PostgreSQL

Replication slots: защита WAL от удаления

Что произойдёт, если replica на час уйдёт в offline (network split, ребут, etc.), а на primary за это время прошло 100 GiB WAL и сработало 50 checkpoint’ов? Все «старые» WAL-сегменты будут удалены checkpoint’ом, потому что primary не знает, что replica их ещё ждёт. Когда replica вернётся — она спросит у primary WAL с LSN, которого больше нет. Repair единственный — пересоздать replica через pg_basebackup.

Решение — replication slot. Это объект на primary, который хранит restart_lsn — минимальный LSN, который ещё нужен какому-то consumer’у. Primary не удалит WAL-сегменты до этого LSN, даже если они старше checkpoint.

Список replication slots. В реальной БД каждая physical replica будет создавать здесь запись с restart_lsn — позицией WAL, до которой replica подтвердила приём. pglite не поддерживает реальные slots, но синтаксис верный.

PostgreSQL

Обратная сторона: если replica умерла навсегда и её slot не удалён вручную, WAL копится бесконечно. Это самая частая причина «вдруг закончилось место в pg_wal/». Мониторинг должен следить за pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) для каждого slot.

Async vs sync vs remote_apply

Параметр synchronous_commit на primary определяет, что должно случиться до того, как COMMIT вернёт OK клиенту:

  • off — COMMIT возвращает OK сразу, WAL ещё не на disk. Самое быстрое, но при crash можно потерять последние транзакции. Допустимо для аналитики.
  • local — WAL flushed на disk primary. Default-альтернатива async-replication. Стандартный durability на одном узле.
  • on (default) — то же, что local, плюс если есть synchronous standby — WAL должен быть записан на disk standby.
  • remote_write — WAL записан в pg_wal/ standby, но необязательно fsync’нут.
  • remote_apply — WAL применён на standby (изменения видны в SELECT). Это даёт строгую read-after-write consistency, но добавляет latency: COMMIT ждёт network round-trip + apply.

«Synchronous standby» определяется параметром synchronous_standby_names. По умолчанию пуст — все replicas async. Можно указать 'FIRST 1 (replica_a, replica_b)' — ждать ответ от любой одной из двух (quorum-like в варианте ANY).

Trade-off: чем строже sync, тем дольше commit latency и тем сильнее доступность primary зависит от replica (если sync replica умерла и других нет — primary висит на commit). Обычная схема: 2-3 replicas, synchronous_standby_names = 'ANY 1 (replica_a, replica_b, replica_c)', чтобы не зависеть от одной конкретной.

Cascading replication: replica → replica → replica

С Postgres 9.2+ replica может сама быть источником для других replicas — это cascading replication. Полезно для:

  • Снижения нагрузки на primary. Если у тебя 10 read-replicas, держать 10 walsender на primary — дорого по CPU и network. Сделай 2 «первого уровня», и от них уже стримь на остальные 8.
  • Геораспределения. Primary в Москве, replica-A в Санкт-Петербурге как «региональный hub», от неё — replicas в Нижнем, Казани, Уфе. Меньше WAN-трафика.

Cascaded replica запускается так же, как обычная — pg_basebackup от primary (или от другой replica), primary_conninfo указывает на upstream replica. Нюансы:

  • WAL приезжает с задержкой = lag(primary → mid) + lag(mid → cascaded). Считайте сумму.
  • Если middle replica умерла, cascaded остаются без WAL. Failover тулинг должен уметь перенаправлять cascaded на новый upstream.

Replication lag: что мониторить

Замер lag — критическая практика. Три LSN на primary в pg_stat_replication:

  • sent_lsn — сколько WAL primary отправил по сети.
  • write_lsn — сколько WAL standby записал в свой pg_wal/.
  • flush_lsn — сколько WAL standby flush’нул на disk.
  • replay_lsn — сколько WAL standby применил к страницам (видно через SELECT на standby).

Разница pg_current_wal_lsn() - replay_lsn — это byte lag. Перевод в секунды (через приближённую скорость WAL) — time lag. На production-мониторинге обычно alert при byte lag > N MiB или time lag > N seconds.

Состояние replication на реальной primary. В pglite вьюшка возвращает 0 строк (нет walsender), но синтаксис верный.

PostgreSQL

Большой replay_lag при маленьком flush_lag означает: WAL пришёл и записан, но recovery не успевает применить. Часто причина — конфликт с долгим SELECT (см. max_standby_streaming_delay). Большой sent_lag — network bottleneck или slow disk на standby. Это разные диагнозы.

Recovery conflict: SELECT vs replay

На hot standby есть фундаментальный конфликт между:

  • read-only нагрузкой (долгие SELECT’ы с устаревшим snapshot),
  • WAL replay (от primary приходят изменения, которые могут «удалить» строки, нужные SELECT).

Конкретный пример: на primary VACUUM удалил dead tuples, чьи xmax относятся к старой завершённой транзакции. На replica в этот момент работает 10-минутный аналитический SELECT, чей snapshot ещё видит эти tuples. Если replica применит WAL — SELECT упадёт с «canceling statement due to conflict with recovery». Если не применит — replication lag растёт.

Параметры управления:

  • max_standby_streaming_delay (default 30s) — сколько максимум ждать SELECT перед его убийством. На аналитических replica часто ставят -1 (бесконечно) при hot_standby_feedback = on.
  • hot_standby_feedback = on — replica шлёт primary информацию о своих active xmin. Primary VACUUM не удаляет нужные replica tuples. Но это раздувает bloat на primary.

Trade-off ясный: либо терпите recovery conflict и убиваете долгие SELECT, либо терпите bloat на primary и работаете на replica спокойно. Выбор зависит от того, что важнее для системы.

Failover: что значит «replica становится primary»

Promote — это команда pg_ctl promote или вызов pg_promote(). На replica она:

  1. Закрывает walreceiver.
  2. Применяет всё, что есть в локальном WAL.
  3. Создаёт новый timeline — увеличивает timeline ID на 1 (см. имя WAL-файла: первые 8 hex). Это нужно, чтобы будущие WAL-записи новой primary не пересекались с «параллельной вселенной» старой primary, если та когда-то вернётся.
  4. Снимает standby.signal и открывается на запись.

После promote старая primary уже не вернётся как primary к новой — она должна быть либо пересоздана как replica новой (pg_rewind или pg_basebackup), либо изолирована. Иначе получишь split-brain: две БД с расходящимися timeline.

Production HA-tooling (Patroni, repmgr) автоматизирует выбор новой primary, обновление DNS/HAProxy, fencing старой primary. Postgres сам не делает автоматический failover — это задача внешнего оркестратора.

Проверка знанийKnowledge check
На вашей primary включён synchronous_commit = on и synchronous_standby_names = 'replica_a'. Единственная sync-replica replica_a недоступна (network). Что произойдёт с транзакциями, делающими COMMIT? Что делать?
ОтветAnswer
Транзакции будут зависать на COMMIT — primary ждёт подтверждения от replica_a, что WAL получен, а оно не приходит. Чтения работают, но любая запись стоит. Это «sync replication tax»: ты получаешь сильную durability ценой доступности. Что делать в инциденте: либо быстро восстановить replica_a (ребут, сеть), либо ALTER SYSTEM SET synchronous_standby_names = '' и SELECT pg_reload_conf() — primary переключится в async-режим, COMMIT'ы пойдут (теряя durability). Чтобы избежать этого впредь — используй ANY N (replica_a, replica_b, replica_c) с N меньше числа replicas: тогда падение одной не блокирует commit. И всегда имей наготове runbook для перевода в async во время инцидента.

Чек-лист

  • Physical streaming replication = байт-в-байт копия страниц через WAL-поток. Major version и архитектура должны совпадать.
  • pg_basebackup — initial sync; флаги -X stream -R дают self-contained базу + автоконфиг replica.
  • walsender / walreceiver / startup — пара процессов на primary/standby + recovery-loop, applying WAL.
  • Hot standby — replica принимает read-only соединения. Грозит recovery conflict; hot_standby_feedback решает, но даёт bloat на primary.
  • Replication slotrestart_lsn защищает WAL от удаления для активной replica. Заброшенный slot = вечный рост pg_wal/.
  • synchronous_commit = off / local / on / remote_write / remote_apply — trade-off latency vs durability vs read-after-write.
  • Failover = promote replica, новый timeline, старая primary исключается из кластера. Авто-failover — задача внешнего оркестратора (Patroni).
Журнал репликации и восстановление Unix domain sockets — быстрее TCP на одной машине

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём ключевое свойство physical streaming replication?

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

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

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

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