Пошук уроків, статей та іншого контенту
Зрозумієте обмеження розподілених систем і навчитеся аналізувати компроміс між узгодженістю, доступністю та стійкістю до розділення.
Теорема CAP описує фундаментальне обмеження розподілених систем:
Система не може одночасно гарантувати узгодженість, доступність і стійкість до розділення мережі.
CAP — це три властивості:
C — Consistency, узгодженість
A — Availability, доступність
P — Partition tolerance, стійкість до розділення мережі
Важливо: CAP не означає, що будь-яка система може мати лише дві властивості за будь-яких умов. Точніше твердження таке:
Якщо в системі сталося розділення мережі, потрібно обрати між узгодженістю та доступністю.
Розділення мережі — не рідкісна теоретична ситуація. Воно може виникнути через:
збій мережевого з'єднання;
проблеми з маршрутизатором;
тимчасову недоступність дата-центру;
мережеві затримки;
втрату пакетів;
перевантаження вузла або каналу зв'язку.
У контексті CAP узгодженість зазвичай означає лінійризовану узгодженість.
Після успішного запису будь-яке наступне читання має повернути це значення або пізніше. Система поводиться так, ніби всі операції виконуються атомарно в одному спільному порядку.
Наприклад, є два вузли:
клієнт записує balance = 100 на вузол A;
запис успішно завершився;
клієнт читає баланс із вузла B.
За сильної узгодженості вузол B має повернути 100, а не старе значення.
Узгодженість CAP не слід плутати з узгодженістю даних у сенсі перевірки обмежень або коректності бізнес-правил. CAP описує видимість операцій між вузлами системи.
Доступність означає, що кожен запит до справного вузла отримує відповідь без необмеженого очікування.
Відповідь може бути успішною або повідомляти про помилку, але вузол не повинен просто чекати, поки відновиться недоступна частина системи.
Наприклад, під час мережевого розділення AP-система може прийняти запис на локальному вузлі, навіть якщо не може синхронізувати його з іншими вузлами.
Доступність не означає:
мінімальну затримку;
відсутність помилок;
правильність відповіді з погляду бізнесу;
гарантовано найновіші дані.
Стійкість до розділення означає, що система продовжує працювати, навіть якщо її вузли тимчасово не можуть обмінюватися повідомленнями.
Розглянемо два вузли:
Клієнт ──> Вузол A
Вузол A X Вузол B
мережеве розділенняОбидва вузли можуть залишатися справними, але не бачити один одного. Якщо система повинна працювати в такому середовищі, вона має бути partition-tolerant.
Для реальних розподілених систем відмова від P зазвичай не є практичним варіантом. Мережеві розділення неможливо повністю виключити, тому під час такого розділення вибирають між C та A.
Уявімо сервіс залишків товару з двома репліками:
Репліка A: stock = 1
Репліка B: stock = 1Між репліками стався мережевий розрив. Два клієнти надсилають замовлення до різних реплік.
Система намагається зберегти узгодженість:
репліка A не може підтвердити операцію без зв'язку з B;
репліка B також не може підтвердити операцію;
запити блокуються або повертають помилку.
Перевага — система не продасть один товар двічі.
Недолік — під час розділення частина запитів недоступна.
Система намагається зберегти доступність:
репліка A приймає одне замовлення;
репліка B також приймає одне замовлення;
обидві відповідають клієнтам успішно.
Перевага — сервіс продовжує відповідати.
Недолік — після синхронізації виявиться конфлікт: товар був один, а замовлень — два.
Тоді системі потрібен спосіб розв'язати конфлікт:
скасувати одне замовлення;
запропонувати заміну;
виконати компенсацію;
використати інше бізнес-правило.
CP-система зберігає:
узгодженість;
стійкість до розділення.
Під час мережевого розділення вона може відмовити частині запитів, щоб не повертати суперечливі дані.
Прикладами задач, де часто важлива поведінка CP, є:
вибір лідера;
блокування ресурсу;
керування конфігурацією;
координація розподілених процесів;
операції, де подвійне виконання неприпустиме.
AP-система зберігає:
доступність;
стійкість до розділення.
Під час розділення різні вузли можуть тимчасово бачити різні значення. Після відновлення зв'язку вони синхронізуються та розв'язують конфлікти.
Такий підхід часто підходить для:
стрічок новин;
лічильників переглядів;
кешів;
кошиків покупок;
систем, де тимчасово застарілі дані прийнятні.
AP не означає «дані назавжди суперечливі». Часто AP-системи прагнуть до eventual consistency — зрештою всі репліки приходять до узгодженого стану, якщо нових змін немає і зв'язок відновлено.
CA-система забезпечує:
узгодженість;
доступність.
Але це можливо лише за припущення, що розділення мережі не відбувається або не розглядається.
Наприклад, база даних на одному вузлі може бути одночасно узгодженою та доступною. Проте щойно ми розподіляємо її між вузлами й допускаємо мережеві збої, потрібно визначити поведінку під час partition.
Тому «CA» для справжньої розподіленої системи часто є спрощеним позначенням. У реальному проєктуванні важливо питати:
Як система поводиться саме під час розділення мережі?
Наступний приклад демонструє дві стратегії для лічильника під час мережевого розділення:
CP-вузол відмовляється від запису, якщо не може синхронізувати його;
AP-вузол приймає локальний запис, а потім синхронізує зміни.
class Replica {
constructor(name, mode) {
this.name = name;
this.mode = mode;
this.value = 0;
this.connectedReplica = null;
}
connect(replica) {
this.connectedReplica = replica;
}
increment() {
const otherReplicaIsReachable = this.connectedReplica !== null;
if (this.mode === "CP" && !otherReplicaIsReachable) {
throw new Error(
`${this.name}: запис відхилено, щоб не порушити узгодженість`
);
}
this.value += 1;
console.log(`${this.name}: значення стало ${this.value}`);
if (otherReplicaIsReachable) {
this.connectedReplica.value = this.value;
}
}
read() {
return this.value;
}
}
const cpA = new Replica("CP-A", "CP");
const cpB = new Replica("CP-B", "CP");
cpA.connect(cpB);
cpB.connect(cpA);
cpA.increment();
console.log("CP-B читає:", cpB.read());
// Імітуємо мережеве розділення.
cpA.connect(null);
cpB.connect(null);
try {
cpA.increment();
} catch (error) {
console.log(error.message);
}
const apA = new Replica("AP-A", "AP");
const apB = new Replica("AP-B", "AP");
apA.connect(apB);
apB.connect(apA);
apA.increment();
console.log("AP-B читає:", apB.read());
// Під час розділення обидві репліки приймають локальні записи.
apA.connect(null);
apB.connect(null);
apA.increment();
apB.increment();
console.log("AP-A після розділення:", apA.read());
console.log("AP-B після розділення:", apB.read());Результат показує різницю в поведінці:
CP-запис під час розділення відхиляється;
AP-записи приймаються незалежно на обох репліках;
локальні значення AP-реплік можуть відрізнятися.
Це спрощена модель. Реальні системи додатково використовують версії записів, журнали операцій, кворуми та алгоритми розв'язання конфліктів.
Під час проєктування поставте такі запитання.
Опишіть конкретну поведінку:
запити блокуються;
запити повертають помилку;
записи приймаються локально;
читання можуть повертати застарілі дані;
доступною залишається лише частина вузлів.
Якщо поведінка не визначена, система може поводитися непередбачувано саме під час найскладнішого сценарію.
Для деяких даних це допустимо. Наприклад, два паралельні оновлення профілю можна об'єднати або попросити користувача повторити одну з дій.
Для інших даних конфлікт неприйнятний:
використання одного унікального ідентифікатора двома об'єктами;
подвійне списання коштів;
одночасне бронювання одного місця;
призначення двох лідерів.
Якщо помилка під час розділення краща за неправильний результат, варто обирати CP-поведінку.
Якщо важливіше прийняти запит зараз і розібрати конфлікт пізніше, може підійти AP-поведінка.
Компроміс потрібно описувати не лише словами «C або A», а й наслідками:
користувач побачить старий стан;
операцію буде відкладено;
операцію буде скасовано;
потрібно буде виконати компенсацію;
частина функцій тимчасово стане недоступною.
У багатьох розподілених системах рішення про запис або читання приймається за участю кворуму вузлів.
Нехай:
N — загальна кількість реплік;
W — кількість реплік, що підтверджують запис;
R — кількість реплік, що беруть участь у читанні.
Часто використовують умову:
R + W > NВона збільшує ймовірність того, що читання перетнеться з набором реплік, які підтвердили запис.
Проте кворуми не скасовують CAP:
якщо мережа розділена, кворум може бути недоступним;
якщо система дозволяє працювати без кворуму, можливі суперечливі дані;
налаштування R і W впливають на компроміс між доступністю, затримкою та узгодженістю.
Кворум — це механізм реалізації обраної поведінки, а не окрема четверта властивість CAP.
Це неточне формулювання. Ключове обмеження проявляється під час розділення мережі. Коли мережа працює нормально, система може одночасно виглядати узгодженою та доступною.
Доступність означає, що система відповідає, а не обов'язково приймає кожну операцію. Відповідь із помилкою може бути доступною відповіддю, якщо вона повертається без необмеженого очікування.
AP-система може зберігати всі операції, але тимчасово мати різні стани реплік. Після синхронізації конфлікти потрібно об'єднати або обробити за визначеним правилом.
CP-система може продовжувати працювати через вузли, які мають необхідний кворум. Недоступною стає лише частина операцій або вузлів, для яких неможливо гарантувати узгодженість.
У реальній системі різні операції можуть мати різні вимоги. Наприклад:
платіж може вимагати узгодженої поведінки;
лічильник переглядів може приймати тимчасові розбіжності;
кеш може повертати застаріле значення.
Тому CAP слід аналізувати для конкретної операції та конкретних даних.
CAP описує компроміс між узгодженістю, доступністю та стійкістю до розділення мережі.
Під час розділення мережі система повинна обрати між узгодженістю та доступністю.
CP-система краще захищає від суперечливих результатів, але може відмовляти під час розділення.
AP-система продовжує відповідати, але допускає тимчасово різні значення та потребує розв'язання конфліктів.
CA має сенс лише за відсутності або ігнорування мережевих розділень.
CAP потрібно застосовувати до конкретних операцій: записів, читань і бізнес-сценаріїв.
Кворуми допомагають реалізовувати узгодженість, але не усувають фундаментального компромісу CAP.