Один сервер PostgreSQL тянет на себе запись, живые транзакции и вечерние отчёты, пока не случится первая серьёзная поломка. Диск, питание, неудачное обновление, ошибка в консоли: любого из них достаточно, чтобы приложение остановилось целиком, ведь второй копии данных нет. Потоковая репликация закрывает уязвимость штатными средствами самой СУБД: второй сервер непрерывно применяет журнал изменений и в любой момент готов подхватить нагрузку. Заодно копия принимает тяжёлые SELECT, от которых раньше страдали живые транзакции. Ниже путь от параметров первичного сервера до плана действий при его отказе, с командами, готовыми к копированию в терминал.

Как журнал WAL превращается в транспорт для полной копии базы

PostgreSQL не пишет изменения сразу в файлы данных. Каждая операция сначала попадает в журнал упреждающей записи WAL, строгую ленту записей, по которой база восстанавливается до последней транзакции. Репликация пользуется этой лентой напрямую. Фоновый процесс walsender на первичном сервере читает новые записи журнала и отправляет их по обычному сетевому соединению. На другом конце процесс walreceiver складывает полученное на диск, а процесс восстановления накатывает записи на собственную копию данных. Результат совпадает с оригиналом байт в байт.

Это физическая репликация: копируется весь кластер целиком, со схемой, ролями и табличными пространствами. Логическая репликация со своими подписками на отдельные таблицы создана для других задач, например для переноса данных между ветками СУБД. Горячий резерв строится именно на потоке WAL.

Механику поддерживает любая свежая ветка. Стабильной сейчас считается 18, актуальная младшая версия имеет номер 18.6, следующая большая версия 19 уже доступна в бете. Каждая ветка получает исправления пять лет после релиза.

Подготовка первичного сервера к передаче журнала WAL реплике

Отправная точка, три параметра в postgresql.conf. Уровень ведения журнала задаёт wal_level, для репликации нужно значение replica или выше, иначе поток не включится. Число одновременно подключённых копий ограничивает max_wal_senders, по умолчанию 10. Слоты репликации, о них ниже, требуют запаса в max_replication_slots, тоже 10 по умолчанию.

# postgresql.conf на первичном сервере
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10

Для репликационных подключений нужна отдельная роль с правом REPLICATION. Строка подключения хранится на реплике в открытом виде, поэтому пароль выбирают стойкий и держат подальше от репозиториев и конфигурационных бэкапов.

CREATE ROLE replicator WITH LOGIN REPLICATION PASSWORD 'RepPass-2026';

Последний штрих, правило в pg_hba.conf, которое разрешает репликацию только из внутренней сети. Репликационный трафик помечен значением replication в колонке базы, поэтому правило живёт отдельной строкой и не пересекается с обычными клиентскими подключениями.

# pg_hba.conf на первичном сервере
host replication replicator 192.168.1.0/24 scram-sha-256

Создание реплики утилитой pg_basebackup и запуск в режиме ожидания

Копию снимает штатная утилита pg_basebackup, причём без остановки записи на первичном сервере. Утилита делает согласованный снимок данных, параллельно подтягивает нужные сегменты журнала и сама готовит стартовую конфигурацию реплики.

pg_basebackup -h 192.168.1.50 -p 5432 -U replicator \
  -D /var/lib/postgresql/18/standby \
  -Fp -Xs -R -P

Флаги читаются без словаря. Значение -Fp оставляет данные в привычном формате каталога. Передача -Xs копирует журнал потоком прямо во время снимка, чтобы копия вышла целостной. Флаг -R пишет в новый каталог файл standby.signal и строку подключения primary_conninfo в файл postgresql.auto.conf, после чего реплика знает, у кого догонять. Именно пустой файл standby.signal отличает реплику от обычного сервера: при старте PostgreSQL видит его, не открывает базу для записи и подключается к первичному серверу за журналом.

Проверить роль можно одним запросом после запуска:

SELECT pg_is_in_recovery();

Ответ true означает, что сервер применяет журнал и отдаёт только чтение. Параметр hot_standby включён по умолчанию, поэтому запросы к реплике работают сразу после старта, не дожидаясь полной синхронизации.

Слоты репликации и запас журнала на случай отставания копии

Реплика отстаёт по разным причинам: сеть стала узким местом, диск копии не поспевает за журналом, ночь принесла витрину отчётов. Первичный сервер в это время утилизирует старые сегменты журнала, перезаписывая их новыми. Если нужный сегмент уже стёрт, поток обрывается, и копию остаётся пересоздавать целиком.

Защиты две. Первая, запас wal_keep_size, удерживает на первичном сервере заданный объём старого журнала в мегабайтах. Вторая надёжнее: слот репликации фиксирует позицию конкретной копии и запрещает утилизацию журнала за её спиной. Слот создаётся одной командой:

SELECT * FROM pg_create_physical_replication_slot('node_a_slot');

На реплике слот указывается в postgresql.conf рядом со строкой подключения:

primary_conninfo = 'host=192.168.1.50 port=5432 user=replicator password=RepPass-2026'
primary_slot_name = 'node_a_slot'

Размер запаса wal_keep_size считают от худшего сценария: пиковая скорость записи журнала умножается на самое долгое время починки сети. Кластер, который в час пик доводит журнал до 200 МБайт, при восьмичасовом окне недоступности обязан удерживать не меньше 1600 МБайт. Слот закрывает тот же риск без арифметики, поэтому на практике чаще выбирают его.

У слотов есть обратная сторона. Параметр max_slot_wal_keep_size по умолчанию равен -1, то есть слоту разрешено удерживать журнал без ограничений. Реплика легла на неделю, журнал занял весь диск, первичный сервер остановил запись. Практичное решение: назначить верхний предел удержания и следить за слотами через представление pg_replication_slots, где колонки active и restart_lsn показывают, кто жив, где он остановился и сколько журнала накопилось сверх нужного.

Мониторинг отставания по позициям LSN и представлению pg_stat_replication

Позиция в журнале называется LSN и записывается парой шестнадцатеричных чисел вроде 0/5A400320. Разница позиций даёт отставание в байтах, самый честный показатель состояния пары. Главный источник данных на первичном сервере, представление pg_stat_replication, содержит по строке на каждую подключённую реплику:

SELECT application_name, client_addr, state,
       sent_lsn, write_lsn, flush_lsn, replay_lsn,
       write_lag, flush_lag, replay_lag
FROM pg_stat_replication;

Колонка state обязана показывать streaming, любое другое значение это повод заглянуть в логи. Тройка write_lag, flush_lag, replay_lag измеряет задержку в миллисекундах на трёх этапах пути: приём, сброс на диск, применение в данных. Байтовое отставание считается одной функцией разности:

SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;

Сама реплика смотрит на себя функцией pg_last_xact_replay_timestamp, по которой видно, сколько времени прошло с последней применённой транзакции:

SELECT now() - pg_last_xact_replay_timestamp() AS lag_interval;

На самой реплике виден и внутренний затор: журнал получен, но ещё не применён.

SELECT pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()) AS apply_backlog_bytes;

Ненулевой хвост означает очередь записей перед процессом восстановления: сеть не виновата, диск копии не поспевает за применением. Порог тревоги подбирают по характеру приложения: тяжёлая аналитика спокойно живёт с отставанием в минуту, балансировка критичного чтения считает на десятки миллисекунд.

Стабильный рост лага это симптом: медленная сеть, перегруженный диск копии или запросы, которые мешают восстановлению применять журнал. Голые числа лучше отдать графикам: разовый всплеск заметен сразу, а пологий суточный подъём без графика почти всегда пропускают.

Реплика под запросами чтения и конфликты с применяемым журналом

Режим горячего ожидания позволяет копии отвечать на SELECT параллельно с накатом журнала. Запросы работают по обычным правилам многоверсионности и восстановлению не мешают. Конфликт рождается в узком месте: очистка на первичном сервере пометила строки к удалению, реплика получила соответствующую запись журнала, а отчёт на копии всё ещё читает эти строки. Приходится выбирать между свежестью данных и живучестью запроса.

Развязку задаёт параметр max_standby_streaming_delay. По умолчанию 30 секунд: реплика терпеливо ждёт окончания запроса и лишь затем отменяет его ради применения журнала. Значение -1 разрешает ждать вечно, что красиво звучит, пока тяжёлый отчёт не превратит копию в вечно отстающую. Второй инструмент, hot_standby_feedback, заставляет реплику сообщать первичному серверу о своих долгих запросах, чтобы очистка не трогала нужные ей строки. Плата предсказуема: мусор задерживается на первичном сервере, и таблицы растут в объёме. Для аналитической копии это нормальный размен, для реплики под критичное чтение feedback лучше держать выключенным.

Счётчик сработавших конфликтов живёт в представлении pg_stat_database_conflicts. Если отмены повторяются, выходов два: вынести тяжёлые отчёты на отдельную копию с большим лимитом ожидания либо ускорить сами запросы. Дальше начинается человеческая часть: приложение должно знать, куда слать запись, а куда чтение. Современные фреймворки и ORM поддерживают два канала из коробки, нужен лишь отдельный адрес реплики в конфигурации пула соединений. Тонкость одна: чтение сразу после записи может не увидеть собственных изменений, пока журнал дойдёт до копии, поэтому сценарии с требованием прочитать своё немедленно отправляют на первичный сервер без всяких балансировок.

Синхронный режим подтверждённых коммитов и план действий при аварии

По умолчанию репликация асинхронная: коммит возвращается клиенту после записи в локальный журнал, копия догоняет в фоновом порядке. Такой режим быстрый, но полный отказ первичного диска способен унести последние незадублированные транзакции. Когда цена нескольких коммитов выше лишних миллисекунд, включается синхронный режим:

synchronous_standby_names = 'FIRST 1 (standby1, standby2)'

С непустым списком каждый коммит ждёт подтверждения, что реплика записала его на свой диск. Времена измеряются задержкой сети: доли миллисекунды внутри одной стойки, единицы между площадками. Тонкость в том, что список работает в паре с параметром synchronous_commit, который настраивается и на уровне отдельной сессии. Значение off в конкретной сессии отправляет её коммиты без ожидания реплики и разгружает массовые загрузки, а значения remote_write и remote_apply усиливают гарантии: первое ждёт записи в кэш операционной системы реплики, второе полного применения записи в данных.

Дальше план на самый неприятный день. При аварии первичного сервера порядок действий держит переключение коротким и обратимым:

  1. убедиться по мониторингу, что реплика догнала последнюю позицию журнала;
  2. изолировать отказавший узел от приложений и балансировщика, чтобы он не вернулся в строй самовольно;
  3. повысить реплику до первичного сервера командой pg_ctl promote или функцией pg_promote;
  4. переключить строку подключения приложения на новый узел и перезапустить пулы соединений;
  5. восстановить бывший первичный сервер в роли реплики быстрой утилитой pg_rewind и вернуть его в пару.

Главное правило переключения: в роли первичного в каждый момент живёт ровно один сервер. Два узла, одновременно принимающие запись, гарантируют расхождение данных, которое потом сводят вручную, поэтому изоляция старого узла важнее скорости переключения.

После переключения репликация продолжается в новой ветке истории журнала: параметр recovery_target_timeline по умолчанию равен latest, и новые копии следуют за свежей веткой автоматически. Каскадная репликация позволяет второй копии тянуть журнал не с первичного сервера, а с первой реплики, что экономит канал между площадками. И правило, которое экономит нервы лучше любых инструкций: репликация защищает от железа, но не от человека. DELETE без WHERE повторится на копии с той же скоростью, поэтому резервные копии остаются в расписании, а отработку отказа включают в регламент, например раз в квартал на тестовом контуре.