Репликация и eventual consistency
Репликация и eventual consistency
1) Зачем eventual consistency
Когда система распределена по зонам/регионам, синхронная запись везде дает высокую латентность и низкую доступность при сетевых сбоях. Eventual consistency (EC) допускает временную рассинхронизацию реплик ради:- низкой задержки записи (локальный прием),
- лучшей доступности при разделениях сети,
- горизонтального масштабирования.
Ключевая задача — контролируемая нестрогая согласованность: пользователь видит «достаточно свежие» данные, инварианты домена сохраняются, конфликты детектируются и разрешаются предсказуемо.
2) Модели согласованности — что обещаем клиенту
Strong: чтение сразу видит последнюю запись.
Bounded stale / read-not-older-than (RNOT): чтение не старее отметки (LSN/версия/время).
Causal: сохраняются «причинно-следственные» отношения (A до B).
Read-Your-Writes: клиент видит свои недавние записи.
Monotonic Reads: каждое следующее чтение не «откатывается» назад.
Session: набор гарантий в рамках одной сессии.
Eventual: при отсутствии новых записей все реплики сходятся.
Практика: комбинируйте Session + RNOT на критичных путях и Eventual на витринах/кэшах.
3) Репликация: механики и anti-entropy
Синхронная (кворум/RAFT): запись считается успешной после подтверждения N узлами; минимальный RPO, выше p99.
Асинхронная: лидер коммитит локально, раздает журнал позже; низкая латентность, RPO > 0.
Физическая (WAL/binlog): быстрая, гомогенная.
Логическая/CDC: поток изменений на уровне строк/событий, гибкая маршрутизация, фильтры.
Anti-entropy: периодические сверки и починка (Merkle-деревья, сравнение хэшей, фоновый re-sync).
4) Идентификаторы версии и заказы причинности
Монотонные версии: increment/LSN/epoch; просто, но не кодируют параллелизм.
Lamport timestamp: частичный порядок по логическим часам.
Vector clock: фиксирует параллельные ветви и позволяет детектировать конфликтные апдейты (concurrent).
Hybrid/TrueTime/Clock-SI: логика «не раньше T» для глобального порядка.
Рекомендация: для CRDT/конфликтных апдейтов — vector clock; для «не старее» — LSN/GTID.
5) Конфликты: обнаружение и разрешение
Типовые ситуации: запись из двух регионов в один и тот же объект.
Стратегии:1. Last-Write-Wins (LWW) по часовому/логическому штампу — просто, но может «потерять» апдейты.
2. Merge-функции по доменной логике:- поля-счетчики складываются (G-Counter/PN-Counter),
- множества объединяются с «add-wins/remove-wins»,
- суммы/балансы — только через транзакционные журналы, не через простое LWW.
- 3. CRDT (конвергентные типы): G-Counter, OR-Set, LWW-Register, RGA для списков.
- 4. Операционные трансформации (редко для БД, чаще для редакторов).
- 5. Manual resolution: конфликт в «inbox», пользователь выбирает верную версию.
Правило: инварианты домена диктуют стратегию. Для денег/остатков — избегайте LWW; используйте транзакции/события с компенсацией.
6) Гарантии записей и идемпотентность
Идемпотентные ключи на командах (payment, withdraw, create) → повтор безопасен.
Дедупликация на «входе» (inbox) и «выходе» (outbox) по ключу идемпотентности/серийному номеру.
Exactly-once недостижимо без сильных предпосылок; практикуйте at-least-once + идемпотентность.
Outbox/Inbox-паттерн: запись в БД и публикация события атомарны (локальная транзакция), получатель обрабатывает по idempotency-key.
7) Чтения «не старее X» (RNOT)
Техники:- LSN/GTID-гейт: клиент передает минимальную версию (из ответа записи), роутер/прокси направляет на реплику, догнавшую LSN ≥ X, иначе — на лидер.
- Time-bound: «не старее 2 сек» — простой SLA без версий.
- Session pinning: после записи N секунд читаем только лидера (Read-Your-Writes).
8) Потоки изменений и согласование кэшей
CDC → шина событий (Kafka/Pulsar) → потребители (кэши, индексы, витрины).
Инвалидация кэшей: топики `invalidate:{ns}:{id}`; idempotent обработка.
Rebuild/Backfill: при рассинхроне пересоберите проекции из журнала событий.
9) Саги и компенсации (межсервисные транзакции)
В EC-мире долгоживущие операции разбиваются на шаги с компенсирующими действиями:- Оркестрация: координатор вызывает шаги и их компенсации.
- Хореография: шаги реагируют на события и сами публикуют следующие.
Инварианты (пример): «баланс ≥ 0» — проверка на границах шага + компенсация при отклонении.
10) Мульти-регион и сетевые разделения
Local-write, async-replicate: запись в локальном регионе + доставка в другие (EC).
Geo-fencing: данные «приклеены» к региону (низкая латентность, меньше конфликтов).
Кворумные БД (Raft) для CP-данных; кэши/витрины — AP/EC.
Split-brain план: при потере связи регионы продолжают работать в рамках доменных лимитов (write fencing, квоты), затем — reconcile.
11) Наблюдаемость и SLO
Метрики:- Replica lag: время/LSN-дистанция/offset (p50/p95/p99).
- Staleness: доля ответов старше порога (например, >2s или LSN
- Conflict rate: частота конфликтов и успешных merge.
- Convergence time: время схождения реплик после пика.
- Reconcile backlog: объем/время отстающих партиций.
- RPO/RTO по категориям данных (CP/AP).
- Лаг > целевого, рост конфликтов, «длинные» окна несходимости.
12) Проектирование схемы данных под EC
Явная версия/вектор в каждой записи (колонки `version`, `vc`).
Append-only журналы для критичных инвариантов (балансы, начисления).
Идентификаторы событий (snowflake/ULID) для порядка и дедупа.
Поля с коммутативной природой (счетчики, множества) → кандидаты на CRDT.
Дизайн API: PUT с if-match/etag, PATCH с precondition.
13) Паттерны хранения и чтения
Read model / CQRS: запись в «источник», чтение из проекций (могут отставать → отображайте «обновляется…»).
Stale-OK маршруты (каталог/лента) vs Strict (кошелек/лимиты).
Sticky/Bounded-stale флаги в запросе (заголовок `x-read-consistency`).
14) Чек-лист внедрения (0–45 дней)
0–10 дней
Категоризировать данные: CP-критичные (деньги, заказы) vs EC/стале-OK (каталоги, поисковые индексы).
Определить SLO стейла (например, «не старее 2s»), целевые лаги.
Включить версионирование объектов и idempotency-keys в API.
11–25 дней
Внедрить CDC и outbox/inbox, маршруты инвалидации кэша.
Добавить RNOT (LSN-гейт) и session pinning на запись-критичных путях.
Реализовать минимум одну merge-стратегию (LWW/CRDT/доменная) и журнал конфликтов.
26–45 дней
Автоматизировать anti-entropy (сверки/починка) и отчеты по стейлу.
Провести game-day: разделение сети, всплеск конфликтов, восстановление.
Визуализировать на дашбордах: lag, staleness, conflict rate, convergence.
15) Анти-паттерны
Слепой LWW для критичных инвариантов (потеря денег/баллов).
Отсутствие idempotency → дубли операций при ретраях.
«Сильная» модель на всем → чрезмерные хвосты p99 и хрупкость при сбоях.
Нет RNOT/Session гарантий → UX «мигает», пользователи «не видят» свои изменения.
Скрытая рассинхронизация кэша и источника (нет CDC/инвалидации).
Отсутствие инструмента reconcile/anti-entropy — данные «на века» расходятся.
16) Метрики зрелости
Replica lag p95 ≤ целевого (например, ≤ 500 ms внутри региона, ≤ 2 s межрегионы).
Staleness SLO выполняется ≥ 99% запросов на «строгих» маршрутах.
Conflict resolution success ≥ 99.9%, среднее время разрешения ≤ 1 мин.
Convergence time после пиков — минуты, не часы.
100% «денежных» операций покрыты idempotency-ключами и outbox/inbox.
17) Рецепты (сниппеты)
If-Match/ETag (HTTP)
PUT /profile/42
If-Match: "v17"
Body: { "email": "new@example.com" }
Если версия изменилась — `412 Precondition Failed` → клиент решает конфликт.
Запрос «не старее LSN» (псевдо)
x-min-lsn: 16/B373F8D8
Роутер выбирает реплику с `replay_lsn ≥ x-min-lsn`, иначе — лидер.
CRDT G-Counter (идея)
Каждый регион хранит свой счетчик; итог — сумма всех компонент; репликация — операция коммутативна.
18) Заключение
Eventual consistency — не компромисс качества, а осознанный контракт: где-то мы платим свежестью ради скорости и доступности, но защищаем критичные инварианты доменными стратегиями и инструментами. Введите версии, idempotency, RNOT/Session гарантии, CDC и anti-entropy, измеряйте lag/staleness/conflicts — и ваша распределенная система будет быстрой, устойчивой и предсказуемо сходящейся даже под сбоями и пиковыми нагрузками.