Модуль: Базы данных · Уровень: Senior

TL;DR#

  • Физическая (streaming) репликация — побайтовая копия кластера на уровне WAL. Standby проигрывает WAL primary. Всё или ничего: реплицируется весь инстанс, версии PG должны совпадать, архитектура CPU тоже. Основа HA.
  • Логическая репликация — на уровне логических изменений строк (decode WAL → INSERT/UPDATE/DELETE). Выборочно по таблицам (publication/subscription), между разными мажорными версиями, можно писать в таблицы-подписчики. Цена — нет DDL, нужны PK/REPLICA IDENTITY, выше overhead.
  • sync vs async: synchronous_commit управляет, ждать ли подтверждения standby перед возвратом COMMIT. Async = низкая latency, риск потери данных при падении primary. Sync = durability, но latency растёт и primary может «зависнуть», если standby недоступен. quorum/ANY — компромисс.
  • Replication lag = задержка между primary и standby. Три фазы: write → flush → replay. Мониторинг через pg_stat_replication (на primary) и pg_stat_wal_receiver / pg_last_wal_replay_lsn() (на standby).
  • Чтение с реплик даёт масштабирование read-нагрузки, но порождает stale reads и проблему read-your-writes. Нужен sticky-routing или чтение свежих данных с primary.
  • Failover: pg_promote() переводит standby в primary. Опасность split-brain — нужен fencing. В проде это Patroni/repmgr + DCS (etcd/Consul), а не ручной promote.
  • Go: два пула *pgxpool.Pool / *sql.DB (writer → primary, reader → реплики), роутинг по типу запроса, с fallback на primary для read-your-writes.

Теория#

1. Физическая (streaming / WAL) репликация#

PostgreSQL пишет все изменения сначала в WAL (Write-Ahead Log) — последовательность записей, описывающих физические изменения страниц данных (блоков 8 КБ). Физическая репликация — это передача потока WAL с primary на standby и его непрерывное применение.

Ключевые свойства:

  • Бинарная, побайтовая идентичность кластера. Standby — точная копия primary на уровне файлов данных.
  • Реплицируется весь инстанс целиком (все базы, все таблицы). Нельзя выбрать отдельную таблицу.
  • Требования: одинаковая мажорная версия PostgreSQL, одинаковая архитектура (endianness, размер указателя), совместимые сборки.
  • Standby по умолчанию — read-only (если включён hot_standby = on, можно выполнять SELECT).

Как поток устроен:

  1. На primary процесс walsender читает WAL и шлёт его по протоколу репликации.
  2. На standby walreceiver принимает WAL, пишет его на диск.
  3. startup/recovery процесс проигрывает (replay) WAL, применяя изменения к файлам данных.

Hot standby — режим, когда standby принимает read-only запросы во время проигрывания WAL. Возникает конфликт: replay может удалить версии строк (vacuum cleanup), которые нужны долгому SELECT на реплике. PostgreSQL разрешает его, либо задерживая replay (max_standby_streaming_delay), либо отменяя запрос (ERROR: canceling statement due to conflict with recovery).

Настройка primary (postgresql.conf):

wal_level = replica            # минимум для физической репликации
max_wal_senders = 10           # сколько standby/pg_basebackup одновременно
wal_keep_size = '1GB'          # сколько WAL держать на случай отставания (без слота)
hot_standby = on

pg_hba.conf на primary — разрешить подключение по replication:

host  replication  repl_user  10.0.0.0/24  scram-sha-256

Создание standby через pg_basebackup:

pg_basebackup -h primary-host -U repl_user -D /var/lib/postgresql/data \
  -Fp -Xs -P -R -C -S standby1_slot
# -R создаёт standby.signal + primary_conninfo в postgresql.auto.conf
# -C -S создаёт replication slot standby1_slot

После старта postgresql.auto.conf standby содержит:

primary_conninfo = 'host=primary-host user=repl_user ... application_name=standby1'
primary_slot_name = 'standby1_slot'

Replication slots#

Слот — это объект на primary, который гарантирует, что primary не удалит WAL (и для логической — не «отвакуумит» строки), пока standby их не получил. Без слота при долгом отставании standby primary может удалить нужный WAL → standby «отвалится» и потребует пересоздания (или восстановления из архива).

-- физический слот
SELECT pg_create_physical_replication_slot('standby1_slot');

-- посмотреть слоты и сколько WAL «застряло»
SELECT slot_name, slot_type, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;

Подвох слотов: неактивный слот (standby умер и не вернулся) будет бесконечно держать WAL → переполнение pg_wal → primary падает по нехватке места на диске. Защита: max_slot_wal_keep_size (PG 13+) ограничивает объём удерживаемого WAL; при превышении слот инвалидируется, но primary выживает.

max_slot_wal_keep_size = '50GB'

Cascading replication#

Standby может сам отдавать WAL другим standby (walsender работает и на реплике), снижая нагрузку на primary.

2. Логическая репликация#

Логическая репликация декодирует WAL в логический поток изменений (через output plugin pgoutput) и применяет их как обычные SQL-операции на подписчике. Работает по модели publish/subscribe.

Отличия от физической:

СвойствоФизическая (streaming)Логическая
Уровеньбайты/блоки WALстроки (INSERT/UPDATE/DELETE)
Гранулярностьвесь кластервыбранные таблицы/публикации
Версии PGдолжны совпадатьможно между мажорными (10→16)
Архитектурадолжна совпадатьможет отличаться
Standby writable?нет (read-only)да (подписчик — обычная база)
DDLреплицируетсяНЕ реплицируется (до PG 16/17 ограниченно)
Накладные расходыминимальныевыше (decode + apply как SQL)
СценарииHA, read-replicasмажорный upgrade без downtime, выборочная синхронизация, ETL/CDC, мульти-источник

Publication на источнике:

CREATE PUBLICATION my_pub FOR TABLE orders, customers;
-- или для всех таблиц:
CREATE PUBLICATION all_pub FOR ALL TABLES;
-- выборочно по операциям и даже по строкам (PG 15+):
CREATE PUBLICATION active_orders FOR TABLE orders
  WHERE (status = 'active')
  WITH (publish = 'insert, update');

Subscription на приёмнике:

CREATE SUBSCRIPTION my_sub
  CONNECTION 'host=source-host dbname=app user=repl_user password=...'
  PUBLICATION my_pub
  WITH (copy_data = true, create_slot = true, slot_name = 'my_sub_slot');

Требования: wal_level = logical на источнике. Таблицы-подписчики должны существовать заранее (схема не реплицируется). Каждой таблице нужен REPLICA IDENTITY для UPDATE/DELETE:

-- по умолчанию = PRIMARY KEY. Если PK нет:
ALTER TABLE orders REPLICA IDENTITY FULL;   -- сравнение по всем колонкам (дорого)
-- или по уникальному индексу:
ALTER TABLE orders REPLICA IDENTITY USING INDEX orders_uniq_idx;

Мониторинг логической:

-- на подписчике
SELECT subname, received_lsn, latest_end_lsn, last_msg_send_time
FROM pg_stat_subscription;

-- ошибки применения (PG 15+)
SELECT * FROM pg_stat_subscription_stats;

Подводные камни логической репликации:

  • DDL не реплицируется — добавили колонку на источнике, забыли на подписчике → ошибка применения, репликация встаёт.
  • Конфликты (дубликат PK на подписчике) останавливают подписку до ручного вмешательства (skip LSN или pg_replication_origin_advance).
  • Sequences не синхронизируются автоматически (до PG 16). После failover на логическую копию нужно вручную выставить sequence.
  • TRUNCATE реплицируется с PG 11. Большие транзакции стримятся с PG 14 (streaming = on).
  • Начальный copy_data снимок может быть тяжёлым для больших таблиц.

3. Synchronous vs Asynchronous#

synchronous_commit определяет, на каком этапе COMMIT возвращает управление клиенту.

ЗначениеСемантикаDurability при падении primary
offне ждёт даже локального fsync WALможно потерять последние транзакции даже без репликации
localждёт локальный fsync WAL, не ждёт standbyпотеря данных, не дошедших до standby (async-репликация по факту)
remote_writestandby принял и записал в ОС (не fsync)потеря при одновременном крахе ОС standby
onstandby сделал fsync WALнет потери при падении primary (есть на standby)
remote_applystandby применил WAL (виден в SELECT на реплике)read-your-writes на реплике гарантирован

synchronous_standby_names на primary определяет, какие standby считаются синхронными:

# первый доступный из списка
synchronous_standby_names = 'standby1, standby2'

# FIRST N — ждать первых N из списка по приоритету
synchronous_standby_names = 'FIRST 1 (standby1, standby2, standby3)'

# ANY N — кворум: ждать ЛЮБЫХ N из списка (PG 9.6+)
synchronous_standby_names = 'ANY 2 (s1, s2, s3, s4)'

application_name standby (из primary_conninfo) должен совпадать с именами в этом списке.

Компромисс latency vs durability:

  • Синхронная репликация добавляет к каждому COMMIT сетевой round-trip до standby. Под нагрузкой это резко увеличивает latency записи и снижает throughput.
  • Если синхронный standby недоступен и нет других кандидатов, COMMIT на primary зависает (primary доступен на запись, но коммиты блокируются). Это сознательный выбор: durability важнее доступности.
  • Решение для баланса: ANY 1 (s1, s2) — нужен ответ любого одного из двух. Один может упасть без остановки primary.

Можно выставлять synchronous_commit на уровне транзакции/сессии: критичные платежи — remote_apply, фоновые логи — off.

SET synchronous_commit = 'off';  -- для конкретной сессии/транзакции

4. Replication lag#

Lag — насколько standby отстаёт от primary. Измеряется в LSN (объём WAL) и во времени.

Три фазы (для каждого standby):

  • write lag — WAL дошёл до standby, но ещё не записан на диск standby.
  • flush lag — WAL записан (fsync) на standby.
  • replay lag — WAL применён к файлам данных и виден для запросов.

replay_lsn всегда ≤ flush_lsnwrite_lsnsent_lsn.

Причины lag:

  • Высокий объём записи на primary (поток WAL шире, чем сеть/диск standby).
  • Медленный диск или CPU на standby (replay — однопоточный startup-процесс, не параллелится так, как параллельные бэкенды на primary).
  • Долгие запросы/локи на standby с hot_standby_feedback, блокирующие replay (конфликт recovery).
  • Сетевые задержки между ЦОД.
  • Контрольные точки / тяжёлые batch-операции (VACUUM FULL, массовый UPDATE, индексация).

Мониторинг на primary:

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

-- лаг в байтах для каждого standby:
SELECT application_name,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_lag_bytes
FROM pg_stat_replication;

Мониторинг на standby:

-- лаг по времени (на самом standby)
SELECT now() - pg_last_xact_replay_timestamp() AS replication_delay;

-- состояние приёмника
SELECT status, received_lsn, latest_end_lsn, last_msg_receipt_time
FROM pg_stat_wal_receiver;

SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();

pg_last_xact_replay_timestamp() врёт, если на primary нет транзакций (lag будет «расти», хотя реально standby в актуальном состоянии — нечего применять). Поэтому для алертов комбинируйте LSN-diff с временной метрикой.

5. Чтение с реплик и его подвохи#

Чтение с реплик масштабирует read-нагрузку, но реплики eventually consistent (async) — данные на них могут отставать.

Read-your-writes проблема: пользователь записал данные (на primary), сразу читает (с реплики), но реплика ещё не получила изменение → видит старое состояние. Классика: создал заказ → редирект на список → заказа нет.

Stale reads: любой SELECT с реплики может вернуть устаревшие данные. Опасно для бизнес-логики, основанной на свежем состоянии (проверка баланса перед списанием).

Стратегии маршрутизации:

  1. Write → primary, read → реплики по умолчанию. Простой выигрыш для read-heavy нагрузки.
  2. Sticky-to-primary после записи: в течение N секунд / до конца сессии после write читать с primary. Решает read-your-writes.
  3. LSN-tracking (causal consistency): после write запоминаем pg_current_wal_lsn(), при чтении ждём, пока реплика догонит этот LSN (pg_wal_replay_wait в PG 17, или проверка pg_last_wal_replay_lsn() ≥ нужного, иначе fallback на primary).
  4. synchronous_commit = remote_apply для критичных записей — гарантирует, что синхронная реплика уже видит изменение.
-- PG 17: дождаться применения конкретного LSN на реплике перед чтением
CALL pg_wal_replay_wait('0/170E180', timeout => 1000);

hot_standby_feedback и его влияние на vacuum primary:

По умолчанию primary не знает о долгих запросах на standby и может через VACUUM удалить (или заморозить) версии строк, которые ещё нужны запросу на реплике → конфликт recovery → запрос отменяется (canceling statement due to conflict with recovery).

hot_standby_feedback = on на standby заставляет standby сообщать primary самый старый xmin, который ему нужен. Primary тогда не вакуумит эти версии строк.

# на standby
hot_standby_feedback = on

Цена: на primary накапливается «мусор» (dead tuples), который нельзя вычистить, пока долгий запрос на реплике жив → table bloat, рост размера таблиц и индексов, деградация. Долгий зависший запрос на реплике (или idle-in-transaction) фактически блокирует VACUUM на primary — это частая причина незаметного раздувания базы. Альтернатива без feedback — увеличить max_standby_streaming_delay, разрешая standby отставать ради завершения запросов.

6. Failover#

Promote — перевод standby в роль primary:

SELECT pg_promote();           -- из SQL (PG 12+)
pg_ctl promote -D /var/lib/postgresql/data

После promote standby перестаёт быть read-only, начинает новую timeline (timeline ID +1) — это важно, чтобы старый primary и другие standby не «смешали» WAL разных историй.

Split-brain — два узла одновременно считают себя primary и принимают записи → расхождение данных, потеря записей при «склейке». Главная опасность автоматического failover.

Fencing (STONITH) — гарантия, что старый primary точно не принимает записи перед promote нового: отключение сети, остановка сервиса, IPMI-reset, отзыв VIP. Без fencing нельзя безопасно автоматизировать failover.

Recovery старого primary: после failover бывший primary отстаёт и имеет «расходящуюся» timeline. Чтобы вернуть его как standby к новому primary, используется pg_rewind — синхронизирует только разошедшиеся блоки, не делая полный basebackup:

pg_rewind --target-pgdata=/var/lib/postgresql/data \
  --source-server='host=new-primary user=repl_user ...' -R

Требует wal_log_hints = on (или data checksums) на узлах.

Инструменты HA:

ИнструментСуть
PatroniШаблон HA на Python; лидер-выборы через DCS (etcd/Consul/ZooKeeper); автоматический failover, fencing, REST API, интеграция с HAProxy/pgBouncer. Де-факто стандарт.
repmgrУправление репликацией и failover, witness-узел против split-brain, repmgrd-демон.
pg_auto_failoverMicrosoft; отдельный monitor-узел, проще Patroni.
StolonHA на Go, поверх etcd/Consul, k8s-friendly.

Patroni использует DCS как источник истины: лидер держит lock с TTL; если не продлил — другой узел берёт lock и промоутится. DCS-консенсус (Raft) предотвращает split-brain — лидером может быть только тот, кто держит lock. Приложение ходит не напрямую, а через HAProxy (опрашивает Patroni REST /primary, /replica) или service discovery.

7. Go: разделение connection на read/write пулы#

Идея: два пула — один на primary (writer), один на реплику/балансировщик реплик (reader). Роутинг по типу операции.

pgx (pgxpool):

type DB struct {
    writer *pgxpool.Pool // primary
    reader *pgxpool.Pool // реплика(и) / HAProxy reader-порт
}

func New(ctx context.Context, primaryDSN, replicaDSN string) (*DB, error) {
    w, err := pgxpool.New(ctx, primaryDSN)
    if err != nil {
        return nil, err
    }
    r, err := pgxpool.New(ctx, replicaDSN)
    if err != nil {
        w.Close()
        return nil, err
    }
    return &DB{writer: w, reader: r}, nil
}

// Чтение по умолчанию с реплики.
func (db *DB) QueryRead(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
    return db.reader.Query(ctx, sql, args...)
}

// Запись и read-your-writes — на primary.
func (db *DB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
    return db.writer.Exec(ctx, sql, args...)
}

// Транзакция всегда на primary (она может писать).
func (db *DB) Begin(ctx context.Context) (pgx.Tx, error) {
    return db.writer.Begin(ctx)
}

Read-your-writes через контекст (sticky-to-primary):

type ctxKey struct{}

// помечаем контекст после записи: следующие чтения идут на primary
func ForcePrimary(ctx context.Context) context.Context {
    return context.WithValue(ctx, ctxKey{}, true)
}

func (db *DB) pool(ctx context.Context) *pgxpool.Pool {
    if v, _ := ctx.Value(ctxKey{}).(bool); v {
        return db.writer
    }
    return db.reader
}

func (db *DB) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
    return db.pool(ctx).Query(ctx, sql, args...)
}

database/sql — то же с двумя *sql.DB. Балансировку нескольких реплик отдают либо HAProxy/pgBouncer (одна DSN на reader-порт), либо клиентскому списку хостов в connection string (host=r1,r2 target_session_attrs=...).

target_session_attrs (libpq/pgx) для авто-выбора узла из списка хостов:

postgres://r1,r2,r3/app?target_session_attrs=read-only      # только реплики
postgres://p,s1,s2/app?target_session_attrs=read-write      # только узел, принимающий запись (= primary)
postgres://...?target_session_attrs=prefer-standby

Это даёт примитивный failover на стороне клиента: при потере primary read-write найдёт новый promote’нутый узел.

Практические замечания для Go-сервиса:

  • Раздельные таймауты/размеры пулов: writer обычно меньше и «дороже», reader масштабируется.
  • Метрики: экспортировать pool.Stat() (AcquiredConns, IdleConns, TotalConns) по обоим пулам.
  • Не открывать транзакцию на reader для записи — получите cannot execute INSERT in a read-only transaction.
  • Health-check реплики: при большом lag временно слать чтения на primary (circuit breaker по метрике lag из pg_stat_wal_receiver).

Подводные камни / gotchas#

  • Неактивный replication slot заполняет диск primary. Standby упал и не вернулся — слот вечно держит WAL → pg_wal растёт → primary падает. Лечится max_slot_wal_keep_size и алертами на неактивные слоты.
  • Синхронная репликация без кворума = единая точка отказа на запись. synchronous_standby_names = 'standby1' + standby1 недоступен → все COMMIT висят. Используйте ANY N или мониторьте.
  • hot_standby_feedback = on → bloat на primary. Долгий/зависший запрос на реплике блокирует VACUUM. Следите за idle in transaction и долгими запросами на standby.
  • Логическая репликация молча встаёт при конфликте/DDL. Добавили колонку на источнике — apply падает на подписчике, lag растёт. Нужен мониторинг pg_stat_subscription и алерты.
  • Sequences не реплицируются логически (до PG 16). После failover на логическую копию — дубликаты ключей. Выставляйте sequence вручную.
  • pg_last_xact_replay_timestamp() показывает «фейковый лаг» при простое primary (нет новых транзакций). Комбинируйте с LSN-diff.
  • Split-brain при ручном/наивном авто-failover. Без fencing старый primary продолжит принимать записи. Никогда не автоматизируйте promote без DCS-консенсуса и fencing.
  • Конфликт recovery на standby отменяет запросы (canceling statement due to conflict with recovery). Либо hot_standby_feedback (ценой bloat), либо max_standby_streaming_delay (ценой lag).
  • timeline mismatch после failover. Старые standby не подключатся к новому primary без pg_rewind или восстановления через общий WAL-архив (restore_command).
  • REPLICA IDENTITY по умолчанию = PK. Таблица без PK → UPDATE/DELETE не реплицируются логически (или требуют дорогой FULL).
  • Чтение с реплики в той же бизнес-операции, что и запись, без sticky-логики = баги «исчезающих данных».

Вопросы на собеседовании#

В: В чём принципиальная разница между физической и логической репликацией и когда что выбирать? О: Физическая работает на уровне WAL (байты/блоки), копирует весь кластер побайтово, требует одинаковых версий и архитектуры, standby read-only — это HA и read-replicas. Логическая декодирует WAL в строки (INSERT/UPDATE/DELETE), реплицирует выбранные таблицы между разными версиями, подписчик writable — это zero-downtime мажорный upgrade, выборочная синхронизация, CDC/ETL. Логическая не реплицирует DDL и дороже по overhead.

В: Что делает replication slot и какой главный риск с ним связан? О: Слот гарантирует, что primary не удалит WAL (а для логических — не вакуумит нужные версии строк), пока standby их не получил, защищая от рассинхрона при отставании. Риск: неактивный слот (упавший standby) бесконечно держит WAL → переполнение pg_wal → падение primary. Защита — max_slot_wal_keep_size и мониторинг active = false.

В: Объясните разницу между synchronous_commit = on, remote_write, remote_apply и local. О: local — ждём только локальный fsync, не ждём standby (фактически async). remote_write — standby принял и записал в ОС, но без fsync. on — standby сделал fsync WAL (нет потери при падении primary). remote_apply — standby применил WAL и изменение видно в его SELECT (даёт read-your-writes на реплике). Чем сильнее гарантия, тем выше latency коммита.

В: Что произойдёт, если единственный синхронный standby станет недоступен? О: Все COMMIT на primary зависнут — primary жив, но коммиты блокируются, ожидая подтверждения недоступного standby. Это by design: durability приоритетнее. Чтобы избежать остановки, используют ANY N (...) (кворум) с несколькими кандидатами или несколько синхронных standby в FIRST N.

В: Какие фазы replication lag существуют и как его мониторить? О: write (WAL дошёл до standby), flush (записан/fsync на standby), replay (применён и виден запросам). На primary — pg_stat_replication (write_lag, flush_lag, replay_lag, LSN-поля). На standby — pg_stat_wal_receiver, pg_last_wal_replay_lsn(), now() - pg_last_xact_replay_timestamp(). Лаг в байтах через pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn).

В: Что такое read-your-writes проблема при чтении с реплик и как её решить? О: Пользователь пишет на primary и сразу читает с реплики, которая ещё не получила изменение → видит старые данные. Решения: sticky-to-primary после записи (читать с primary N секунд), LSN-tracking (запомнить LSN записи, дождаться его применения на реплике или fallback на primary, pg_wal_replay_wait в PG 17), либо synchronous_commit = remote_apply для критичных записей.

В: Как hot_standby_feedback влияет на primary? О: Standby сообщает primary свой минимальный нужный xmin, чтобы VACUUM на primary не удалял версии строк, которые читает долгий запрос на реплике (избегаем conflict with recovery). Цена — VACUUM на primary не может вычистить эти dead tuples, что ведёт к bloat таблиц и индексов. Долгий/зависший запрос на реплике фактически блокирует очистку на primary.

В: Что такое split-brain и как HA-инструменты его предотвращают? О: Split-brain — два узла одновременно считают себя primary и принимают записи, данные расходятся. Предотвращение: DCS-консенсус (etcd/Consul с Raft) — лидером может быть только держатель распределённого lock; fencing/STONITH старого primary (отзыв VIP, остановка сервиса) перед promote нового. Patroni/repmgr реализуют это автоматически; ручной pg_promote без fencing опасен.

В: Как организовать read/write splitting в Go-приложении? О: Два пула — writer на primary, reader на реплику(и)/HAProxy. Записи и транзакции → writer, обычные SELECT → reader. Read-your-writes решаем sticky-флагом в контексте (после записи следующие чтения идут на primary) или LSN-tracking. Балансировку реплик отдаём HAProxy/pgBouncer или target_session_attrs в connection string; мониторим pool.Stat() и lag реплики для circuit breaker.

В: Зачем нужен pg_rewind после failover? О: После promote старый primary имеет разошедшуюся timeline и не может просто стать standby нового primary. pg_rewind синхронизирует только разошедшиеся блоки данных (а не делает полный basebackup), быстро возвращая старый узел в кластер как standby. Требует wal_log_hints = on или data checksums.

На что копают на senior+#

  • Durability-математика sync-репликации. Понимаете ли вы, что synchronous_commit = on защищает от потери на primary, но НЕ гарантирует, что данные применены и видны на standby (это remote_apply)? Что async может потерять подтверждённые клиенту транзакции при крахе primary?
  • Кворумная репликация и trade-off CAP. ANY N vs FIRST N, как выбрать N относительно числа реплик и требований к доступности записи vs durability. Геораспределённые синхронные реплики и влияние RTT на latency коммита.
  • Конфликт recovery vs vacuum. Глубокое понимание треугольника hot_standby_feedback ↔ bloat на primary ↔ max_standby_streaming_delay ↔ отмена запросов. Как диагностировать bloat, вызванный зависшим запросом на реплике.
  • Логическая репликация в проде. Обработка конфликтов (skip LSN, pg_replication_origin_advance), стриминг больших транзакций (PG 14+), DDL-проблема и как её обходят (вручную или через инструменты вроде pglogical/обёртки), синхронизация sequences, мониторинг застрявшей подписки.
  • Causal consistency на уровне приложения. LSN-tracking, pg_wal_replay_wait (PG 17), session-token/cookie с LSN для маршрутизации чтений — умеете ли спроектировать без глобального sticky-to-primary, который убивает смысл реплик.
  • Анатомия failover. Роль DCS и Raft, TTL lock, fencing/STONITH, watchdog в Patroni, timeline divergence, pg_rewind vs полный rebuild, как приложение узнаёт о новом primary (HAProxy + Patroni REST, target_session_attrs, service discovery), оценка RTO/RPO.
  • Производительность replay. Почему replay однопоточный и становится бутылочным горлышком, recovery_prefetch (PG 15+), влияние тяжёлых операций (массовые UPDATE, индексация) на lag, cascading replication для разгрузки primary.
  • Пулинг и реплики. Взаимодействие pgBouncer (transaction pooling) с read/write split, почему prepared statements и target_session_attrs ломаются в transaction-pooling режиме, мониторинг и circuit breaker по lag в Go-сервисе.