Реплікація та 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 ЄС/стале-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 - і ваша розподілена система буде швидкою, стійкою і передбачувано збігається навіть під збоями і піковими навантаженнями.