Eventual Consistency на практиці
Eventual consistency (EC) - модель, в якій копії даних можуть тимчасово розходитися, але з часом сходяться без глобальної координації. Це ключ до високої доступності (AP по CAP) і низької латентності (PACELC), якщо правильно визначити інваріанти, правила мерджа і клієнтські гарантії.
1) Коли вибирати EC (і коли - ні)
Підходить:- Фіди, профілі, лайки/лічильники, каталоги/пошук, кешовані уявлення.
- Глобальні системи з локальними записами і м'якими інваріантами.
- Проекції (CQRS), де джерело істини - суворе ядро, а читання - асинхронні.
- Жорсткі інваріанти: гроші, єдиність, ліміти, інвентар «не піти в мінус». Там - СР/сильніше EC, саги/ТСС.
2) Дизайн даних під EC: конфлікти та їх вирішення
Принцип: кожен запис несе метадані версій і детерміновану функцію злиття.
Мітки часу/версіонування: `version`, `ts`, `actor`.
Векторний годинник: фіксують причинність, дозволяють зрозуміти «конфліктуючі паралелі».
- LWW (Last-Write-Wins): просто і швидко, але може втратити «сенс».
- CRDT: комутативні/ідемпотентні структури, що гарантують сходження.
- Доменний merge: бізнес-функція (наприклад, об'єднати списки без дублів, підсумовувати лічильники, «найновіший email + об'єднання тегів»).
- Лічильники → G-Counter/PN-Counter.
- Безлічі → OR-Set (видалення без «залипання»).
- Регістри → LWW-Register (з обережністю до «втрат»).
- Карти/документи → Map of CRDTs.
- Спільне редагування → текстові CRDT/OT.
3) Реплікація та анти-ентропія
Gossip/anti-entropy: періодичний обмін станами/хешами між вузлами.
Hinted handoff: тимчасове «депонування» запису для недоступного вузла.
Read repair: при читанні виявили неузгодженість - підтягнули свіжі версії.
Пакети змін (deltas): ганяємо дельти, а не повні знімки.
Кворуми R/W: налаштовуємо'R','W','N'під компроміс швидкості і свіжості (наприклад,'R + W> N'ближче до strong на «останньому записі»).
4) Клієнтські гарантії поверх EC
Read-Your-Writes (RYW): автор після свого запису бачить її (sticky-session/маркування версії).
Monotonic Reads: не «відкочуємо» клієнта на старіше значення (зберігаємо watermark останньої версії).
Causal Consistency: зберігаємо причинність в межах сесії/потоку дій (векторні мітки в заголовках/токенах).
Bounded Staleness: гарантія «не старше Δ t/N версій» для UX-критичних екранів.
5) UX-патерни для EC
Оптимістичні апдейти: моментально відображаємо дію, позначаючи «синхронізацію».
Маркування свіжості: бейдж «оновлено X сек назад», кнопка «Оновити».
Конфлікт-UI: для рідкісних колізій - «показати обидві версії і вибрати/об'єднати».
Скелетон/placeholder + soft refresh: не блокувати UI очікуванням глобальних кворумів.
6) Архітектурні шаблони
6. 1 CQRS + проекції
Write-ядро (CP): Суворі інваріанти.
Read-площина (EC): асинхронні проекції, індекси, кеші; лаг припустимо.
6. 2 Мульти-регіон AP
Записи локально швидко, реплікація асинхронно.
Geo-partitioning: дані «живуть» ближче до користувача; крос-регіон - агрегати.
CRDT/merge-функції знімають біль конфліктів.
6. 3 Кворумне налаштування
yaml consistency:
replicas: 3 # N write_quorum: 2 # W read_quorum: 2 # R => R + W> N, closer to freshness on "last record"
read_repair: true hinted_handoff: true
7) Політики версіювання і merge (приклад)
yaml entity: "profile"
versioning:
clock: "vector" # или "hybrid_time"
fields:
name: { merge: "lww" }
emails: { merge: "set_union" } # OR-Set tags: { merge: "or_set" }
likes: { merge: "pn_counter" }
conflict_ui:
enabled: true show_diff_for: ["name"]
auto_merge_for: ["emails","tags","likes"]
8) Спостережуваність EC: Що міряти
Staleness Age (p50/p95/p99): 'now − data_version_ts' або «число версій відставання».
Replication Lag: затримка доставки між регіонами/вузлами.
Conflict Rate: частка паралельних апдейтів, розподіл за типами.
Read-Repair Rate/Latency: як часто і як швидко «лікуємо» при читанні.
Convergence Time: час до сходження після сплеску записів/відмови вузла.
Семантичні SLO: «95% профілів не старше 2с», «99% фіду сходиться <10с».
9) Runbook'і і інциденти
Сценарії:1. Зростання lag міжрегіонально: знизити'write fan-out', включити агресивний read-repair, троттлити важкі письменники.
2. Сплеск конфліктів: тимчасово включити більш «суворе» правило (наприклад, causal/RYW), обмежити конкурентні апдейти на гарячих ключах.
3. Відставання проекцій: приоритизувати черги реплікації, тимчасово урізати частоту не-критичних апдейтів.
4. Дані «залипли» у частини вузлів: форс-анти-ентропія, ребаланс партій, аудит hinted handoff.
5. Ручний розбір: вивантаження конфліктних ключів, інструмент «merge-preview», батчевий фікс.
10) Тестування EC
Jepsen-подібні тести: розділення мережі, clock-skew, повторні записи.
Property-based: інваріанти merge-функцій (комутативність, ідемпотентність, асоціативність).
Fuzz-конфлікти: паралельні апдейти на один ключ з варіативним порядком доставки.
Навантажувальні «пилки»: чергування бурстів/затиший для оцінки convergence time.
UX-симуляції: видимість RYW/monotonic у типових сценаріях.
11) Мульти-тенант і плани
Теги'tenant _ id/plan/region'у подіях/записах.
Fairness: ліміти на реплікацію/repair per tenant, щоб «галасливий» клієнт не збільшував загальний staleness.
Residency: дані та їх репліки в межах юрисдикції; крос-регіональні представлення тільки агрегати.
12) Типові помилки
LWW «для всього». Втрачає смислові паралельні зміни; використовуйте CRDT/доменні merge.
Немає клієнтських гарантій. Користувач «не бачить» власний запис → втрата довіри.
Відсутність спостережуваності застарівання. Немає метрик staleness/lag → «прихована деградація».
Dual-write в різні системи без merge. Фантоми і розбіжності нескінченно.
Глобальний порядок будь-якою ціною. Зайві кворуми вбивають p95, а бізнесу досить локального порядку.
13) Швидкі рецепти
Фід/стрічка: EC + causal/RYW для автора, CRDT для реакцій, staleness p95 ≤ 2-5с.
Профілі/налаштування: bounded staleness (≤1 -2с), RYW, доменний merge (union множин).
Глобальний каталог: geo-partition, асинхронна реплікація, read-repair за запитом, конфлікти через OR-Set.
Метрики/лічильники: PN-Counter, консолідація в фоні; відображення «приблизних» значень з позначкою.
14) Міні-еталон (словесна схема)
Write-edge: локальний запис з версією ('vector/hybrid'), журнал подій.
Replication: черги + gossip/anti-entropy, hinted handoff.
Storage: партіонування за ключем, CRDT/мердж-функції на рівні запису.
Read-plane: кеші з read-repair, RYW/monotonic токени, bounded staleness для критичних екранів.
Observability: лаги/застарівання/конфлікти, алерти на перевищення SLO стейлнесса.
15) Чек-лист перед продом
- Чітко описані інваріанти і де допускається EC.
- Вибрані версіонування (vector/hybrid) і детерміновані функції merge/CRDT.
- Реалізовані клієнтські гарантії (RYW/monotonic/causal) для критичних UX.
- Налаштовані реплікація, read-repair, hinted handoff; кворуми R/W документовані.
- Метрики staleness/lag/convergence і алерти по порогах p95/p99.
- Runbook'і на зростання конфліктів/лагів; безпечні інструменти ручного merge.
- Тести на мережеві розділення, паралельні апдейти і властивість збіжності.
- Мульти-тенантні ліміти і residency-політики враховані.
- UX-індикатори свіжості і fallback-поведінка узгоджені з продуктом.
Висновок
Eventual consistency - не «компроміс заради компромісу», а інструмент масштабованості та доступності. Якщо ви формалізуєте інваріанти, виберете коректні merge-функції (бажано CRDT там, де це доречно), дасте клієнтські гарантії і будете вимірювати стейлнесс і час сходження, система буде швидкою, стійкою і чесною - і для користувачів, і для бізнесу.