Пошук уроків, статей та іншого контенту
З’ясуємо компроміси між strong і eventual consistency та навчимося обирати модель для конкретного сценарію.
У розподіленій системі дані часто зберігаються не в одному місці, а на кількох вузлах або репліках. Це покращує доступність і продуктивність, але створює проблему: після зміни даних репліки можуть деякий час містити різні значення.
Узгодженість визначає, які значення може побачити клієнт під час читання після запису.
Наприклад, користувач змінив адресу доставки:
запис потрапив на основний вузол;
система підтвердила успішний запис;
користувач одразу відкрив профіль;
запит потрапив на репліку, яка ще не отримала нове значення.
Поведінка системи в цей момент залежить від обраної моделі узгодженості.
За сильної узгодженості після успішного запису наступне читання отримує найновіше значення згідно з визначеним порядком операцій.
Спрощене правило:
Якщо система підтвердила запис, усі наступні читання повинні бачити цей запис.
Наприклад:
write balance = 100 → успішно
read balance → 100Навіть якщо читання виконується з іншого вузла, система повинна гарантувати, що воно не поверне старе значення.
Типові підходи:
читати лише з основного вузла;
спочатку синхронно оновити кілька реплік, а потім підтвердити запис;
використовувати узгоджений протокол між вузлами;
не дозволяти читання з репліки, доки вона не наздогнала основний вузол.
Сильна узгодженість не обов’язково означає, що всі вузли фізично оновлюються одночасно. Важливо, щоб система не дозволяла клієнту побачити некоректне або застаріле значення після підтвердженого запису.
простіша логіка для клієнта;
передбачувані результати читання;
зручно реалізовувати фінансові операції;
менше спеціальної логіки для обробки застарілих даних.
більша затримка записів або читань;
нижча доступність під час проблем із репліками;
складніше масштабування між регіонами;
можливість блокування операцій під час мережевих проблем.
За зрештою узгодженої моделі після запису репліки можуть тимчасово повертати старі значення. Якщо нових записів більше немає, система зрештою поширить останнє значення на всі доступні репліки.
write status = "paid" → успішно
read from replica A → "pending"
read from replica B → "paid"
через деякий час:
read from replica A → "paid"
read from replica B → "paid"У цій моделі система допускає тимчасову неузгодженість між вузлами.
Записується нове значення на основний вузол.
Основний вузол одразу підтверджує операцію.
Оновлення асинхронно надсилаються на репліки.
Репліки застосовують зміни із затримкою.
Затримка може виникати через:
мережеві проблеми;
чергу реплікації;
тимчасово недоступний вузол;
навантаження на систему.
низька затримка записів;
висока доступність;
зручне масштабування на багато реплік;
краща робота між географічно віддаленими регіонами;
система може продовжувати приймати операції навіть при тимчасовій недоступності частини вузлів.
клієнт може побачити старі дані;
різні користувачі можуть тимчасово бачити різні значення;
бізнес-логіка має враховувати затримку реплікації;
складніше тестувати й пояснювати поведінку системи.
У прикладі основний вузол отримує запис одразу, а репліка оновлюється асинхронно через 300 мс.
class EventualStore {
constructor(replicaCount, replicationDelayMs) {
this.primary = new Map();
this.replicas = Array.from(
{ length: replicaCount },
() => new Map()
);
this.replicationDelayMs = replicationDelayMs;
}
write(key, value) {
// Основний вузол застосовує запис одразу.
this.primary.set(key, value);
for (const replica of this.replicas) {
setTimeout(() => {
// Репліка отримає значення із затримкою.
replica.set(key, value);
}, this.replicationDelayMs);
}
// Запис підтверджується до завершення реплікації.
return Promise.resolve();
}
readStrong(key) {
// Сильне читання виконується з основного вузла.
return this.primary.get(key);
}
readEventual(key, replicaIndex) {
// Зрештою узгоджене читання може повернути старе значення.
return this.replicas[replicaIndex].get(key);
}
}
function wait(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
async function main() {
const store = new EventualStore(2, 300);
await store.write("order:42:status", "pending");
// Чекаємо, поки початкове значення потрапить на репліки.
await wait(350);
await store.write("order:42:status", "paid");
console.log(
"Сильне читання:",
store.readStrong("order:42:status")
);
console.log(
"Зрештою узгоджене читання одразу:",
store.readEventual("order:42:status", 0)
);
await wait(350);
console.log(
"Зрештою узгоджене читання після реплікації:",
store.readEventual("order:42:status", 0)
);
}
main();Очікуваний результат:
Сильне читання: paid
Зрештою узгоджене читання одразу: pending
Зрештою узгоджене читання після реплікації: paidЦе спрощена модель. У реальній системі сильна узгодженість може забезпечуватися не лише читанням із primary-вузла, а й протоколами реплікації, кворумами та контролем порядку операцій.
Вибір між моделями зазвичай зводиться до компромісу між:
коректністю даних у кожен момент;
затримкою;
доступністю;
складністю клієнтського коду;
здатністю працювати під час мережевих збоїв.
Сильна узгодженість зазвичай вимагає чекати підтвердження від достатньої кількості вузлів або направляти операції через один узгоджений вузол. Це збільшує час відповіді та може зробити систему менш доступною під час проблем із мережею.
Зрештою узгоджена система швидше відповідає і легше масштабується, але клієнт повинен бути готовий тимчасово побачити старий стан.
Сильна узгодженість доречна, коли помилка або застаріле читання може призвести до неправильного результату:
списання грошей із банківського рахунку;
перевірка доступного залишку товару перед оплатою;
бронювання останнього місця;
зміна прав доступу;
реєстрація унікального імені або ідентифікатора;
підтвердження важливої транзакції.
Наприклад, два користувачі не повинні одночасно успішно забронювати одне й те саме місце. Для такого сценарію система має координувати записи й не дозволяти використовувати застарілий стан.
Зрештою узгодженість підходить, коли тимчасово старі дані не створюють критичної проблеми:
кількість переглядів статті;
стрічка новин;
лічильник реакцій;
кеш профілю;
пошуковий індекс;
рекомендації;
аналітичні звіти;
статус, який може оновитися через кілька секунд.
Наприклад, якщо користувач поставив реакцію на публікацію, інші користувачі можуть побачити нове значення не миттєво. Для такого сценарію нижча затримка й висока доступність можуть бути важливішими за миттєву однаковість усіх реплік.
Перед вибором моделі корисно відповісти на такі запитання:
Що станеться, якщо клієнт прочитає старе значення?
Чи може застаріле читання призвести до подвійної операції?
Чи потрібно гарантувати унікальність або порядок дій?
Яку затримку готовий прийняти користувач?
Чи повинна система працювати при недоступності частини вузлів?
Чи можна виправити тимчасово неправильне відображення пізніше?
Чи може клієнт повторити операцію без небезпечного дублювання?
Якщо неправильне читання змінює фінальний результат операції, потрібні сильніші гарантії. Якщо воно лише тимчасово впливає на відображення, зрештою узгоджена модель часто є практичнішою.
Не обов’язково обирати одну модель для всієї системи. Різні дані можуть мати різні вимоги.
Наприклад, інтернет-магазин може використовувати:
сильну узгодженість для залишків товару й платежів;
зрештою узгоджену модель для рекомендацій;
сильну узгодженість для прав доступу;
зрештою узгоджену модель для кількості переглядів.
Навіть в одному сценарії можна розділити операції:
запис замовлення має бути сильним;
відображення статистики замовлень може оновлюватися із затримкою.
Такий підхід допомагає не платити ціну сильної узгодженості там, де вона не потрібна.
Якщо система використовує зрештою узгоджену модель, клієнтський код не повинен припускати, що будь-яке читання одразу повертає останній запис.
Можливі підходи:
показувати стан «оновлення виконується»;
повторювати читання через короткий інтервал;
читати після запису з вузла, який точно має актуальні дані;
повертати клієнту щойно записане значення;
використовувати версію або часову мітку даних;
робити критичну перевірку безпосередньо перед незворотною операцією.
Водночас повторне читання саме по собі не гарантує отримання нового значення. Клієнт може знову звернутися до тієї самої застарілої репліки. Гарантії повинна забезпечувати інфраструктура або протокол взаємодії із системою.
Зрештою узгодженість означає тимчасову різницю між репліками, а не обов’язкову втрату запису. Якщо система правильно реалізована, зміна має зрештою дійти до доступних реплік.
Сильна узгодженість не захищає від неправильних бізнес-правил, помилок авторизації чи дублювання запитів. Вона лише визначає гарантії видимості та порядку даних.
Якщо на основі читання вирішується, чи можна списати гроші або зарезервувати ресурс, застаріле значення може призвести до некоректного результату.
Це може без потреби збільшити затримку, вартість і складність системи. Для лічильників, рекомендацій або пошукових індексів часто достатньо зрештою узгодженої моделі.
Узгодженість відповідає на питання:
Яке значення побачить читач?
Довговічність відповідає на інше питання:
Чи збережеться запис після підтвердження та збою?
Це різні властивості системи.
Сильна узгодженість гарантує, що після підтвердженого запису читання не поверне застарілий стан.
Зрештою узгоджена модель допускає тимчасово старі значення, але репліки зрештою приходять до спільного стану.
Сильна узгодженість спрощує логіку клієнта, але може збільшити затримку та зменшити доступність.
Зрештою узгоджена модель покращує масштабованість і доступність, але вимагає обробляти застарілі читання.
Для платежів, бронювання, залишків і прав доступу зазвичай потрібні сильніші гарантії.
Для стрічок, рекомендацій, лічильників і пошукових індексів часто достатньо зрештою узгодженої моделі.
В одній системі можна використовувати різні моделі для різних типів даних.