Репликация в PostgreSQL#
Модуль: Базы данных · Уровень: 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.
Простыми словами#
Репликация — это копия базы, которая повторяет за основной все изменения.
Нужна она для двух разных вещей, и их стоит различать: пережить смерть основного сервера (отказоустойчивость) и разгрузить его от чтения (масштабирование).
Видов два. Физическая — побайтовая копия всего кластера: реплика проигрывает журнал WAL основного сервера. Всё или ничего, версии PostgreSQL должны совпадать. Это основа отказоустойчивости. Логическая — построчная и выборочная: можно взять три таблицы, отправить их в базу другой версии и даже писать в них на стороне подписчика. Так делают миграции между мажорными версиями без простоя.
Синхронная или асинхронная — вопрос про то, ждать ли подтверждения реплики перед ответом «сохранено». Асинхронная быстрее, но при внезапной смерти основного сервера последние транзакции потеряются. Синхронная надёжнее, но каждый коммит теперь стоит похода по сети, а если реплика отвалилась, основной может встать колом.
И главная ловушка чтения с реплик: устаревшие чтения. Пользователь сохранил профиль, приложение тут же читает его с реплики, а изменения туда ещё не доехали — пользователь видит старые данные и жмёт «сохранить» ещё раз. Лечится чтением своих свежих записей с основного сервера.
Если ты с другого языка. Схема универсальна для всех СУБД. Что специфично: в PostgreSQL нет встроенного автоматического переключения при отказе — pg_promote надо кому-то вызвать. В проде для этого ставят Patroni, иначе легко получить два «основных» сервера одновременно.
Словарь этой статьи:
- primary / standby — основной сервер и реплика;
- replication lag — отставание реплики; измеряется в байтах WAL или в секундах;
- failover — переключение на реплику при отказе; split-brain — авария, когда основными считают себя двое;
- replication slot — механизм, гарантирующий, что основной не удалит WAL, который реплика ещё не забрала (и переполнит диск, если реплика умерла надолго).
Теория#
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-сервисе.