Database Replication Deep Dive: synchronous vs async vs semi-sync replication, WAL/binlog/oplog physical streaming, cascading and delayed replicas. Topology shows app writer/reader, Postgres leader, sync replica (remote_apply), two async replicas, plus a DR/cascading group with intermediate replica feeding cascaded replicas and a 1h delayed replica. Five scenarios: (1) sync commit waiting for replica ack, (2) async stale read pitfall (read-your-writes violation), (3) MySQL semi-sync silent degrade to async on timeout with crash, (4) read-your-writes routing using LSN to leader, (5) delayed replica rescuing from human-error DELETE.
Базовый replication показывает три топологии (single-leader / multi-leader / leaderless) и три режима (sync / async / semi-sync) на пальцах. В реальной эксплуатации этого не хватает: на проде ломается не «концепция», а детали реализации конкретной СУБД. Semi-sync под нагрузкой тихо деградирует в async. Cascading replicas копят lag на каждом hop. Async replica отдаёт пользователю «своё же» сообщение, которого он не видит. Delayed replica спасает после DELETE без WHERE, но только если её настроили заранее.
Этот гайд — для DBA / SRE, который уже несколько раз делал failover и теперь решает:
synchronous_commit ставить для платёжного кластера vs ленты постов,Репликация — это не «sync vs async», а матрица из четырёх вопросов:
- Чего ждёт
COMMITперед ACK клиенту?- Что считается «ack» от replica (получил WAL / записал в OS buffer / fsync / apply)?
- Что происходит при таймауте ack — failure или silent fallback в async?
- Как читатель узнаёт, что replica догнала его собственный write?
Любая СУБД отвечает на эти вопросы по-своему. Различия в ответах определяют RPO, p99 commit latency и поведение в incident'е. Поэтому единственное «правильное» решение — per-workload: ledger и timeline не могут жить в одном режиме.
Replication lag — это не «3ms», это распределение с длинным хвостом: медианно может быть 1ms, p99 — 200ms, p99.9 — 5s (vacuum, WAL flood, cross-region blip). Любая стратегия, которая молча предполагает «replica догонит к моменту запроса», обязательно сломается на хвосте.
Сцена — Postgres-кластер в DC-A плюс DR / cascading-плечо в DC-B:
app-writer (INSERT/UPDATE) и app-reader (read query). Разделены умышленно: пишущий путь и читающий путь имеют разные SLA.leader (WAL writer), replica-sync (synchronous_standby_names, режим remote_apply), две replica-async-* для read-scaling.intermediate replica подписана на leader, а уже от неё расходятся cascade-1 / cascade-2 (cascading) и replica-delayed (recovery_min_apply_delay = '1h').Edges соответствуют физическим связям: WAL-стрим от leader → каждая прямая replica, дальше от intermediate ветвление в DC-B. Reads-edges от app-reader идут к leader (для RYW) и к async replicas (для масштабирования). Ответы на запросы анимируются обратно по тем же edges — отдельных reverse-edges не создаём (см. CLAUDE.md, раздел про edges vs animation).
Слой cascading включает DR-плечо; в base-слое его видно как затемнённую зону, чтобы сначала разобраться с основным кластером.
sync-commit — что значит «zero loss». Write идёт app-writer → leader, leader делает локальный WAL fsync, шлёт запись в replica-sync, ждёт ACK уровня remote_apply (запись уже применена на replica, не просто получена), и только после этого возвращает клиенту 200. Цена — local_fsync + 1 × RTT. Выгода — RPO = 0 на failover в пределах кластера. Это режим для ledger / billing.
async-stale — где ловится stale read. Leader коммитит локально, моментально отвечает клиенту, WAL уходит на async replicas в фоне. Через 250ms app-reader спрашивает replica-async-1 про только что вставленный пост #999 — и получает 404. Это нарушение read-your-writes, классическая жалоба «я написал, но в ленте не вижу». Лечится либо sticky-to-leader, либо передачей LSN с клиента (см. сценарий 4).
semi-sync-degrade — silent fallback, на который не алертит никто. MySQL rpl_semi_sync_master_timeout (default 10s) при недоступной sync replica молча переводит leader в async. Клиенты продолжают получать ACK'и, но WAL никто не подтверждает. Если leader упадёт в этом окне — все эти writes потеряны, хотя приложение видело успешный commit. Postgres ведёт себя аналогично: если в synchronous_standby_names = 'ANY 1 (...)' все standby недоступны, поведение зависит от synchronous_commit_timeout / oversight оператора. Обязательное правило: алерт на Rpl_semi_sync_status=OFF (MySQL) или pg_stat_replication.sync_state != 'sync' (Postgres) с пейджингом, не email'ом.
read-your-writes — как читать с replicas без вранья. После commit leader возвращает LSN (0/16A3F8 в сценарии). Клиент кладёт LSN в session / cookie / sticky-route. Следующий read в окне (обычно 5-10s) роутится либо на leader, либо на replica, у которой pg_last_wal_replay_lsn() >= 0/16A3F8. После окна — обычные reads на любую replica. Альтернативы: hybrid logical clock (CockroachDB), bounded staleness reads (Spanner).
delayed-dr — единственный реальный сценарий, где recovery_min_apply_delay спасает. Junior выполняет DELETE FROM users; без WHERE. Через секунды это применилось на leader, async replicas, intermediate, обоих cascade replicas. На replica-delayed WAL уже принят и лежит на диске — но apply отложен на час. У ops есть 60 минут, чтобы остановить replica, снять pg_dump нужных таблиц и восстановить в leader. PITR из backup'ов сделает то же самое, но медленнее (15-60 минут vs минута) и тяжелее по ресурсам.
Context. При построении HA нужно выбрать режим репликации между primary и standby. Этот выбор управляет тремя осями одновременно: durability (сколько данных теряется при отказе leader), commit latency (сколько ждёт клиент), availability (что происходит при медленной/недоступной replica).
Decision matrix.
| Workload | Mode | RPO | Commit latency | Failover loss |
|---|---|---|---|---|
| Payments / ledger / billing | sync remote_apply, quorum | 0 | local + 1×RTT | 0 |
| User-generated content (posts/comm.) | semi-sync (ANY 1) | sub-second | local + 1×RTT | seconds |
| Analytics / metrics ingestion | async | seconds | local fsync only | seconds-min |
| Geo-replicated read-replicas | async + periodic sync для RYW | per-route | local (+ cross-RTT для sync slot) | depends |
Forces.
remote_apply ко всем standby = любая медленная replica становится SPOF. Решение — quorum (ANY 1 (r1, r2, r3)), который ждёт ack от любого одного из N.semi_sync_status ваш «semi-sync кластер» — это async со штампом «sync» на схеме. Это бомба замедленного действия, которая выстрелит ровно при failover, когда хочется иметь zero loss.Decision (default для новых кластеров).
synchronous_standby_names = 'ANY 1 (r1, r2, r3)', synchronous_commit = remote_apply. Минимум 3 standby, чтобы потеря одного не делала кластер degraded.ANY 1) с обязательным алертом на degrade. synchronous_commit = remote_write (быстрее remote_apply, но WAL гарантированно в OS buffer standby — этого достаточно для большинства failover-сценариев).synchronous_commit = local). Loss window ≤ replication lag = acceptable.leader → intermediate → N × read-replicas). Один WAL stream от leader, fanout в cascading layer.recovery_min_apply_delay = '1h') плюс PITR backup window. Не «или». Delayed ловит human error быстро; PITR ловит то, что заметили через сутки.Consequences (positive). Per-workload tuning — нет «единственно правильного» режима. Quorum предотвращает SPOF на медленной replica. Delayed replica ловит human error в первый час без участия backup-системы.
Consequences (negative). Sync mode добавляет latency, видимый в p99 → нужен capacity headroom на standby (медленный standby = медленный leader). Semi-sync обязывает к мониторингу — без него кластер незаметно превращается в async-кластер с «sync»-этикеткой. Cascading увеличивает end-to-end lag (сумма по hop'ам) и требует мониторинга lag на каждом уровне дерева.
Alternatives considered.
CREATE PUBLICATION / SUBSCRIPTION, можно подмножество таблиц), cascading через primary_conninfo указывающий на upstream replica. HA: Patroni (etcd/Consul/ZK) — лидер в проде, ~10-30s failover. Aurora Postgres — кастомный shared-storage layer, replica «promote» near-instant.rpl_semi_sync_* плагины, Group Replication (Paxos, multi-primary опционально). HA: Orchestrator для обнаружения и автоматического failover + ProxySQL для routing, InnoDB Cluster как «батарейки». Sharding — Vitess (используется YouTube / Slack / GitHub).writeConcern: {w: "majority", wtimeout: ...} — но wtimeout означает «commit локально успешен, но не реплицирован» (та же ловушка, что у semi-sync).ANY / ONE / QUORUM / ALL). RYW реализуется выбором R + W > N.semi_sync_status обязателен, страница — не email.pg_wal. Конец — read-only mode или crash.Тонкости из этого гайда — для production-кластера с реальными SLA. Они избыточны и вредны, если:
synchronous_commit руками может конфликтовать с automation. Сначала проверьте, что provider'ского режима не хватает.synchronous_commit reference.wtimeout и почему это не «отказ записи».docs/SETTINGS.rst и watchdog-секцию.::concept{slug="replication"} (базовая теория), ::concept{slug="wal-write-ahead-log"} (откуда WAL), ::concept{slug="change-data-capture"} (logical decoding в CDC pipeline), ::case{slug="payment-system"} (где sync обязателен), ::case{slug="disaster-recovery"} (delayed + PITR на практике).