Arhitectura datelor și a sistemelor, de la zero Lecția 11 / 80

PACELC: ce a ratat CAP

Extensia lui Daniel Abadi. Chiar și în absența partițiilor, dai latency pentru consistency.

Lecția anterioară a lăsat CAP într-o poziție ușor neflatantă. Teorema e corectă, are o formulare precisă și descrie ceva real: în prezența unei partiții de rețea, trebuie să alegi între consistency și availability. Dar nu spune nimic despre cazul care ocupă aproape tot timpul sistemului tău, adică cazul în care nu există partiție. Baza ta de date distribuită e în starea partiționată câteva secunde pe săptămână, într-o săptămână proastă. Restul de 99,99% din timp, CAP nu spune nimic despre ea.

Daniel Abadi a observat asta în 2010 și ne-a dat cadrul care umple lacuna. L-a numit PACELC. Acronimul e urât, ideea e curată. Hai să-l formulez o dată și apoi să-l despachetez:

PACELC: dacă Partition, alege Availability sau Consistency; Else, alege Latency sau Consistency.

Prima jumătate (PAC, „dacă Partition, A sau C”) e exact CAP. A doua jumătate (ELC, „Else, L sau C”) e partea pe care CAP nu a numit-o. Spune: chiar și când nimic nu e stricat, chiar și pe o rețea perfect sănătoasă, chiar și într-o marți după-amiază fără incidente în desfășurare, încă faci un compromis între latency și consistency la fiecare scriere într-un sistem replicat. Compromisul ăla nu apare în CAP pentru că CAP e doar despre cazul partiționat. PACELC îl trage la lumină.

De ce cazul de zi cu zi are propriul compromis

Ca să vezi de ce există compromisul ELC, imaginează-ți o bază de date strongly consistent cu replici în trei regiuni: Londra, Frankfurt și Singapore. Strong consistency înseamnă că orice read, din orice regiune, returnează cea mai recentă scriere comisă. Ca să onoreze promisiunea, sistemul trebuie să se asigure că o scriere e confirmată durabil de un quorum de replici, inclusiv cel puțin câteva din regiuni diferite de cea care acceptă scrierea. Asta înseamnă că fiecare scriere suportă cel puțin un round trip cross-region.

Un round trip cross-region e scump. Londra-Frankfurt e în jur de cincisprezece milisecunde. Londra-Singapore e mai aproape de o sută șaptezeci. Chiar dacă aplicația ta nu face nimic altceva, fiecare scriere te costă între cincisprezece și două sute de milisecunde de timp inevitabil pe ceas, plătit ca taxă de consistency.

Acum imaginează-ți alternativa. Același sistem, dar operatorul optează pentru scrieri „eventually consistent”. Scrierea e confirmată de îndată ce replica locală o are stocată durabil. Replicarea către celelalte regiuni se întâmplă asincron în fundal. Latency scade de la o sută șaptezeci de milisecunde la în jur de două. Costul e că, pentru o fereastră scurtă, un read din Singapore poate să nu vadă încă scrierea pe care Londra tocmai a confirmat-o.

Acela e compromisul ELC. Rețeaua nu e partiționată. Totul e sănătos. Tot plătești, la fiecare scriere, fie în latency, fie în consistency. Nu ai opțiunea să renunți. Ai doar opțiunea să alegi.

Clasificarea în patru cadrane

PACELC dă fiecărei baze de date distribuite un cod de două litere. Prima literă e comportamentul în partiție: PA (disponibil în timpul partiției, posibil învechit) sau PC (consistent în timpul partiției, posibil indisponibil). A doua literă e comportamentul fără partiție: EL (low latency, posibil învechit) sau EC (consistent, cu costul de latency).

Combinându-le obții patru cadrane. Majoritatea sistemelor reale stau în două dintre ele.

flowchart TD
    A[PACELC quadrants] --> Q1[PA / EL]
    A --> Q2[PC / EC]
    A --> Q3[PA / EC]
    A --> Q4[PC / EL]
    Q1 --> Q1E[Cassandra default, DynamoDB default, Riak, S3, MongoDB default]
    Q2 --> Q2E[Spanner, FaunaDB, CockroachDB, sync-replicated relational]
    Q3 --> Q3E[Rare in pure form: trades availability for latency wins]
    Q4 --> Q4E[Rare in pure form: trades consistency only on partition]

PA/EL: disponibil, rapid, posibil învechit

Sistemele PA/EL sunt optimizate pentru high availability și low latency. Servesc scrieri local, replică asincron și acceptă că doi clienți pot vedea momentan valori diferite. Cassandra cu setările implicite de consistency, Riak, MongoDB cu setările implicite și DynamoDB fără strongly-consistent reads sunt toate PA/EL.

Aceste sisteme se potrivesc perfect cu workload-uri unde datele învechite sunt ieftine, iar downtime-ul e scump. Activity feed-uri, recomandări de conținut, stocare de sesiuni, ingestie analitică la scară mare, telemetrie IoT. Orice caz în care „răspunsul ar putea fi cu câteva secunde în urmă” nu contează, dar „sistemul e indisponibil un minut” te face să pierzi bani.

PC/EC: consistent, în ambele capete, mai lent

Sistemele PC/EC plătesc costul de latency al consistency-ului la fiecare scriere și refuză în timpul partițiilor pentru a păstra corectitudinea. Spanner e exemplul de manual. Google l-a construit specific pentru a oferi strong consistency cross-region, folosind TrueTime sincronizat cu GPS-uri și ceasuri atomice pentru a limita driftul de ceas, și apoi acceptând că fiecare scriere costă timpul de round-trip global.

CockroachDB și FaunaDB sunt descendenții oarecum open-source ai ideii Spanner. Bazele de date relaționale tradiționale cu replicare sincronă (Postgres cu synchronous_commit = remote_apply și un standby sincron într-o altă zonă) stau în același cadran.

Sistemele PC/EC sunt potrivite pentru workload-uri unde consistency e nenegociabilă și bugetul de latency poate absorbi zeci de milisecunde per scriere. Registre financiare, sisteme de inventar unde over-selling e un cost real, date reglementare unde corectitudinea auditului contează mai mult decât throughput-ul. Argumentul Spanner a fost mereu că, la scară, ai prefera să iei lovitura de latency decât să construiești corectitudinea peste un store eventually-consistent singur.

PA/EC și PC/EL: cadranele rare

În forma lor pură, celelalte două cadrane sunt rare. PA/EC e un sistem care renunță la consistency în partiție, dar insistă pe consistency în afara uneia. Asta e greu de construit corect pentru că fereastra de partiție va produce stare divergentă pe care read-urile în mod consistent trebuie apoi să o reconcilieze. PC/EL e inversul: refuză în partiție, dar e eventually consistent în afara uneia. Asta e și ea rară pentru că, dacă ești dispus să refuzi în partiție pentru corectitudine, de obicei vrei consistency și în restul timpului.

Sistemele tunable estompează cadranele. Cassandra la consistency level QUORUM se comportă mult mai aproape de PC/EC pentru operațiile care îl folosesc, rămânând PA/EL pentru operațiile care folosesc ONE. DynamoDB lasă fiecare apel să aleagă. Așa că diagrama cadranelor e punctul de plecare arhitectural; alegerea per operație e experiența reală trăită.

Un exemplu concret: un session store

Hai să facem asta concret. Proiectezi session store-ul pentru o aplicație web cu utilizatori autentificați. Sesiunile sunt citite la fiecare cerere și actualizate când utilizatorul face orice ar trebui să-i actualizeze timestamp-ul de last-active. Două întrebări de răspuns:

  1. Ce se întâmplă în partiție? Dacă session store-ul e partiționat, refuzi login-urile (PC) sau servești o sesiune posibil învechită (PA)?
  2. Ce se întâmplă în cazul de zi cu zi? Read-urile costă un round trip cross-region (EC) sau citesc dintr-o replică locală (EL)?

Pentru majoritatea session store-urilor, răspunsul e PA/EL. O sesiune învechită e bine pentru câteva secunde (utilizatorul rămâne autentificat, timestamp-ul de last-active e ușor în urma realității). Refuzul tuturor login-urilor în timpul unei partiții e mult mai rău decât servirea dintr-o replică învechită. Iar bugetul de read latency la fiecare încărcare de pagină e suficient de strâns încât să plătești pentru cross-region consistency la fiecare read e un compromis prost.

Așa că implementarea e, să zicem, DynamoDB cu eventually-consistent reads, sau Cassandra cu consistency level ONE, sau Redis cu replicare async. Cost mai mic, latency mai mică, date învechite ocazional, disponibil în timpul micilor partiții inevitabile.

Acum contrastează cu un registru bancar. Aceleași două întrebări:

  1. În partiție, ce ar trebui să se întâmple cu o retragere? Refuz (PC). Mai bine o eroare decât două retrageri din aceiași o sută de dolari.
  2. În cazul de zi cu zi, poate un read din altă regiune să fie învechit? Nu (EC). Registrul trebuie să fie linearizable astfel încât fiecare cont să vadă același sold.

Așa că implementarea e Spanner, FaunaDB sau un cluster Postgres cu replicare sincronă peste două AZ-uri. Latency mai mare, cost mai mare, refuză în timpul partiției, nu minte niciodată.

Cele două sisteme stau în cadrane PACELC opuse. Ambele sunt corecte. Fiecare e corect pentru workload-ul său.

Întrebarea per operație

Cea mai utilă cale de a aplica PACELC nu e să clasifici un sistem, ci să clasifici fiecare operație. O singură aplicație are de obicei ambele tipuri de operații.

Un flux de login are nevoie de PC pentru verificarea credențialelor (mai bine să refuze decât să autorizeze utilizatorul greșit) și EC pentru read-ul rolului și permisiunilor utilizatorului (astea ar fi bine să nu fie învechite). Cost: câteva milisecunde în plus și o eroare în loc de o blocare în timpul unei partiții. Acceptabil.

Un feed „arată activitatea recentă” pe dashboard-ul aceluiași utilizator poate fi fericit PA/EL. Dacă feed-ul e cu o secundă în urmă, nu moare nimeni. Dacă dashboard-ul refuză să se încarce într-o mică partiție, utilizatorii tăi sunt mai supărați decât dacă arată date ușor vechi.

Un flux de cumpărare cu autorizare de plată are nevoie de PC pentru taxare (nu taxa dublu în partiție) și EC pentru read-ul totalului din coș (nu lăsa utilizatorul să plătească o sumă veche). Email-ul de confirmare post-cumpărare poate fi PA/EL.

Ideea e că „ce bază de date ar trebui să folosim” e nivelul greșit la care să pui întrebarea. Nivelul corect e „de ce are nevoie fiecare operație”, iar de acolo ori alegi o bază de date tunable care îți permite să exprimi ambele moduri, ori alegi două baze de date (un primary cu strong consistency pentru operațiile critice, un cache sau o replică eventually-consistent pentru rest), ori accepți costul de latency al rulării a totul pe un sistem PC/EC pentru că simplitatea operațională valorează mai mult decât economiile de latency.

Un ghid de citire pentru cele patru cadrane

Dacă cunoști fiecare sistem doar după reputație, plasarea în cadran te ajută să pui întrebări mai bune în timpul evaluării. Un scurt ghid de citire:

  • Cassandra și Riak sunt PA/EL implicit. Punctul lor de vânzare e să rămână în picioare. Tunable până la PC/EC pentru operații individuale folosind consistency level QUORUM sau ALL, dar costul operațional crește cu consistency-ul.
  • DynamoDB e PA/EL implicit și suportă strongly-consistent reads per apel (care mută acel apel la PC/EC). Auto-scaling și complet gestionat, ceea ce e adevăratul punct de vânzare.
  • MongoDB e PA/EL implicit la read-uri pe o singură replică și PC/EC dacă citești de la primary cu majority write concern. Setările implicite au cauzat câteva incidente publice genuin jenante.
  • Spanner, CockroachDB, FaunaDB sunt PC/EC. Strong consistency e funcționalitatea de pe afiș; latency e costul. Folosește când corectitudinea cross-region e cerința.
  • Un Postgres sau MySQL tradițional cu replicare sincronă către un standby e PC/EC la scară mică (un primary, o replică sincronă, ambele în aceeași regiune). Adaugă replici async cross-region și obții PA/EL pe acele replici, ceea ce e bine atâta timp cât citești din ele doar când datele învechite sunt acceptabile.

Numele se schimbă pe măsură ce setările implicite se schimbă între versiuni, așa că sfatul durabil e: întreabă „ce face în timpul unei partiții” și „ce face pe o rețea sănătoasă” și scrie răspunsurile. PACELC îți dă pur și simplu cele patru căsuțe în care să le scrii.

Ce înseamnă cu adevărat „consistent”

Cuvântul „consistent” a făcut muncă grea în lecția asta și în cea anterioară. CAP și PACELC îl tratează amândouă ca pe un binar: linearizable sau nu. În sistemele reale, „consistent” e un spectru, cu cel puțin patru puncte numite: linearizable, sequential, causal și eventual. Fiecare are un cost diferit și un set diferit de garanții. Un sistem care e „eventually consistent” face lucruri foarte diferite de unul care e „causally consistent”, iar diferența contează pentru ce vede aplicația ta.

Lecția 12 despachetează acel spectru, cu exemple despre ce garanție se mapează la ce tip de bug. La sfârșitul ei, vei putea citi o pagină de documentație de bază de date care spune „oferim read-your-writes consistency într-o sesiune și eventual consistency între sesiuni” și să știi ce îți cumpără asta, ce te costă și ce bug-uri de aplicație te protejează și nu te protejează.

După aceea, restul Modulului 2 se întoarce către pattern-urile și protocoalele care pun aceste compromisuri în practică: strategii de replicare, sisteme de quorum, rezolvare de conflicte și rolul central al algoritmilor de consensus în orice sistem care vrea să facă garanții în stil CP. PACELC îți dă meniul. Restul modulului e despre ce face fiecare element din meniu sistemului tău.

Citate și lecturi suplimentare

  • Daniel Abadi, “Problems with CAP, and Yahoo’s little known NoSQL system” (2010), the original blog post that introduced PACELC. Reproduced at http://dbmsmusings.blogspot.com/2010/04/problems-with-cap-and-yahoos-little.html (retrieved 2026-05-01).
  • Daniel Abadi, “Consistency Tradeoffs in Modern Distributed Database System Design”, IEEE Computer, 2012. The peer-reviewed write-up of PACELC with the four-quadrant classification of the major systems of the time.
  • James C. Corbett et al., “Spanner: Google’s Globally Distributed Database”, OSDI 2012. The canonical PC/EC system, with the TrueTime mechanism that makes cross-region linearizability tractable.
  • Apache Cassandra documentation on tunable consistency levels.
  • Amazon DynamoDB Developer Guide, sections on consistent reads and global tables. Retrieved 2026-05-01.
  • Kyle Kingsbury’s Jepsen series at https://jepsen.io/. Empirical testing of what databases actually do under partition. The single best resource for cross-checking vendor consistency claims against observed behaviour.
Caută