Модуль: Базы данных · Уровень: 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).
Как поток устроен:
- На primary процесс
walsenderчитает WAL и шлёт его по протоколу репликации. - На standby
walreceiverпринимает WAL, пишет его на диск. 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 = onpg_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_write | standby принял и записал в ОС (не fsync) | потеря при одновременном крахе ОС standby |
on | standby сделал fsync WAL | нет потери при падении primary (есть на standby) |
remote_apply | standby применил 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_lsn ≤ write_lsn ≤ sent_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 с реплики может вернуть устаревшие данные. Опасно для бизнес-логики, основанной на свежем состоянии (проверка баланса перед списанием).
Стратегии маршрутизации:
- Write → primary, read → реплики по умолчанию. Простой выигрыш для read-heavy нагрузки.
- Sticky-to-primary после записи: в течение N секунд / до конца сессии после write читать с primary. Решает read-your-writes.
- LSN-tracking (causal consistency): после write запоминаем
pg_current_wal_lsn(), при чтении ждём, пока реплика догонит этот LSN (pg_wal_replay_waitв PG 17, или проверкаpg_last_wal_replay_lsn()≥ нужного, иначе fallback на primary). 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_failover | Microsoft; отдельный monitor-узел, проще Patroni. |
| Stolon | HA на 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 NvsFIRST 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_rewindvs полный 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-сервисе.