Компромиси
Накратко: CAP теоремата казва, че при прекъсната връзка между възлите системата избира между съгласуваност (CP, отказва) и наличност (AP, приема и после помирява).
Проблем
Последната кафемашина и двама купувачи
Магазинът вече има два склада, в София и във Варна. Всеки пази свое копие на наличността: страниците отговарят бързо, а единият склад работи и когато другият е бавен. Остава последната кафемашина за 349 €.
В петък вечер връзката между двата склада пада за десет минути. В същите минути Мария поръчва машината и поръчката ѝ стига до Варна. Иван поръчва същата машина и неговата стига до София.
Какво трябва да направи магазинът? Каквото и да избереш, някой губи. Или и двамата получават отказ, докато машината стои на рафта. Или и двамата плащат за една машина. Трети вариант няма, и точно за това е урокът.
Идеята
CAP на практика
Когато мрежата раздели системата на части (partition, P), всяка част отговаря сама. Тогава тя избира едно от двете:
- съгласуваност (consistency, C): всеки отговор е последната истина. Ако не може да е сигурна, частта отказва;
- наличност (availability, A): всяка заявка получава отговор, дори от копие, което може да е старо.
Разделяне не избираш: мрежите падат, а това е и мрежата между два склада, и между две зони в облака. Изборът е само какво правиш, когато стане. Това е компромисът между съгласуваност и наличност, известен като теоремата CAP. Видя я от страната на базите в урока „Репликация, шардинг и CAP“. Тук е от страната на магазина: кой губи и колко.
В програмите складът е обект, връзката е флаг linkUp, а времето е тик. Така двата избора се пускат един до друг, без мрежа.
class Warehouse { constructor(name, stock) { this.name = name; this.stock = stock; this.sales = []; // { buyer, tick } }}Виж го
CP: по-добре отказ, отколкото лъжа
CP складът продава само ако и другият потвърди. Без връзка не може да провери, затова отказва:
// CP: продава само ако и другият склад потвърдиfunction sellCP(here, other, linkUp, buyer) { if (!linkUp) return `${buyer}: отказ, ${here.name}` + ` не вижда ${other.name}`; if (here.stock === 0) return buyer + ": изчерпано"; here.stock--; other.stock--; return buyer + ": продадено";}const sofia = new Warehouse("София", 1);const varna = new Warehouse("Варна", 1);let linkUp = false; // връзката е падналаconsole.log(sellCP(varna, sofia, linkUp, "Мария"));console.log(sellCP(sofia, varna, linkUp, "Иван"));linkUp = true; // връзката се връщаconsole.log(sellCP(varna, sofia, linkUp, "Мария"));console.log(`наличност: ${sofia.name} ${sofia.stock}, ${varna.name} ${varna.stock}`);Мария: отказ, Варна не вижда СофияИван: отказ, София не вижда ВарнаМария: продаденоналичност: София 0, Варна 0Докато връзката я няма, никой не купува. Щом се върне, Мария купува, и двата склада казват 0. Данните са верни през цялото време. Цената е в другото качество: в тези десет минути магазинът не продава, а клиентите виждат грешка.
Виж го
AP: продай сега, помири после
AP складът продава от своето копие и записва продажбата. Когато връзката се върне, помиряването (reconciliation) събира продажбите от двата склада, подрежда ги по време и раздава истинските бройки:
// AP: продава от своето копие, без да питаfunction sellAP(here, buyer, tick) { if (here.stock === 0) return buyer + ": изчерпано"; here.stock--; here.sales.push({ buyer, tick }); return `${buyer}: продадено (${here.name})`;}
// помиряване: продажбите от двата склада, по времеfunction reconcile(real, a, b) { const all = [...a.sales, ...b.sales]; all.sort((x, y) => x.tick - y.tick); for (const s of all) { const ok = real > 0; if (ok) real--; const result = ok ? "потвърдено" : "ваучер 20 €"; console.log(`${s.buyer} (тик ${s.tick}): ${result}`); } a.stock = b.stock = real;}const sofia = new Warehouse("София", 1);const varna = new Warehouse("Варна", 1);// връзката е паднала: всеки склад продава от своето копиеconsole.log(sellAP(varna, "Мария", 3));console.log(sellAP(sofia, "Иван", 5));// връзката се връщаreconcile(1, sofia, varna);console.log("наличност:", sofia.stock);Мария: продадено (Варна)Иван: продадено (София)Мария (тик 3): потвърденоИван (тик 5): ваучер 20 €наличност: 0Двамата чуват „продадено“. После се оказва, че машината е една. Мария е била първа и я получава. Иван получава извинение и ваучер за 20 €. AP плаща с обезщетение и с код за помиряване.
Двете копия се разминаваха десет минути и се срещнаха, щом връзката се върна. Това е евентуална съгласуваност (eventual consistency): копията може да се различават известно време, но когато спрат новите промени, стигат до едно и също. Обещанието е, че се срещат, а не кога.
Виж го
Двата избора един под друг
// AP: продава от своето копие, без да питаfunction sellAP(here, buyer, tick) { if (here.stock === 0) return buyer + ": изчерпано"; here.stock--; here.sales.push({ buyer, tick }); return `${buyer}: продадено (${here.name})`;}
// помиряване: продажбите от двата склада, по времеfunction reconcile(real, a, b) { const all = [...a.sales, ...b.sales]; all.sort((x, y) => x.tick - y.tick); for (const s of all) { const ok = real > 0; if (ok) real--; const result = ok ? "потвърдено" : "ваучер 20 €"; console.log(`${s.buyer} (тик ${s.tick}): ${result}`); } a.stock = b.stock = real;}// CP: продава само ако и другият склад потвърдиfunction sellCP(here, other, linkUp, buyer) { if (!linkUp) return `${buyer}: отказ, ${here.name}` + ` не вижда ${other.name}`; if (here.stock === 0) return buyer + ": изчерпано"; here.stock--; other.stock--; return buyer + ": продадено";}// AP: продава от своето копие, без да питаfunction sellAP(here, buyer, tick) { if (here.stock === 0) return buyer + ": изчерпано"; here.stock--; here.sales.push({ buyer, tick }); return `${buyer}: продадено (${here.name})`;}
// помиряване: продажбите от двата склада, по времеfunction reconcile(real, a, b) { const all = [...a.sales, ...b.sales]; all.sort((x, y) => x.tick - y.tick); for (const s of all) { const ok = real > 0; if (ok) real--; const result = ok ? "потвърдено" : "ваучер 20 €"; console.log(`${s.buyer} (тик ${s.tick}): ${result}`); } a.stock = b.stock = real;}Стъпка по стъпка, с думи
- Последната кафемашина за 349 €. Два склада пазят по копие на наличността: 1 и 1. Два магазина един под друг: горният избира CP, долният AP.
- Връзката между София и Варна пада. Мария е във Варна, Иван в София. Всеки склад трябва да реши сам.
- CP продава само ако другият склад потвърди. Връзка няма, затова и двамата получават отказ. Данните остават верни, но магазинът не продава.
- AP продава от своето копие. Мария купува на тик 3, Иван на тик 5. И двете копия казват 0, а продадени са две от една.
- Връзката се връща. Помиряването събира продажбите, а не числата, и ги подрежда по време.
- Мария е първа и получава машината. Иван получава извинение и ваучер 20 €. CP плати с изгубени продажби, AP с обезщетение.
Горният магазин е CP, долният е AP. Едно и също разделяне, едни и същи купувачи. Горе губи магазинът (продажби), долу губи един клиент (стоката), а магазинът плаща ваучер и време в поддръжката.
Пробвай
Предвиди изхода
Бройките са две. Връзката е паднала, а складовете продават по AP: Иван в София на тик 1, Мария във Варна на тик 4, Елена в София на тик 2. Какво печата помиряването?
const sofia = new Warehouse("София", 2);const varna = new Warehouse("Варна", 2);sellAP(sofia, "Иван", 1);sellAP(varna, "Мария", 4);sellAP(sofia, "Елена", 2);reconcile(2, sofia, varna);Помиряването подрежда по тик, без значение в кой склад е продажбата: Иван (1), Елена (2), Мария (4). Двете бройки отиват при първите двама, а Мария получава ваучер, макар че във Варна наличността ѝ е изглеждала 2.
Пробвай
Попълни
По какво се подреждат продажбите при помиряването?
const all = [...a.sales, ...b.sales];
all.sort((x, y) => x. - y.tick);По тика: момента на продажбата. Така е честно за клиентите и не зависи от реда, в който складовете пращат продажбите си.
Пробвай
Намери грешката
Колегата пише по-кратко помиряване: всеки склад взима по-малкото от двете числа. След разделянето и двата склада казват 0, никой не получава ваучер, а Иван чака машина, която я няма. На кой ред е грешката?
Избери реда с грешката.
Числата са резултат, а не история. От 0 и 0 не се вижда, че са продадени две бройки от една: всяко копие е извадило своята. Помиряват се операциите (продажбите), а числото се смята от тях.
Идеята
Цената на всяко качество е в друго качество
CAP е най-известният компромис, но не единственият. Архитектурата е избор кое качество плаща за кое:
- съгласуваност срещу наличност при разделяне, както видя;
- съгласуваност срещу закъснение и без разделяне: потвърждението от другия склад е още едно пътуване по мрежата при всяка продажба;
- наличност срещу коректност: AP иска обезщетения, процес по помиряване и хора в поддръжката;
- сигурност срещу скорост и удобство: още една проверка е още време, още една стъпка при вход;
- мащаб срещу простота: два склада с копия са по-сложни от една база.
Изборът не е един за целия магазин, а за всеки вид данни. Количката е добра за AP: ако изгубиш артикул, клиентът го добавя пак, а количката е там винаги. Последните бройки, плащането и сметките са за CP: продадена несъществуваща стока или двойно платена сума струват пари и доверие. Каталогът с описанията търпи стари данни с минути.
Който решава, е бизнесът, а архитектът му дава цената на всеки вариант с числа: колко минути без продажби в годината при CP, колко ваучера при AP. Решението и последиците му отиват в ADR.
Идеята
Кога вреди
- Евентуална съгласуваност, когато потребителят не вижда своето действие. Клиентът плаща, страницата чете от копие, което още не знае, и казва „нямаш поръчки“. Той плаща пак. Видя същото с репликите в урока за мащабирането. Там, където човекът чете току-що написаното, чети от мястото, където е написано.
- Когато обезщетение няма. Пари, които вече са изтеглени два пъти, лекарство с ограничен брой, билет за място в самолета. Тук AP не е избор, а грешка.
- Помиряване, по-сложно от ползата. Ако разделянето се случва веднъж в годината за минута, отказът е по-евтин от код за помиряване, който някой трябва да разбира.
- Разпределени данни, без да трябват. Една база с транзакции изобщо не задава този въпрос (урокът за транзакциите). Копие в друг склад или друга държава се прави, когато NFR го иска с число.
По-точно, за любопитните
CAP по-точно. Теоремата (Brewer, доказана от Gilbert и Lynch) говори за една стойност, много копия и разделяне. „Избери 2 от 3“ подвежда: P не е избор. Казва се по-точно така: при разделяне избираш C или A. PACELC добавя втората половина: иначе (Else) избираш между закъснение (Latency) и съгласуваност (Consistency).
Времето. Тук всички виждат един и същ брояч на тикове. Часовниците на истинските машини се разминават, затова „кой е първи“ се решава с логически часовници: Lamport, векторни часовници, hybrid logical clocks. Spanner на Google ползва часовници с атомни еталони и GPS и чака, докато несигурността мине.
CRDT (conflict-free replicated data types) са структури, които се сливат винаги в един и същ резултат, без координация: броячи, множества, списъци. Количката на Amazon в статията за Dynamo е пример за AP с добавяне, което не губи артикули.
Кворуми. С N копия, запис в W и четене от R, при W + R > N четенето винаги вижда последния запис. Видя ги в урока за репликацията. Cassandra и DynamoDB дават избор на нивото на съгласуваност за всяка заявка.
Няколко услуги. Помиряването тук е за една стока в два склада. Когато една бизнес операция минава през няколко услуги и при отказ трябва да се върне назад стъпка по стъпка, това става шаблон със свое име. Той идва по-нататък в модула.
Array.prototype.sort е стабилно от ES2019, а сравнението x.tick - y.tick подрежда числата като числа, не като текст.Упражнение
CP за последните бройки, AP за останалото
Реши задачата в редактора си.
Двата склада продават кафемашини (по 1 бройка) и филтри за кафе (по 500). Напиши sell, който продава кафемашини само с потвърждение от другия склад (CP), а филтри от своето копие (AP). Връзката е паднала. Мария във Варна иска машина и филтри, Иван в София иска филтри.
Мария, кафемашина: отказ, Варна не вижда СофияМария, филтри: продадено (Варна)Иван, филтри: продадено (София)Още задачи
- Праг. Вместо списък с „важни“ стоки сложи праг: AP, докато в копието има повече от 3 бройки, CP за последните 3. Какво става, ако и двата склада продадат по 3 по време на разделянето?
- Количката. Количката е в две копия. В едното клиентът е добавил чаши, в другото е махнал филтрите. Напиши помиряване на операциите „добави“ и „махни“, подредени по тик.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Стабилно сортиране и вграденият sort · Мащабиране · ADR: записани архитектурни решения · Нефункционални изисквания, които се мерят
- Продължава в: Транзакция през няколко услуги (saga) · Твоята първа архитектурна задача
- Задачи по темата: Архитектът в работа