38.1 Репликация как незаканчивающееся восстановление
Начнём с модели, а не с конфигов. Вспомни crash recovery: сервер берёт последнюю контрольную точку (см. checkpoint) и проигрывает WAL вперёд до конца журнала, восстанавливая страницы. Что будет, если конец журнала не наступает - записи всё прибывают?
Сервер так и останется в режиме восстановления, вечно догоняя поток. Это и есть standby. Он не «копирует таблицы» - он применяет тот же журнал, который primary пишет у себя. Любое изменение, дошедшее до WAL на primary, рано или поздно проиграется на standby и ляжет в те же самые байты.
Отсюда сразу следуют свойства физической репликации:
- копия побайтовая: версия, схема, физическая раскладка - всё идентично, выбрать «только одну таблицу» нельзя;
- standby read-only: он в режиме recovery, писать туда нельзя в принципе, отвергается любой INSERT;
- один primary, его поток могут принимать много standby.
38.2 Три процесса: walsender, walreceiver, startup
Поток журнала обслуживают три процесса. Запусти ps на обоих
серверах - и ты их увидишь поимённо.
primary standby
┌──────────────┐ ┌──────────────┐
│ backends │ пишут WAL │ walreceiver │ принимает по TCP
│ ↓ │ │ ↓ │
│ WAL на диск │ ──walsender──▶ │ WAL на диск │
│ │ │ ↓ │
└──────────────┘ │ startup │ проигрывает (replay)
└──────────────┘
- walsender на primary читает WAL и отдаёт его в сетевое соединение. По одному walsender на каждый подключённый standby.
- walreceiver на standby принимает поток и сбрасывает на диск.
- startup на standby проигрывает принятые записи в строгом порядке.
Разделение walreceiver и startup важно: принять журнал и применить его - разные задачи с разной скоростью. Именно отсюда возьмётся лаг.
38.2.1 Копнуть глубже: hot_standby и чтение с реплики
Параметр hot_standby = on (по умолчанию включён) разрешает
подключаться к standby и читать, пока тот применяет журнал. Без него
standby - чёрный ящик до самого failover.
Чтение с реплики - частый способ разгрузить primary от отчётов. Но запросы на standby конкурируют со startup-процессом за те же страницы, и тут рождается конфликт восстановления: startup хочет проиграть очистку строк, которые читает твой запрос (про горизонт уборки см. transaction-horizon). Об этом - hot-standby-feedback и врезка 38.5 ниже.
38.3 LSN: линейка, по которой меряют всё
LSN (Log Sequence Number) - это позиция в журнале. Монотонно растущее число, которое можно прочитать как «сколько байт WAL сервер уже произвёл». Любую точку репликации выражают в LSN.
SELECT pg_current_wal_lsn(); -- докуда primary записал
SELECT pg_last_wal_replay_lsn(); -- докуда standby проиграл
Поскольку LSN растёт линейно, разница двух LSN - это объём журнала
между ними в байтах. Функция pg_wal_lsn_diff(a, b) считает эту
разницу. На ней строится всё измерение лага: лаг репликации - это
pg_current_wal_lsn() на primary минус то, докуда добрался standby.
Прежде чем читать дальше, предскажи: если на standby полностью остановить применение журнала, а на primary продолжать писать - что будет с разницей этих двух LSN? Проверишь в лабе.
38.4 pg_stat_replication: три рубежа standby
На primary есть представление pg_stat_replication - по строке на
каждый подключённый standby. В нём три LSN, и они не совпадают не
случайно:
sent_lsn- досюда primary отправил журнал;flush_lsn- досюда standby сбросил на диск;replay_lsn- досюда standby проиграл (это видно на чтении).
Порядок всегда такой: sent >= flush >= replay. Разрыв между
flush и replay - это «журнал уже принят и надёжно лежит, но ещё
не применён». Под нагрузкой первым отстаёт именно replay: записать
сегмент на диск дёшево, а проиграть его - значит конкурировать с
читающими запросами на standby.
SELECT application_name, state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_behind_bytes,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
Колонки *_lag показывают то же отставание, но во времени, а не в
байтах - удобно для алертов «реплика отстаёт больше N секунд».
38.5 Подводный камень: standby без слота теряет журнал
Primary не хранит WAL вечно. Он удаляет старые сегменты по своим
правилам (max_wal_size, контрольные точки), как только они больше
не нужны для его собственного восстановления. Про standby он по
умолчанию ничего не знает.
Сценарий аварии: standby отключился на обслуживание на час. За этот
час primary под нагрузкой переехал контрольную точку и удалил
сегменты, которые standby ещё не получил. Standby возвращается, просит
нужный сегмент - а его уже нет. Репликация рвётся:
requested WAL segment ... has already been removed.
Чинит это слот репликации (pg_create_physical_replication_slot).
Слот - это запись на primary «вот этот потребитель дочитал до
restart_lsn»: primary держит WAL до этой границы и не удаляет раньше.
Но у слота есть обратная сторона, и она опаснее. Забытый неактивный слот удерживает WAL навсегда - primary не удаляет журнал, которого ждёт давно мёртвый потребитель, и диск primary забивается под ноль. Поэтому слоты надо мониторить:
SELECT slot_name, active, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;
active = false на слоте, который копит retained - это будущий
инцидент «диск кончился на primary».
38.6 Асинхронно против синхронно: что теряем при падении
По умолчанию репликация асинхронная. COMMIT на primary возвращает управление, не дожидаясь, пока изменение уедет на standby. Это быстро: клиент не платит за сеть до реплики. Цена - окно потери: если primary умрёт прямо после COMMIT, последние транзакции могли не успеть отправиться, и при переключении на standby они пропадут.
Синхронная репликация закрывает это окно. С
synchronous_standby_names и synchronous_commit = on COMMIT не
вернётся, пока standby не подтвердит запись. Потерь при failover нет.
Платит каждый COMMIT - задержкой в один сетевой круг до реплики.
Это не «как лучше», а честный выбор. Финансовая запись, которую нельзя потерять - синхронно, смиряемся с задержкой. Поток событий, где потеря секунды некритична - асинхронно, бережём latency. Тот же компромисс между задержкой и согласованностью разберём строго в главе про распределённые ловушки.
38.7 Failover и таймлайны
Когда primary умирает, standby повышают до нового primary
(pg_promote()). С этого момента он выходит из recovery и начинает
писать собственный WAL. Чтобы новый поток не спутали со старым,
PostgreSQL увеличивает номер таймлайна - это «ветка истории» журнала.
Таймлайн отвечает на вопрос «чей это WAL»: до промоушена сегменты шли по таймлайну 1, после - по таймлайну 2. Старые standby, которые хотят последовать за новым primary, должны переключиться на новый таймлайн. Без этого механизма два сервера, разошедшихся после аварии, писали бы конфликтующий журнал с одинаковыми LSN.
38.8 Как это собрать руками (обзор)
Полная настройка standby - это, по сути, четыре шага, и каждый опирается на уже знакомый механизм:
- на primary разрешить репликационные подключения (роль с
REPLICATION, строка вpg_hba.conf); - снять базовую копию primary -
pg_basebackupделает физический слепок работающего кластера; - положить рядом файл-признак
standby.signal, чтобы сервер поднялся в режиме standby, а не как обычный primary; - указать в
primary_conninfo, к кому подключаться, и (желательно) имя слота.
Дальше standby сам подключится, walreceiver начнёт принимать поток, а startup - проигрывать. В лабе мы соберём то, что можно собрать на одном сервере: создадим слот, посмотрим, как он держит WAL, и измерим продвижение LSN. Полную пару primary плюс standby даёт отдельная топология сэндбокса.
Уроки в sandbox
lab-38.1. Слот репликации и продвижение LSN на primary
Полную пару primary плюс standby поднимает отдельная топология, а здесь мы разберём primary-сторону на одном сервере: создадим физический слот, увидим, как он держит WAL, и пронаблюдаем, как растёт LSN под нагрузкой. Сначала предскажешь поведение, потом проверишь.
Посмотри стартовую позицию журнала:
SELECT pg_current_wal_lsn();. Запомни значение.Создай физический слот:
SELECT pg_create_physical_replication_slot('lab_slot');. Проверь его:SELECT slot_name, active, restart_lsn FROM pg_replication_slots;- слот есть, active = false (потребителя нет), restart_lsn пока NULL.Предскажи: вырастет ли pg_current_wal_lsn() после серии изменений? Затем нагенерируй WAL:
UPDATE bloat_demo SET payload = payload;несколько раз.Снова сними
SELECT pg_current_wal_lsn();и посчитай разницу через pg_wal_lsn_diff - журнал продвинулся ровно на объём изменений.Убери за собой:
SELECT pg_drop_replication_slot('lab_slot');. Неудалённый неактивный слот в проде держал бы WAL и забил диск.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Физическая репликация - это незаканчивающееся восстановление: standby вечно проигрывает WAL primary и получается побайтовая копия.
- Поток обслуживают три процесса: walsender на primary, walreceiver и startup на standby. Принять журнал и применить его - разные задачи, отсюда лаг.
- LSN - линейная позиция в журнале; разница двух LSN в байтах через pg_wal_lsn_diff - это и есть мера лага.
- pg_stat_replication даёт три рубежа по каждому standby: sent >= flush >= replay; первым под нагрузкой отстаёт replay.
- Без слота primary может удалить WAL, который standby ещё не получил, и репликация порвётся; забытый неактивный слот, наоборот, забьёт диск primary.
- Асинхронная репликация быстрая, но при падении primary теряет хвост транзакций; синхронная закрывает окно потерь ценой задержки каждого COMMIT.
- При failover standby повышают до primary, номер таймлайна растёт - это разделяет старую и новую ветку истории журнала.
Контрольные вопросы
Почему физическую репликацию называют побайтовой копией и что из этого следует?
Показать ответ
Standby применяет тот же WAL, что и primary, поэтому страницы, relfilenode и системный идентификатор у них идентичны. Следствия: нельзя реплицировать только часть таблиц, нельзя писать на standby (он в режиме recovery), и обе стороны обязаны быть одной мажорной версии. Если нужна выборочная репликация или запись на копии - это уже логическая репликация, другой механизм.
Почему в pg_stat_replication replay_lsn обычно отстаёт от flush_lsn сильнее всего?
Показать ответ
Потому что принять и сбросить журнал на диск дёшево, а проиграть его (replay) - дорого: startup-процесс на standby конкурирует за страницы и буферы с читающими запросами, если включён hot_standby. Поэтому flush_lsn догоняет sent_lsn быстро, а replay_lsn - медленнее. Разрыв flush-replay означает «журнал надёжно лежит на standby, но ещё не применён и не виден на чтении».
Чем опасен слот репликации, который остался активным после удаления standby?
Показать ответ
Слот заставляет primary держать WAL до restart_lsn слота. Если потребитель (standby) исчез, но слот не удалён, primary не имеет права удалять журнал, которого «ждёт» этот слот, - и копит сегменты бесконечно, пока не кончится место на диске. Поэтому неактивные слоты (active = false) с растущим retained-объёмом надо находить и удалять; это типовая причина «внезапно кончился диск на мастере».
В чём именно состоит выбор между асинхронной и синхронной репликацией?
Показать ответ
Асинхронная: COMMIT возвращается, не дожидаясь standby - быстро, но при падении primary последние транзакции могут не доехать до реплики и потеряются при failover. Синхронная: COMMIT ждёт подтверждения standby - потерь при failover нет, но каждый COMMIT платит задержкой в сетевой круг до реплики. Это выбор между задержкой и согласованностью, а не «что лучше вообще».
Зачем при failover увеличивается номер таймлайна?
Показать ответ
После промоушена новый primary начинает писать собственный WAL. Таймлайн - это номер ветки истории журнала; его увеличение отделяет журнал, написанный после промоушена, от старого. Без этого два сервера, разошедшиеся после аварии, генерировали бы записи с одинаковыми LSN на разном содержимом, и их нельзя было бы непротиворечиво совместить. Новый таймлайн делает «чей это WAL» однозначным.