Към съдържанието
wannacode

Компромиси

Накратко: 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;
}
Стъпка по стъпка, с думи
  1. Последната кафемашина за 349 €. Два склада пазят по копие на наличността: 1 и 1. Два магазина един под друг: горният избира CP, долният AP.
  2. Връзката между София и Варна пада. Мария е във Варна, Иван в София. Всеки склад трябва да реши сам.
  3. CP продава само ако другият склад потвърди. Връзка няма, затова и двамата получават отказ. Данните остават верни, но магазинът не продава.
  4. AP продава от своето копие. Мария купува на тик 3, Иван на тик 5. И двете копия казват 0, а продадени са две от една.
  5. Връзката се връща. Помиряването събира продажбите, а не числата, и ги подрежда по време.
  6. Мария е първа и получава машината. Иван получава извинение и ваучер 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);
Отговор

Пробвай

Попълни

По какво се подреждат продажбите при помиряването?

const all = [...a.sales, ...b.sales];
all.sort((x, y) => x. - y.tick);

Пробвай

Намери грешката

Колегата пише по-кратко помиряване: всеки склад взима по-малкото от двете числа. След разделянето и двата склада казват 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 по време на разделянето?
  • Количката. Количката е в две копия. В едното клиентът е добавил чаши, в другото е махнал филтрите. Напиши помиряване на операциите „добави“ и „махни“, подредени по тик.

Тест

Провери се

1. Връзката между двата склада е паднала. Какво може да избере всеки склад?
2. Какво значи „евентуална съгласуваност“?
3. Защо помиряването събира продажбите, а не числата на наличността?
4. Кое е разумно за онлайн магазина?
5. Кога евентуалната съгласуваност вреди най-много?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·