Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Репликация PostgreSQL и read-реплики

Репликация PostgreSQL: streaming, реплики для чтения и failover

Как устроена streaming-репликация: WAL, hot standby и lag, маршрутизация чтения на реплики, failover-сценарии и консистентность чтения после записи.

Прямой ответ

Streaming-репликация Postgres — асинхронная (по умолчанию) передача WAL-журнала с primary на standby: реплика воспроизводит изменения физически (побайтово), оставаясь точной копией; в режиме hot standby она принимает запросы на чтение — отсюда «read-реплика». Что важно понимать разработчику до первого SELECT с реплики: (1) lag — реплика отстаёт (миллисекунды в норме, минуты при нагрузке/сети) — «записал и сразу прочитал с реплики — данных нет» — классический баг read-after-write (решения ниже); (2) реплика read-only — попытки записи падают (cannot execute INSERT in a read-only transaction) — маршрутизация чтения/записи должна быть явной (на уровне приложения/proxy — Postgres сам не «раздаёт» роли); (3) failover не автоматический — при смерти primary standby не становится главным сам: нужен Patroni/repmgr (автоматика с кворумом) или ручной promote (pg_ctl promote) — и это осознанный выбор, потому что авто-failover с потерей данных при асинхронной репликации возможен (последние транзакции не доехали); (4) слоты репликации удерживают WAL на primary (реплика отвалилась — WAL копится — диск primary кончается — max_slot_wal_keep_size обязателен; слоты также держат xmin — bloat-последствия). Типовая полезная нагрузка реплик: аналитические/отчётные запросы (не мешают OLTP — тяжёлые сканы), тяжёлый экспорт (выгрузки), резервное копирование (бэкап с реплики — primary не нагружен — PITR); реплика — не бэкап (случайный DROP TABLE реплицируется мгновенно — копия != резервная копия).

Механика: WAL, режимы, лаг

Первичная пишет WAL (журнал изменений) — walsender-процессы шлют его standby, walreceiver принимает, startup-процесс воспроизводит (recovery — физически: страницы, не SQL-команды); режимы синхронности: asynchronous (дефолт — primary коммитит не дожидаясь реплики — возможна потеря последних транзакций при failover), remote_write/on/remote_apply (synchronous_standby_names — коммит ждёт подтверждения реплики — remote_apply гарантирует, что реплика уже применила — read-your-writes из коробки ценой задержки каждого коммита и риска: синхронная реплика недоступна — коммиты primary встают — synchronous_commit=off локально/деградация — настройка по критичности данных). Измерение lag: pg_stat_replication на primary (write/flush/replay LSN против pg_current_wal_lsn() — байты отставания; replay_lag — время) и pg_last_wal_replay_lsn()/pg_is_in_recovery() на реплике; мониторинг — postgres_exporter-метрики + алерты (lag > 30 с — внимание, > 5 мин — page; рост lag — первичный симптом «реплика не тянет» — обычно тяжёлые запросы на самой реплике блокируют recovery или сеть/диск). Конфликты запросов: долгий запрос на реплике + восстановление конфликтующего WAL (vacuum чистит строки, нужные запросу) — запрос отменяется (canceling statement due to conflict with recovery); баланс — max_standby_streaming_delay (дефолт 30 с — сколько recovery ждёт запрос) и hot_standby_feedback=on (реплика сообщает primary «мне ещё нужны старые версии» — primary vacuum тормозит — bloat-цена — xmin-якоря); для аналитики с часовыми запросами — выделенная реплика с feedback и высоким delay, не «общая для всех».

Маршрутизация чтения и read-after-write

Варианты маршрутизации: приложение — явные «read-only сессии» для подходящих запросов (SQLAlchemy: bind=replica для репорт-движка/сессий чтения — сессии); прокси — PgBouncer сам не маршрутизирует по ролям (это пулер — PgBouncer), роль-роутинг делают HAProxy (health-check pg_is_in_recovery() — балансировка только по standby) или специализированные (Pgpool-II); DNS/сервис — два эндпоинта (db-write/db-read) — простейший честный вариант. Проблема read-after-write и её решения по возрастанию сложности: (1) не маршрутизировать на реплику критичные к свежести чтения (профиль после сохранения — с primary — правило в коде); (2) «липкая сессия» — после записи N секунд читать с primary (простейший таймер в middleware); (3) проверка LSN — запись вернула pg_current_wal_lsn(), чтение на реплике ждёт pg_last_wal_replay_lsn() >= lsn (точно, требует кода); (4) remote_apply синхронность (гарантия ценой латентности коммита); (5) временные метки/версии в данных (приложение сравнивает updated_at — «прочитал старее своего — перечитать с primary»). Для большинства продуктов схема (1)+(2) покрывает 99% кейсов — полная маршрутизация «всё чтение на реплику» без учёта свежести — источник трудноуловимых багов «данные мигают».

Failover, promote и жизненный цикл

Планируемый switchover (обслуживание/апгрейд): promote standby → перенаправить запись → старый primary переприсоединить репликой (pg_rewind — быстрое переподключение без полной копии — при расходимости timeline); автоматизируется Patroni (patronictl switchover) — системный подход к демонам кластера. Аварийный failover: Patroni/repmgr с DCS (etcd/consul — кворум против split-brain — два «primary» — катастрофа хуже простоя; автоматика выбирает standby с наибольшим LSN, promote, VIP/DNS-переключение); ручной promote (pg_ctl promote / SELECT pg_promote()) — без автоматики: рестарт приложения на новый адрес, старый primary — pg_rewind + настройка репликой; потеря данных при async — последние коммиты на умершем primary могли не доехать (оценка — по разнице LSN после поднятия; критичные данные — synchronous-реплика или принятие риска с записанным ADR). Реплика как PITR-источник — не замена бэкапам (бэкапы — отдельно, с реплики можно делать, но хранить независимо); проверка failover — учебная (раз в квартал убить primary на стейдже — проверить promote/переключение/потери — непроверенный failover — лотерея; game day-практика). Каскадные реплики (standby → standby) — разгрузка primary при многих читателях; primary_conninfo + слоты на каждом звене — lag-контроль по всей цепочке.

Практический сценарий: отчёты убивали прод, реплика спасла

Исходник: один primary; утренние отчёты аналитиков (тяжёлые агрегаты) + экспорт (выгрузки) — p95 API в 9:00–11:00 деградирует с 200 мс до 3 с (сканы съедают CPU/IO — OLTP страдает); бэкап ночью — дополнительные 40 минут нагрузки. Развёртывание: standby на отдельной машине (streaming async + слот с max_slot_wal_keep_size=50GB — диск primary защищён), HAProxy: backend-write → primary (health: не in recovery), backend-read → standby; приложение: репорт-движок и export-задачи переведены на read-эндпоинт (SQLAlchemy bind), OLTP — без изменений; max_standby_streaming_delay=120s + hot_standby_feedback=on на реплике (отчётные запросы до 20 мин — отмен нет; bloat-эффект на primary проверен — незначителен при агрессивном autovacuum); бэкап pgBackRest — со standby. Наблюдаемость: lag-метрика в Grafana (дашборд) + алерт 60 с; учебный failover на стейдже: kill primary → Patroni promote за 18 с → приложение переподключилось (DNS TTL 10 с) — потеря 0 транзакций (нагрузка была синхронно закоммичена до убийства — async-риск задокументирован: «до 5 с коммитов при пиковой записи» — приемлемо для продукта, записано в runbook). Итог: p95 OLTP в отчётные часы — 220 мс (ровный день), аналитика довольна (её запросы никто не отменяет), primary разгружен на ~35% IO; read-after-write баг пойман на стейдже («сохранил настройку — страница показала старое» — чтение профиля шло с реплики) — профиль переведён на primary (схема «липкой сессии» не понадобилась).

FAQ

Реплика против логической репликации (logical replication)? Streaming (физическая) — точная копия всего инстанса (все базы, побайтово, версии Postgres должны совпадать) — для HA/чтения; логическая (publication/subscription) — выборочные таблицы между разными базами/версиями (миграции, «отдать таблицу аналитикам», частичные копии) — репликой для чтения «всего» не является (индексы/последовательности не копируются автоматически — знать); для read-реплик — физическая, для точечной доставки данных — логическая.

Можно ли писать на реплику? Нет (hot standby — строго read-only; «записать локально на standby» — невозможно физически); паттерны «шардирование записи по нескольким primary» — это уже Citus/логические схемы, не standby; если чтение+запись на одном узле нужны — это про partition-шардинг или отдельные сервисы со своей БД (микросервисы).

Насколько большой lag допустим? По задаче: интерактивное чтение с ожиданием свежести — миллисекунды-секунды (иначе липкая сессия/primary); аналитика/отчёты — минуты приемлемы (свежесть в шапке отчёта честно — экспорт); бэкап-реплика — часы не важны; недопустим lag растущий (реплика не догоняет — деградирует в бесполезность + WAL на primary растёт — два алерта: абсолютный lag и тренд).

Сколько реплик разумно? Каждая реплика — WAL-поток с primary (walsender + сеть + слот) — десятки реплик с одного primary — нагрузка и риск (каскад снимает); типовые масштабы: 1 (HA+бэкап), 2–3 (HA+аналитика+чтение) — дальше каскады/логические копии/читалки на партициях; «реплика на каждый сервис» —usually признак, что сервису нужна своя проекция данных (логическая репликация/ETL — пайплайны), не копия всей БД.

Источники: документация PostgreSQL (warm standby, hot standby, admin functions) — ссылки в шапке.