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

Arhitectura Lambda vs kappa

Contextul istoric: de ce a existat Lambda, de ce a înlocuit-o Kappa și unde mai are Lambda un rost în 2026.

Lecțiile precedente din Modulul 6 au introdus blocurile de construcție pentru streaming. Kafka ca log-ul durabil. Flink și Spark Structured Streaming ca motoarele care calculează peste date în mișcare. Watermark-uri și event time ca model pentru „când pot face commit la un rezultat”. Lecția asta zoom-uie înapoi și pune întrebarea arhitecturală pe care Modulul 5 și Modulul 6 împreună o forțează asupra fiecărei echipe de date: când ai și pipeline-uri batch, și de streaming, cum se potrivesc între ele?

Există două răspunsuri canonice, ambele cu nume care sună mult mai pretențios decât sunt. Arhitectura Lambda rulează un pipeline batch și un pipeline streaming unul lângă altul și le îmbină la query time. Arhitectura Kappa rulează doar un pipeline streaming și redă log-ul istoric atunci când are nevoie de view-ul batch. Numele au venit de la oamenii care le-au propus. Decizia e mai veche decât numele.

Până în 2026, răspunsul practic e în mare parte Kappa, cu nuanțe. Munca interesantă e să înțelegi de ce a existat Lambda, de ce a înlocuit-o Kappa și setul îngust de cazuri unde Lambda își mai câștigă încă pâinea.

Lumea care a produs Lambda

Nathan Marz a propus arhitectura Lambda într-o serie de postări de blog și prezentări începând din 2011, formalizate în cartea sa din 2015 „Big Data” împreună cu James Warren. Contextul contează. În 2011 unealta de batch a zilei era Hadoop cu MapReduce. Uneltele de streaming erau Storm (pe care Marz îl construise la BackType și Twitter) și o mână de competitori mai puțin maturi. Kafka exista, dar era nou. Flink era un proiect de cercetare. Spark era o lucrare AMPLab.

În acea lume, batch-ul și streaming-ul erau două animale diferite în două sensuri, ambele împingând spre rularea lor separat.

Primul sens era tehnic. Uneltele batch procesau seturi de date istorice mărginite, optimizate pentru throughput, și produceau rezultate exacte. Uneltele de streaming procesau date nemărginite, optimizate pentru latență, și produceau rezultate aproximative: numărători grosiere, valori unice aproximative, sketch-uri care schimbau acuratețea pe viteză. Cele două motoare aveau API-uri diferite, modele diferite de fault-tolerance și profile diferite de performanță. A cere unui motor de streaming din 2011 să facă și cazul istoric batch însemna să-i ceri să facă ceva pentru care nu fusese construit.

Al doilea sens era operațional. Job-urile de streaming erau considerate fragile. Puteau pierde date la eșec, sufereau de evenimente întârziate fără cale evidentă de recuperare și se îndepărtau de adevăr peste uptime-uri lungi. Pipeline-ul batch era sursa adevărului; pipeline-ul streaming era o aproximare care îți dădea ceva de privit cât așteptai să se termine rularea batch. Dacă pipeline-ul streaming se strica, rularea batch îl corecta a doua zi dimineață.

Lambda a formalizat acea separare ca arhitectură.

Cum arată Lambda

Trei straturi, toate citind același log de input de evenimente imutabile.

Stratul batch. Stochează întregul set de date istoric și-l reprocesează periodic (de obicei nocturn) ca să producă view-uri exacte și complete. Output-ul se numește batch view. Dacă o rulare batch durează șase ore, batch view-ul e vechi de până la douăzeci și patru de ore în orice moment dat. E ok, pentru că batch view-ul e sursa adevărului: vechi, dar corect.

Stratul speed. Citește același log de input în timp real și produce un real-time view care acoperă doar evenimentele care au sosit de la ultima rulare batch. Real-time view-ul e aproximativ (poate folosi sketch-uri, sampling sau alte compromisuri viteză-pentru-acuratețe) și e de scurtă durată: imediat ce se termină următoarea rulare batch, porțiunea corespunzătoare din real-time view e aruncată.

Stratul de servire. Primește query-uri, citește din ambele view-uri și îmbină rezultatele. Un query care cere „totalul evenimentelor din ultimele șapte zile” citește batch view-ul pentru zilele pe care le-a acoperit batch-ul și real-time view-ul pentru tot ce a venit de la ultima rulare batch. Utilizatorul vede un singur răspuns care e în mare parte exact (partea batch) și parțial aproximativ (coada real-time).

flowchart LR
    LOG[(Event log)]
    BATCH[Batch layer<br/>full history<br/>exact, slow]
    SPEED[Speed layer<br/>recent only<br/>approximate, fast]
    BV[(Batch view)]
    RTV[(Real-time view)]
    SERVE[Serving layer]
    Q[Query]
    LOG --> BATCH --> BV
    LOG --> SPEED --> RTV
    BV --> SERVE
    RTV --> SERVE
    Q --> SERVE

În lumea 2011-2015 ăsta era un design de apărat. Primeai răspunsuri cu latență mică din stratul speed, răspunsuri exacte din stratul batch, iar îmbinarea la query time îți dădea cele mai bune din ambele. Arhitectura supraviețuia eșecurilor de motor cu grație: stratul batch ar fi recuperat data viitoare când rula, iar aproximările stratului speed erau ieftine de aruncat.

Plângerea despre Lambda

Plângerea, exprimată tare de Jay Kreps în 2014 și de mulți alții de atunci, e că Lambda te face să întreții două baze de cod care calculează același lucru.

Pipeline-ul batch are un job Spark sau MapReduce care agregă șapte zile de evenimente într-o numărătoare. Pipeline-ul speed are un job Storm sau Flink care face aceeași agregare în timp real. Logica e aceeași; implementarea e duplicată, în limbaje diferite, cu API-uri diferite, cu harnesses de testare diferite, deployate la infrastructuri diferite, deținute adesea de echipe diferite.

Urmează trei dureri operaționale.

Bug fix-urile trebuie să aterizeze de două ori. Repară un bug în job-ul batch, apoi portează fix-ul la job-ul streaming. Cele două baze de cod se îndepărtează sub presiunea deadline-ului când doar drumul urgent primește patch. Eventual batch view-ul și real-time view-ul încep să nu fie de acord în moduri subtile, iar a-ți da seama care din ele are dreptate devine un exercițiu de criminalistică.

Schemele se îndepărtează. Un câmp nou e adăugat la evenimentele de input. Job-ul batch îl prinde; job-ul streaming nu, pentru că topologia Storm a fost atinsă ultima oară acum opt luni și nimeni nu și-a amintit s-o updateze. View-ul îmbinat începe să arate valori inconsistente pentru noul câmp în funcție de cum cade punctul de date în partea batch sau partea speed a ferestrei.

Suprasarcina operațională se dublează. Două pipeline-uri înseamnă două clustere, două căi de deployment, două rotații on-call, două seturi de dashboard-uri, două seturi de alerte. Pentru o echipă mică asta e o fracțiune reală din capacitatea totală de inginerie.

Costul e plătit zilnic. Beneficiul (latență mică plus acuratețe exactă) e real, dar costul era destul de mare încât oamenii au început să se întrebe dacă era strict necesar.

Kappa: un singur pipeline

Jay Kreps, atunci la LinkedIn și autorul original al Kafka, a propus arhitectura Kappa într-o postare din 2014 intitulată „Questioning the Lambda Architecture”. Argumentul era simplu: dacă procesorul tău de stream e suficient de bun încât să gestioneze cazul istoric prin replay, nu ai nevoie deloc de pipeline-ul batch.

Designul Kappa e un singur pipeline.

flowchart LR
    LOG[(Event log<br/>Kafka, durable, replayable)]
    STREAM[Stream processor<br/>Flink or Spark Streaming]
    VIEW[(Materialised view)]
    Q[Query]
    LOG --> STREAM --> VIEW --> Q

Procesorul de stream citește dintr-un log durabil și redabil (Kafka, Kinesis, Pulsar). Pentru muncă în timp real, citește coada log-ului și actualizează view-ul materializat pe măsură ce sosesc evenimente noi. Ca să reconstruiești view-ul de la zero (pentru că logica s-a schimbat, sau view-ul s-a corupt, sau ai descoperit un bug), pornești o instanță nouă a procesorului de stream care citește de la începutul log-ului, o lași să recupereze și comuți traficul către view-ul nou când îl depășește pe cel vechi. Același cod procesează ambele cazuri. Există o bază de cod, un set de garanții, o schemă, o rotație on-call.

Precondițiile tehnice ca Kappa să funcționeze nu erau satisfăcute în 2011. Erau satisfăcute până în 2018 și foarte mult până în 2026.

Log-ul trebuie să fie destul de durabil. Kafka cu retenție rezonabilă (lecția 42) ține evenimentele suficient cât să redea toată istoria. Pentru date care nu încap în retenția Kafka, evenimentele trăiesc și într-un tabel lakehouse (lecția 37) din care procesorul de stream poate face replay.

Procesorul de stream trebuie să gestioneze replay-ul în masă. Flink și Spark Structured Streaming, în 2026, pot mesteca date istorice cu throughput comparabil cu batch când au resursele. Același motor care procesează o sută de evenimente pe secundă din coada live poate procesa o sută de milioane pe secundă când face replay.

Store-urile de stare trebuie să fie reconstruibile. Job-urile streaming cu stare (lecția 43) își țin starea în RocksDB sau store-uri embedded similare susținute de checkpoint-uri. Un replay de la zero construiește starea de la zero pe măsură ce avansează. Nu există un pas separat de „încălzește cache-ul”.

Într-un setup Kappa, același job Flink care întreține un agregat în timp real e și job-ul care, dacă îndrepți o instanță proaspătă spre începutul log-ului, va produce agregatul istoric de la zero. O bază de cod. Un set de garanții. Un singur lucru de operat.

De ce a câștigat Kappa, în mare parte

Până în 2026 majoritatea arhitecturilor noi cu streaming-și-batch o iau pe Kappa ca default. Trei motive.

Procesoarele de stream au devenit destul de puternice. Flink cu stare în RocksDB, sink-uri exactly-once și watermark-uri pe event time face ce Storm n-a putut face în 2011. Spark Structured Streaming îți dă același motor pentru cazul streaming și cel batch, codul de streaming fiind un superset strict al codului batch. Obiecția tehnică care motiva Lambda nu mai e valabilă.

Lakehouse-urile furnizează stocarea de bază. Un setup Kappa are nevoie de o istorie durabilă și redabilă. Pentru fluxuri de mare volum care depășesc retenția Kafka, acea istorie trăiește în tabele Iceberg, Delta sau Hudi (lecția 37), pe care procesoarele de stream le pot citi ca sursă. Întrebarea „unde țin istoria lungă” are un răspuns curat.

Costul bazei de cod duble a fost mereu prea mare. Chiar și echipele care iubeau separarea de preocupări a Lambda ajungeau să plătească taxa de sincronizare la fiecare release. Odată ce Kappa a devenit viabil tehnic, simplificarea operațională a fost greu de combătut.

Studiul de caz de la finalul acestui modul (lecția 48, Uber) e o poveste în formă Kappa în practică: ingestion streaming-first, stocare lakehouse, query-uri batch și real-time servite din același strat fizic. Pattern-ul se repetă în cele mai mari platforme de date din industrie.

Unde mai are Lambda un rost

Default-ul 2026 e Kappa. Lambda nu e dispărută, dar cazurile unde-și câștigă complexitatea sunt mai înguste decât erau.

Răspunsurile batch și stream sunt cu adevărat diferite. Numărătorile aproximative de elemente distincte folosind HyperLogLog în stratul streaming sunt extrem de ieftine cu eroare mărginită. Numărătorile exacte de elemente distincte în stratul batch costă bani reali, dar îți dau adevărul. Dacă produsul tău vrea ambele (un dashboard rapid aproximativ plus un raport lent auditat) și sketch-ul de streaming nu e doar o versiune mai puțin precisă a agregatului batch, ci un calcul diferit, Lambda îți permite să le ții ca pipeline-uri separate care produc lucruri diferite, în loc de același lucru de două ori.

Latența vs acuratețea trăiesc în produse diferite. Un dashboard de trading floor vrea prospețime de treizeci de milisecunde pe volume și prețuri. Reconcilierea de la sfârșitul zilei vrea numere exacte, auditate, lente, semnate de compliance. Cei doi consumatori nu împart o cale de query, iar rularea lor ca două pipeline-uri care se întâmplă să citească același log de input e uneori mai curată decât rularea unui singur pipeline care trebuie să satisfacă ambele contracte.

Sisteme legacy unde stratul batch precedă streaming-ul. O echipă a rulat un batch Spark nocturn timp de zece ani pe același log de input și adaugă un pipeline streaming pentru cazurile real-time care au apărut între timp. Înlocuirea pipeline-ului batch e o migrare de mai multe trimestre cu risc real. Alegerea pragmatică e să-l lași pe loc și să rulezi streaming alături. Asta e Lambda prin acumulare, nu prin design, și e ok, atât timp cât cineva le ține pe cele două consistente.

Echipe destul de mari încât să încadreze ambele pipeline-uri. La scară suficientă, echipe dedicate de streaming și batch cu produse diferite și contracte de latență diferite pot opera două baze de cod fără ca durerea sincronizării să domine, pentru că cele două baze de cod nu calculează de fapt același lucru. Calculează lucruri diferite care se întâmplă să împartă un nume.

În afara acestor cazuri, suprasarcina Lambda e rar justificată.

Sfatul pentru 2026

Default Kappa. Folosește un singur procesor de stream atât pentru coada live, cât și pentru replay-ul istoric. Susține-l cu Kafka pentru replay pe termen scurt și un tabel lakehouse pentru replay pe termen lung. Exprimă-ți agregările o singură dată. Rulează-le pe o singură bază de cod, deployată o singură dată, deținută de o singură echipă.

Dacă te trezești că întinzi mâna spre Lambda, articulează de ce. E răspunsul streaming cu adevărat un calcul diferit de răspunsul batch, nu doar o versiune mai rapidă și mai puțin precisă? Sunt cei doi consumatori destul de diferiți încât o singură bază de cod care îi servește pe amândoi să fie o potrivire arhitecturală mai proastă decât două baze de cod servind-l pe fiecare? Ai echipa să operezi două pipeline-uri sau adaugi un al doilea pipeline la o echipă deja întinsă pe primul?

Dacă răspunsul la acele întrebări e da, Lambda e ok. Dacă răspunsul e „așa am făcut-o mereu” sau „pipeline-ul streaming nu mi se pare încă demn de încredere”, investește în pipeline-ul streaming până devine, în loc să construiești un al doilea pipeline al cărui cost de mentenanță va supraviețui deficitului de încredere.

Cadrul Modulului 5, „aceleași pattern-uri la fiecare scară”, se aplică și aici. O echipă mică ce rulează un singur job Flink pe un singur topic Kafka și un lakehouse de cinci tabele face Kappa. O platformă la scară de petabyte care rulează sute de job-uri Flink pe mii de topic-uri și zeci de mii de tabele lakehouse face același lucru, doar mai mare. Forma arhitecturală e identică. Lecția 48 trece printr-o astfel de formă la scara Uber și arată aceleași primitive făcând aceeași treabă.

Citări și lecturi suplimentare

  • Nathan Marz și James Warren, „Big Data: Principles and Best Practices of Scalable Real-Time Data Systems” (Manning, 2015). Cartea care a formalizat arhitectura Lambda, cu motivația originală și designul strat cu strat.
  • Jay Kreps, „Questioning the Lambda Architecture”, O’Reilly Radar, 2014, https://www.oreilly.com/radar/questioning-the-lambda-architecture/ (consultat 2026-05-01). Postarea care a introdus Kappa ca alternativă și a făcut argumentul pentru un singur pipeline.
  • „Designing Data-Intensive Applications” (Martin Kleppmann, O’Reilly, 2017), capitolele 11 și 12. Referința standard pentru procesarea de stream și relația dintre batch și streaming.
  • Documentația Apache Flink, https://flink.apache.org/ (consultat 2026-05-01). Motorul de streaming care face Kappa practic la scară, cu management de stare și semantică exactly-once care se potrivesc nevoilor arhitecturale.
Caută