Один сервер postgresql рано или поздно упрётся либо в нагрузку на чтение, либо в страх потерять данные при отказе диска. Репликация решает обе задачи сразу: мы запускаем второй сервер (реплику), который повторяет за основным каждое изменение байт в байт. В этом уроке разберём физическую потоковую репликацию: как primary отдаёт журнал, как standby его проигрывает, что такое слоты репликации и hot standby, чем синхронный режим отличается от асинхронного, и как пережить падение мастера через failover. Это фундамент отказоустойчивости, на котором потом строят и аналитические реплики, и кластеры под нагрузкой 1С.

Как это работает
Сердце репликации в PostgreSQL - это WAL (Write-Ahead Log), журнал упреждающей записи. Любое изменение данных сначала пишется в WAL и только потом долетает до файлов таблиц. Идея проста: если у нас есть полный поток WAL-записей, мы можем взять копию базы и, прокатив по ней этот поток, получить точно такое же состояние. Физическая репликация именно это и делает - копирует журнал на блочном уровне, поэтому реплика бит в бит совпадает с мастером, включая физическое расположение строк.
Потоковая (streaming) репликация означает, что standby не ждёт, пока на мастере закроется целый файл сегмента журнала. Вместо этого процесс walreceiver на реплике держит постоянное TCP-соединение с процессом walsender на мастере и тянет WAL почти в реальном времени, маленькими порциями. Задержка обычно измеряется миллисекундами.
Hot standby - это режим, в котором реплику можно не только держать про запас, а ещё и читать с неё (SELECT). Запись на standby запрещена, но запросы на чтение разгружают мастер. Это включается параметром hot_standby = on (по умолчанию так и есть начиная с давних версий).
Теперь про надёжность доставки. По умолчанию репликация асинхронная: мастер подтвердил COMMIT клиенту, не дожидаясь, пока реплика этот коммит получит. Быстро, но при внезапной гибели мастера последние транзакции могут не доехать. Синхронная репликация (synchronous_commit) заставляет мастер ждать подтверждения от реплики перед ответом клиенту - так данные не теряются, но каждый коммит платит сетевой задержкой.
Отдельная важная штука - слоты репликации (replication slots). Без слота мастер не знает, докуда дочитала реплика, и может удалить ещё нужный ей кусок WAL (особенно если реплика отключалась). Слот - это закладка: мастер гарантирует, что не выбросит WAL, пока реплика его не заберёт. Обратная сторона - если реплика умерла навсегда, слот будет вечно держать журнал и однажды переполнит диск мастера. В PG 13+ это лечится параметром max_slot_wal_keep_size, который ставит верхний предел.
SQL и примеры
Сначала готовим мастер. В postgresql.conf задаём уровень журналирования и лимиты на число walsender и слотов.
Код: Выделить всё
-- параметры на primary (postgresql.conf)
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
Код: Выделить всё
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'strong_pw';
-- в pg_hba.conf:
-- host replication replicator 10.0.0.0/24 scram-sha-256
Код: Выделить всё
SELECT * FROM pg_create_physical_replication_slot('standby1');
Код: Выделить всё
pg_basebackup -h 10.0.0.1 -U replicator -D /var/lib/pgdata \
-X stream -R --slot=standby1 -C
Код: Выделить всё
SELECT client_addr, state, sent_lsn, replay_lsn, sync_state
FROM pg_stat_replication;
Код: Выделить всё
SELECT client_addr,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;
Код: Выделить всё
-- на primary
ALTER SYSTEM SET synchronous_standby_names = 'standby1';
SELECT pg_reload_conf();
Код: Выделить всё
SELECT pg_is_in_recovery(); -- true на standby
SELECT pg_last_wal_replay_lsn(); -- докуда проиграли WAL
Код: Выделить всё
-- на standby (через утилиту pg_ctl)
pg_ctl promote -D /var/lib/pgdata
-- или SQL-функцией:
SELECT pg_promote();
Код: Выделить всё
-- читаем с реплики, мастер не нагружаем
SELECT f.flight_no, count(*) AS passengers
FROM bookings.boarding_passes bp
JOIN bookings.ticket_flights tf
ON tf.ticket_no = bp.ticket_no AND tf.flight_id = bp.flight_id
JOIN bookings.flights f ON f.flight_id = bp.flight_id
GROUP BY f.flight_no
ORDER BY passengers DESC
LIMIT 10;
- Слот без присмотра. Мёртвая реплика держит слот, мастер копит WAL и забивает диск под ноль. Мониторьте pg_replication_slots и ставьте max_slot_wal_keep_size.
- Синхронная реплика в единственном числе. Если synchronous_standby_names указывает на одну реплику и она упала, мастер встанет колом на коммитах, ожидая подтверждения. Держите минимум две синхронные кандидатуры или используйте ANY 1 (standbyA, standbyB).
- Конфликты на hot standby. Долгий SELECT на реплике может быть отменён, потому что мастер прислал VACUUM, удаливший нужные строки (ошибка canceling statement due to conflict with recovery). Лечится hot_standby_feedback = on или max_standby_streaming_delay.
- Промоутнули мастер, не убив старый. Два сервера, оба думают, что они primary - это split-brain и расхождение данных. Перед promote убедитесь, что старый мастер реально мёртв (фенсинг).
- Старый recovery.conf. В PG 12 этот файл упразднили: настройки переехали в postgresql.auto.conf, а сигналом служит файл standby.signal. Инструкции из древних статей под PG 11 уже не работают.
- Забыли про timeline. После failover у новой ветки журнала своя временная линия. Чтобы старый мастер вернулся как реплика, его обычно надо переинициализировать через pg_rewind, а не просто перезапустить.
- Поднимите два кластера PostgreSQL 15 на одной машине: основной на порту 5432, будущая реплика на 5433 (разные каталоги данных).
- На мастере задайте wal_level = replica, max_wal_senders = 10, создайте роль replicator с правом REPLICATION и пропишите доступ для replication в pg_hba.conf.
- Создайте физический слот pg_create_physical_replication_slot('standby1') и снимите копию: pg_basebackup -h localhost -p 5432 -U replicator -D pgdata_standby -X stream -R --slot=standby1.
- Запустите реплику на 5433 и убедитесь в pg_stat_replication на мастере, что появилась строка с state = streaming.
- Вставьте на мастере тестовую таблицу с парой строк, затем выполните тот же SELECT на реплике - данные должны приехать. Проверьте, что INSERT на реплике падает с ошибкой read-only.
- Включите синхронный режим (synchronous_standby_names = 'standby1', reload), сделайте коммит и посмотрите sync_state в pg_stat_replication.
- Остановите мастер, выполните pg_promote() на реплике, убедитесь, что pg_is_in_recovery() вернул false и запись теперь разрешена.
- Почему физическая репликация требует именно WAL и что значит "реплика бит в бит совпадает с мастером"?
- Чем потоковая доставка WAL отличается от доставки через архив сегментов и в чём выигрыш по задержке?
- Зачем нужен слот репликации и какой ценой он защищает реплику от потери журнала?
- В каком случае синхронная репликация может полностью заблокировать запись на мастере и как это предотвратить?
- Что такое split-brain при failover и какие меры (фенсинг, pg_rewind) защищают от него?
- Какую проблему решает hot_standby_feedback и за что мы при этом платим на стороне мастера?