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

Split brain: ce este și de ce strică totul

Partiția de rețea în care ambele jumătăți ale unui cluster cred că sunt liderul. De ce quorum e singura apărare fiabilă.

Cu două lecții de partitioning și sharding în spate, aceasta e despre modul de eșec care trăiește discret sub toate. Clusterul are un lider. Liderul replică către followeri. Rețeaua merge în mare parte. Apoi nu mai merge. Un switch eșuează, un routing table se actualizează prost, un cablu e scos din priză în rack-ul greșit, un bug software în țesătura de rețea izolează o zonă de celelalte. Clusterul e partiționat: nu data partitioned, ci network partitioned. Unele noduri pot vorbi între ele, dar nu și cu restul. Liderul e pe o parte, niște followeri sunt pe cealaltă parte, iar ce se întâmplă mai departe e subiectul.

Numele principal pentru cazul cel mai rău e split brain. E numele situației în care ambele jumătăți ale clusterului cred că ele conduc, ambele acceptă scrieri și ambele ajung cu state pe care cealaltă jumătate nu le are. Când rețeaua se vindecă și cele două jumătăți încearcă să facă merge, baza de date are istorii divergente și nu există nicio cale automată de a le reconcilia fără a pierde date sau a viola invarianți. Această lecție e despre cum se întâmplă split brain, de ce e unic de catastrofal printre modurile de eșec și puținele mecanisme care îl previn fiabil.

Scenariul

Consideră un cluster de trei noduri: un lider, L, și doi followeri, F1 și F2. Clienții trimit scrieri liderului; liderul replică la followeri; toată lumea e fericită.

Acum o partiție de rețea taie clusterul: L e izolat pe o parte; F1 și F2 sunt împreună pe cealaltă. Din punctul de vedere al lui F1 și F2, liderul a tăcut. Heartbeat-urile lipsesc. Așteaptă election timeout-ul, decid că liderul e mort și-l aleg pe F1 ca nou lider. De pe partea lor a partiției, asta e exact ce ar fi trebuit să facă.

Din punctul de vedere al lui L, followerii au tăcut. L încă primește heartbeat-uri de la el însuși și de la orice clienți încă pe partea sa de rețea. Crucial, L încă crede că e liderul. Încă acceptă scrieri de la clienți. Clienții de pe partea sa a partiției primesc răspunsuri de succes la cererile lor, complet inconștienți că ceva e în neregulă.

Acum rețeaua se vindecă. F1 și F2, cu F1 ca nou lider, au o serie de scrieri pe care L nu le are. L are o serie de scrieri pe care F1 și F2 nu le au. Cele două jumătăți nu doar că nu sunt de acord asupra istoriei: fiecare are o listă de scrieri confirmate pe care cealaltă nu le-a văzut niciodată, cu chei suprapuse, cu valori conflictuale, fără nicio cale de a le combina fără a alege ce scrieri să piardă.

sequenceDiagram
    participant Client1 as Client (left side)
    participant L as Leader L
    participant F1 as Follower F1
    participant F2 as Follower F2
    participant Client2 as Client (right side)
    Note over L,F2: Network partition isolates L
    Client1->>L: write A
    L->>L: accept A (still thinks leader)
    F1->>F2: heartbeat timeout
    F1->>F2: elect F1 as new leader
    Client2->>F1: write B
    F1->>F2: replicate B
    Note over L,F2: Partition heals
    L->>F1: hello, I am leader
    F1->>L: no, I am leader (term incremented)
    Note over L,F1: A and B are conflicting writes; merge is undefined

Acesta e split brain. Ambele părți au făcut exact ce au fost proiectate să facă; sistemul ca întreg a produs state pe care baza de date nu le poate reconcilia.

De ce split brain e unic de catastrofal

Majoritatea eșecurilor din sistemele distribuite sunt recuperabile. Un nod cade: îl aduci înapoi, redai log-ul, ai recuperat. O replică rămâne în urmă: recuperează prin streaming-ul log-ului. Un lider moare: alegi unul nou, oricum cel vechi nu accepta scrieri. Chiar și eșecurile bizantine (în care un nod minte) pot fi tolerate prin protocoale proiectate pentru asta.

Split brain e diferit. Ambele părți au acceptat scrieri pe care cealaltă parte nu le-a văzut. Ambele părți le-au spus clienților că acele scrieri au reușit. Clienții au mers mai departe. Când partiția se vindecă, baza de date are două istorii la fel de valide care nu sunt de acord pe aceleași chei și niciun mecanism de a alege între ele fără a încălca promisiuni deja făcute.

Exemplul clasic ilustrativ e un registru financiar. Un utilizator are 100 de dolari. Utilizatorul emite o retragere de 60 de dolari către partea stângă a partiției; liderul stâng o autorizează, soldul e acum 40. Utilizatorul, frustrat de un răspuns lent de undeva, emite și o retragere de 60 de dolari către partea dreaptă a partiției; liderul drept, cu o copie veche a soldului care încă arată 100, o autorizează și el, soldul e acum 40 pe partea sa. Când partiția se vindecă, baza de date a autorizat retrageri de 120 de dolari dintr-un cont de 100 de dolari. Nu există un merge automat care să recupereze; banca a pierdut 20 de dolari.

Aceeași formă se aplică oricărui sistem cu invarianți care depind de o vedere globală a datelor: inventar („există doar unul din acest articol, nu poate fi vândut de două ori”), constrângeri de unicitate („acest username e luat”), contoare („această resursă a fost achiziționată de exact un client”), chei de idempotență („această operație trebuie rulată cel mult o dată”). Pentru toate acestea, doi lideri care acceptă scrieri conflictuale e modul de eșec care violează proprietatea pe care sistemul ar fi trebuit s-o ofere.

Quorum e singura apărare fiabilă

Apărarea, singura apărare fiabilă, e să ceri ca o majoritate a nodurilor să fie de acord înainte ca orice scriere să poată fi acceptată. Cu trei noduri, ai nevoie de două pentru un quorum. O partiție 1-2: partea cu două noduri poate forma o majoritate și accepta scrieri; partea cu unul nu poate. Nodul singuratic vede că nu poate ajunge la o majoritate a colegilor săi, refuză să accepte scrieri și fie stă inactiv, fie returnează erori clienților săi. Nu există posibilitatea ca doi lideri să accepte simultan scrieri, pentru că nu există posibilitatea ca două majorități să existe simultan într-o partiție a unui cluster de trei noduri.

Exact din acest motiv Raft și Paxos cer voturi majoritare pentru election-ul liderului și pentru log commit (lecția 14). Cerința de majoritate nu e o alegere arbitrară de design; e singura regulă de quorum care face split brain matematic imposibil. Oricare două majorități de N noduri trebuie să se suprapună în cel puțin un nod, iar acel nod care se suprapune cu „am votat pentru acest lider” sau „am committed acest log entry” impune o singură sursă de adevăr peste partiție.

Rețeta de deployment care decurge de aici e directă și ușor de uitat sub presiunea costurilor: rulează cluster-uri cu număr impar, minim trei, cinci pentru orice sistem la care ții, șapte dacă vrei într-adevăr să supraviețuiești unor eșecuri simultane multiple. Cluster-urile cu număr par sunt operațional mai rele decât cele cu număr impar. Un cluster de patru noduri împărțit 2-2 nu are majoritate nicăieri, deci întreg clusterul e indisponibil pentru scrieri în timpul partiției. Trecerea de la trei la patru noduri nu îmbunătățește disponibilitatea; o înrăutățește. Cinci noduri supraviețuiesc la două eșecuri simultane și se partiționează curat în împărțiri 3-2 cu o majoritate pe partea mai mare.

Cluster-urile de două noduri sunt deployment-ul care te mușcă cel mai des. Două noduri n-au majoritate când sunt partiționate: fiecare parte are exact un nod, iar unul nu e o majoritate din doi. Deci fie accepți că clusterul e indisponibil pentru scrieri în timpul oricărei partiții (și ai putea la fel de bine să rulezi un singur nod, care are același profil de disponibilitate și jumătate din costul operațional), fie permiți fiecărui nod să acționeze pe cont propriu când e partiționat și acum tocmai ai proiectat un split brain. Nu rula cluster-uri de două noduri pentru nimic ce contează. Fie rulează un nod și acceptă onest single-point-of-failure, fie rulează trei noduri și obține beneficiul real de disponibilitate.

Fencing: a doua linie de apărare

Quorum-ul previne doi lideri simultani. Nu previne ca un lider să creadă pentru o scurtă perioadă că încă e lider după ce a pierdut quorum-ul. Scenariul: un lider e izolat de followerii săi, followerii aleg un nou lider, dar liderul vechi încă n-a observat că e partiționat și continuă să trimită scrieri către servicii downstream. Serviciile downstream nu știu despre protocolul de consensus; văd scrieri de la un nod care pretinde a fi liderul și le acceptă.

Soluția e fencing tokens. Sistemul de consensus atașează un token monoton crescător (numit adesea term, sau epoch, sau fencing number) la fiecare acordare de leadership. Fiecare scriere pe care liderul o trimite la un serviciu downstream include acest token. Serviciile downstream își amintesc cel mai mare token pe care l-au văzut vreodată și resping orice scriere care sosește cu un token mai mic. Când noul lider e ales, token-ul său e mai mare decât al liderului vechi. Când liderul vechi, încă operând pe credința sa veche, trimite o scriere unui serviciu downstream cu token-ul său vechi, serviciul o respinge: „token-ul tău e 7, am văzut deja token-ul 8 de la altcineva”. Liderul vechi e fenced out.

Fencing-ul e răspunsul corect pentru intervalul dintre „am pierdut quorum-ul” și „am observat că am pierdut quorum-ul”. Acoperă și cazul liderului mai lent: un lider face pauză pentru garbage collection sau un swap-out timp de cincisprezece secunde, clusterul decide că e mort și alege unul nou, iar când liderul vechi se reia încearcă să scrie ca și cum nimic nu s-ar fi întâmplat. Fără fencing, serviciile downstream îi acceptă scrierile; cu fencing, le resping.

Implementarea fencing-ului cere ca serviciile downstream să fie conștiente de token, ceea ce înseamnă coordonarea fencing-ului între serviciile aplicației și dependențele externe. Unele sisteme fac asta bine; multe nu. Când citești despre un postmortem „am pierdut date din cauza unui split brain”, layer-ul lipsă e de obicei fencing tokens la granița de storage sau de serviciu extern.

Pattern-uri reale de eșec

Rapoartele de incident specifice variază, dar formele revin de-a lungul multor ani și multor sisteme. Pattern-urile care merită recunoscute:

Failover bazat pe DNS cu TTL prea lung în timpul unei partiții. O bază de date primary cade, echipa de operations comută o înregistrare DNS să indice către replică, iar traficul începe să se mute spre replică. Dar clienții au cache-uit înregistrarea DNS veche pentru durata TTL-ului; continuă să vorbească cu primary-ul vechi, care apoi revine online și începe să accepte scrieri din nou, în timp ce alți clienți s-au mutat la noul primary. Două baze de date, ambele acceptând scrieri, lipite împreună de un rollover lent de DNS.

Failover de IP virtual cu split brain la nivelul rețelei. Uneltele HA bazate pe heartbeat (Pacemaker, Keepalived, stack-urile HA mai vechi) mută un IP virtual de pe un primary eșuat la un standby. Dacă link-ul de heartbeat dintre cele două noduri eșuează, dar rețeaua publică încă funcționează, ambele noduri pot decide că sunt nodul activ și ambele pot revendica același VIP. Clienții de pe fiecare parte a rețelei văd „primary-uri” diferite cu aceeași adresă.

Replicare asincronă cu failover manual și rejoin neglijent. Un primary cade, echipa promovează o replică, primary-ul original revine, cineva rulează o comandă „rejoin to the cluster” fără să verifice mai întâi că primary-ul original n-a avut scrieri care nu s-au replicat înainte să cadă. Acele scrieri sunt aruncate tăcut, sau mai rău, sunt merge-uite tăcut într-un mod care creează duplicate.

Cluster-uri de două noduri fără consensus. Rețeta clasică: un primary și o replică cu heartbeat reciproc. Link-ul de heartbeat eșuează, ambele noduri se autopromovează, ambele acceptă scrieri pe durata partiției, iar rejoin-ul face merge la istoriile divergente cu orice euristică alege operatorul la 3 dimineața.

În fiecare caz, eroarea de bază e aceeași: nu exista o cerință de majoritate, niciun fencing token, niciun protocol de consensus care să decidă autoritar „acest nod e liderul și doar acest nod”. Split brain e ce se întâmplă când leadership-ul e decis de ceva mai puțin riguros decât un quorum.

Mitigările

Un set mic de practici previne fiabil split brain.

Folosește un sistem real de consensus pentru cluster membership. Raft (etcd, Consul, varianta ZAB a ZooKeeper-ului) e default-ul modern. Membership-ul și leadership-ul clusterului sunt gestionate de un protocol de consensus, cu majority quorum, cu term-uri monotone. Nu inventa propriul.

Rulează cluster-uri cu număr impar de trei sau mai multe noduri. Cinci e mai bun decât trei pentru producție. Două e mai rău decât unu. Patru e mai rău decât trei.

Implementează fencing tokens la graniță. Când liderul scrie la storage extern sau la un serviciu extern, atașează token-ul. Când storage-ul sau serviciul primește scrierea, verifică token-ul împotriva celui mai mare văzut.

Folosește STONITH pentru HA legacy. Shoot The Other Node In The Head: când un nod își pierde lease-ul, fencing-ul la nivel hardware îl oprește forțat din alimentare astfel încât să nu poată accepta scrieri în timpul intervalului. Acesta e răspunsul mai vechi, brutal și eficient pentru cluster-urile de high-availability construite pe storage partajat.

Nu rula nimic important pe un cluster de două noduri. Fie acceptă operarea cu un singur nod, fie treci la trei.

Ce pregătește această lecție

Split brain e modul de eșec principal al sistemelor distribuite: cel cu cele mai grave consecințe și cel mai des înțeles greșit. Apărările (quorum, fencing, dimensiuni sănătoase de cluster) sunt toate bine cunoscute, dar în fiecare an echipele livrează sisteme cărora le lipsesc și plătesc factura când rețeaua are o zi proastă. Suita de teste Jepsen, rulată de Kyle Kingsbury (Aphyr), a petrecut peste un deceniu publicând rapoarte detaliate despre baze de date distribuite care eșuează sub partiție; multe dintre eșecuri se reduc la „sistemul nu avea de fapt quorum acolo unde pretindea că are”.

Această lecție a acoperit modul de eșec dramatic. Lecția următoare acoperă durerea zilnică mai comună: query-urile cross-shard. Majoritatea workload-urilor nu văd split brain; văd „acest query trebuie să vorbească cu patru shard-uri și unul dintre ele e lent” în fiecare minut din fiecare zi. Strategiile pentru a trăi cu acea realitate obișnuită sunt diferite, iar de acolo continuă lecția 31.

Citări și lecturi suplimentare

  • Martin Kleppmann, “Designing Data-Intensive Applications” (O’Reilly, 2017), Capitolul 9 (consistency and consensus). Tratarea de lungimea unei cărți a split brain-ului, fencing-ului și quorum-ului.
  • Kyle Kingsbury (Aphyr), “Jepsen”, https://jepsen.io/analyses (consultat 2026-05-01). Un deceniu de analize ale bazelor de date distribuite care eșuează sub partiție. Alege aproape orice raport pentru un studiu de caz concret.
  • Diego Ongaro și John Ousterhout, “In Search of an Understandable Consensus Algorithm”, USENIX ATC 2014, https://raft.github.io/raft.pdf (consultat 2026-05-01). Lucrarea Raft, care introduce explicit term-urile (fencing token-urile protocolului).
  • Documentația Pacemaker, “Fencing and STONITH”, https://clusterlabs.org/pacemaker/doc/ (consultat 2026-05-01). Referința HA legacy pentru fencing hardware.
  • Lecția 14 a acestei serii (consensus, Paxos și Raft) și lecția 16 (semantici de delivery și idempotență) pentru blocurile de construcție pe care această lecție le presupune.
Caută