Пошук уроків, статей та іншого контенту
Порівняємо послідовну, причинну, read-your-writes та інші моделі узгодженості розподілених даних.
У розподіленій системі одна й та сама інформація може зберігатися на кількох вузлах або репліках. Через затримки мережі, кешування та асинхронну реплікацію різні клієнти можуть тимчасово бачити різні значення.
Модель узгодженості описує, які результати читання дозволені після виконання операцій запису.
Наприклад, клієнт записав:
status = "paid"А потім одразу прочитав дані з іншої репліки й отримав:
status = "pending"Це не обов’язково помилка системи. Такий результат може бути дозволений обраною моделлю узгодженості.
Важливо розрізняти:
узгодженість даних — які значення можуть бачити клієнти;
доступність — чи відповідає система на запити;
довговічність — чи збережеться запис після збою;
атомарність — чи виконується операція повністю або не виконується зовсім.
Цей урок зосереджений саме на узгодженості.
У побутовому описі часто кажуть сильна узгодженість, маючи на увазі, що після успішного запису кожне наступне читання одразу повертає нове значення.
Точніший термін — лінеаризованість.
Лінеаризована операція поводиться так, наче кожен запит виконується миттєво в певний момент між його початком і завершенням. Усі клієнти бачать один глобальний порядок операцій, який відповідає реальному часу.
Приклад:
Клієнт A: write(balance = 100) завершився
Клієнт B: read(balance) почався після цьогоЗа лінеаризованої моделі клієнт B не може отримати старе значення 50.
Лінеаризованість зручна для міркувань про систему, але має ціну:
потрібна координація між вузлами;
зростає затримка;
мережеві проблеми можуть призвести до відмови або блокування операцій;
масштабування записів стає складнішим.
Для всіх даних така гарантія часто не потрібна. Наприклад, для кількості доступних місць на подію вона може бути важливою, а для лічильника переглядів — ні.
Послідовна узгодженість гарантує, що результати всіх операцій можна пояснити одним послідовним порядком, який зберігає порядок операцій у межах кожного клієнта.
На відміну від лінеаризованості, послідовна узгодженість не обов’язково зберігає глобальний порядок реального часу між різними клієнтами.
Розглянемо двох клієнтів:
Клієнт A: write(x = 1)
Клієнт B: read(x)Якщо операції відбулися майже одночасно, послідовна модель може дозволити системі впорядкувати читання до запису. Але якщо один клієнт виконав дві операції:
Клієнт A: write(x = 1)
Клієнт A: read(x)то читання не може бути логічно розташоване перед записом цього самого клієнта.
Порівняння:
лінеаризованість зберігає порядок операцій у реальному часі;
послідовна узгодженість зберігає порядок операцій кожного клієнта, але допускає інший глобальний порядок.
Послідовна узгодженість слабша за лінеаризованість, але все ще вимагає значної координації.
Причинна узгодженість зберігає порядок причинно пов’язаних операцій.
Операція B причинно залежить від A, якщо:
B читає результат A;
B виконується після того, як клієнт побачив результат A;
A і B належать одному клієнту та виконуються послідовно;
залежність передається через кілька операцій.
Приклад із соціальною мережею:
1. Користувач публікує допис.
2. Інший користувач читає допис.
3. Інший користувач залишає коментар.Коментар причинно залежить від допису. Система не повинна показати коментар на репліці, де самого допису ще немає.
Водночас незалежні операції можуть бути побачені в різному порядку.
Наприклад:
Клієнт A: створює пост X
Клієнт B: створює пост YЯкщо ці операції не залежать одна від одної, одна репліка може показати:
X, Yа інша:
Y, XЦе допустимо за причинної узгодженості.
Для реалізації причинних гарантій системи можуть використовувати:
логічні мітки;
version vectors;
dependency tokens;
метадані сесії.
Ці механізми допомагають репліці визначити, чи має вона вже всі дані, від яких залежить конкретна операція.
Зрештою узгодженість гарантує, що всі репліки зрештою прийдуть до одного значення, якщо:
нові записи припиняться;
мережеві повідомлення зрештою доставляються;
вузли продовжують працювати.
У певний момент репліки можуть містити різні значення:
Репліка A: status = "paid"
Репліка B: status = "pending"Після завершення реплікації вони мають стати однаковими.
Ця модель добре підходить для:
кешів;
лічильників, де короткочасна похибка допустима;
каталогів і пошукових індексів;
реакцій, переглядів і статистики;
даних, для яких важливі доступність і мала затримка.
Eventual consistency не гарантує:
що клієнт одразу побачить власний запис;
що читання ніколи не поверне старе значення;
що різні читання одного клієнта будуть узгодженими;
що причинний порядок операцій буде збережено.
Гарантії сесії описують поведінку даних для одного клієнта або користувацької сесії. Вони часто використовуються поверх eventual consistency.
Read-your-writes гарантує: після успішного запису клієнт не прочитає значення старше за власний запис.
Без цієї гарантії можливий дивний сценарій:
1. Користувач змінює ім’я на "Олена".
2. Сервер підтверджує зміну.
3. Наступний запит повертає старе ім’я "Олег".Реплікація ще не завершилася, але для користувача це виглядає як втрата зміни.
Типові способи реалізації:
після запису читати з primary;
прив’язати сесію до конкретної репліки;
передавати клієнту версію запису та читати лише з репліки, яка досягла цієї версії;
виконувати read repair або чекати завершення реплікації.
Монотонні читання гарантують, що наступне читання клієнта не покаже стан, старіший за той, який він уже бачив.
Без цієї гарантії клієнт може спостерігати:
Перше читання: кількість повідомлень = 10
Друге читання: кількість повідомлень = 7Це може статися, якщо перший запит потрапив на новішу репліку, а другий — на старішу.
Для реалізації можна:
маршрутизувати сесію на одну репліку;
зберігати у сесії останню побачену версію;
не виконувати читання з репліки, яка має старішу версію.
Монотонні записи гарантують, що записи одного клієнта застосовуються в тому самому порядку, у якому клієнт їх створив.
Приклад:
1. write(status = "processing")
2. write(status = "completed")Репліка не повинна застосувати другий запис, а потім перший. Інакше фінальний стан може помилково стати "processing".
Для цього використовують:
порядок повідомлень у черзі;
номери версій;
послідовні токени операцій;
маршрутизацію записів одного ключа на одного primary.
Нижче наведено спрощену модель. Є primary і дві репліки. Запис одразу потрапляє на primary, але до реплік доставляється із затримкою.
class Replica {
constructor(name) {
this.name = name;
this.data = new Map();
}
apply(key, value, version) {
const current = this.data.get(key);
// Репліка не застосовує старішу версію поверх новішої.
if (!current || version >= current.version) {
this.data.set(key, { value, version });
}
}
read(key) {
return this.data.get(key) || null;
}
}
const primary = new Replica("primary");
const replicaA = new Replica("replica-A");
const replicaB = new Replica("replica-B");
const replicas = [
{ replica: replicaA, delay: 100 },
{ replica: replicaB, delay: 500 }
];
let nextVersion = 0;
function write(key, value) {
const version = ++nextVersion;
primary.apply(key, value, version);
for (const { replica, delay } of replicas) {
setTimeout(() => {
replica.apply(key, value, version);
console.log(
`${replica.name} отримала версію ${version}`
);
}, delay);
}
return { key, value, version };
}
function ordinaryRead(replica, key) {
return replica.read(key);
}
function readWithReadYourWrites(session, key) {
const replicaResult = ordinaryRead(replicaB, key);
const requiredVersion = session[key] || 0;
const actualVersion = replicaResult ? replicaResult.version : 0;
if (actualVersion >= requiredVersion) {
return {
source: replicaB.name,
result: replicaResult
};
}
// Якщо репліка відстає, читаємо з primary.
return {
source: primary.name,
result: primary.read(key)
};
}
async function main() {
const session = {};
const writeResult = write("order:42", "paid");
session[writeResult.key] = writeResult.version;
console.log("Звичайне читання одразу після запису:");
console.log(ordinaryRead(replicaB, "order:42"));
console.log("Читання з гарантією read-your-writes:");
console.log(readWithReadYourWrites(session, "order:42"));
await new Promise((resolve) => setTimeout(resolve, 600));
console.log("Звичайне читання після завершення реплікації:");
console.log(ordinaryRead(replicaB, "order:42"));
}
main();Можливий результат:
Звичайне читання одразу після запису:
null
Читання з гарантією read-your-writes:
{
source: "primary",
result: { value: "paid", version: 1 }
}
replica-A отримала версію 1
replica-B отримала версію 1
Звичайне читання після завершення реплікації:
{ value: "paid", version: 1 }Це лише навчальна модель. У реальній системі версії часто є складнішими: для одного ключа може використовуватися лічильник, а для причинних залежностей — набір версій або спеціальний токен.
Вибір моделі залежить від того, які помилки може витримати бізнес-логіка.
Підходить для:
блокувань;
унікальних ресурсів;
залишків, якщо подвійний продаж неприпустимий;
координаторів і конфігурацій, де потрібен єдиний актуальний стан.
Підходить для:
повідомлень;
коментарів;
стрічок подій;
workflow, у якому наступний крок залежить від попереднього;
систем, де важливий порядок пов’язаних змін, але не потрібен глобальний порядок усіх операцій.
Підходить майже для будь-якого користувацького інтерфейсу, де людина очікує побачити щойно збережену зміну:
профіль;
налаштування;
кошик;
статус замовлення;
редагування документа.
Підходить, коли короткочасно застаріле значення допустиме:
статистика;
лічильник переглядів;
кеш;
пошуковий індекс;
рекомендації;
кількість реакцій.
Головне правило: модель узгодженості потрібно визначати для конкретної операції або типу даних, а не обов’язково для всієї системи. В одному застосунку платежі можуть вимагати сильних гарантій, а статистика — eventual consistency.
Замість нечіткого формулювання «дані мають бути узгодженими» варто описувати конкретні гарантії:
після підтвердження запису цей самий клієнт має бачити його;
два послідовні читання клієнта не повинні повертати старішу версію;
коментар не може бути видимим без відповідного допису;
два записи одного клієнта мають застосовуватися в заданому порядку;
після припинення записів усі репліки повинні зрештою збігтися;
конкурентні операції можуть відображатися в різному порядку.
Такі вимоги безпосередньо підказують, яку модель або комбінацію гарантій потрібно реалізувати.
Eventual consistency не означає, що репліки можуть назавжди містити різні дані. Вона допускає тимчасову розбіжність, але за визначених умов репліки мають збігтися.
Успішна відповідь від primary не означає, що кожна репліка вже застосувала запис. Для цього потрібна відповідна гарантія або синхронна реплікація.
«Сильна узгодженість» — нечіткий термін. Під час проєктування краще вказувати конкретну гарантію: лінеаризованість, послідовну, причинну або read-your-writes.
Навіть якщо глобальна узгодженість слабка, користувач зазвичай очікує, що його власні дії не будуть перемішані. Для цього потрібні monotonic reads, monotonic writes або read-your-writes.
Сильні гарантії збільшують затримку та вартість координації. Для кожного типу даних потрібно визначити, які застарілі або різні результати справді неприйнятні.
Модель узгодженості визначає, які результати читання дозволені в розподіленій системі.
Лінеаризованість наближує систему до одного актуального глобального стану та враховує реальний час.
Послідовна узгодженість зберігає порядок операцій кожного клієнта, але не обов’язково реальний час між клієнтами.
Причинна узгодженість зберігає порядок причинно пов’язаних операцій.
Eventual consistency допускає тимчасово застарілі дані, але гарантує збіжність реплік за сприятливих умов.
Read-your-writes, monotonic reads і monotonic writes є корисними гарантіями на рівні сесії.
Модель потрібно обирати відповідно до вимог конкретної операції, а не використовувати однакову гарантію для всіх даних.