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

Backfilling și replay

Momentul în care descoperi un bug vechi de șase luni și trebuie să re-rulezi fiecare zi de atunci. Tiparele care fac backfill-urile rutină în loc de terifiante.

Imaginează-ți ședința. Finance reconciliază un număr trimestrial cu o sursă externă și cele două nu se potrivesc. Investigația duce decalajul la o transformare din pipeline-ul tău zilnic de venituri care a fost greșită șase luni, încă de la o refactorizare pe care nimeni n-a semnalat-o. Fiecare tabel downstream construit peste transformarea aia e greșit de șase luni. Dashboard-uri, rapoarte de executiv, date de antrenament ML, numerele pe care board-ul le-a văzut trimestrul trecut.

Acum întrebarea aterizează la tine. Cum facem să fie corect.

Asta e problema backfill-ului, iar fiecare sistem batch peste o dimensiune banală o întâlnește în cele din urmă. Lecția precedentă a fost despre idempotență, care e ce face backfill-ul posibil în primul rând. Lecția asta e despre tiparele care transformă un backfill dintr-o catastrofă de mai multe săptămâni într-o după-amiază de marți.

De ce „doar re-rulează job-ul” pentru șase luni e greu

Planul naiv e „re-rulează job-ul zilnic pentru fiecare din ultimele 180 de zile, în ordine, și tabelele vor fi corecte”. Planul naiv nu supraviețuiește contactului cu realitatea, din patru motive distincte.

Costul clusterului și timpul real. Un job zilnic care durează patru ore are nevoie de 720 de ore de compute pentru a rula 180 de zile serial. Asta e o lună de timp real pe un cluster care trebuie să ruleze și job-ul de la noapte. Dacă paralelizezi, ai nevoie de 180 de ori capacitatea clusterului pe durată. Oricare dimensiune doare. Backfill-ul e un workload peste workload-ul de regim, iar clusterul trebuie să le absoarbă pe ambele.

Conservarea datelor sursă. Transformarea citește input-uri. Sunt input-urile alea încă acolo? Dacă sursa e o bază de date tranzacțională care purjează rândurile vechi după 90 de zile, input-urile de acum șase luni au dispărut, iar re-rularea pe datele sursă din prezent nu reproduce output-ul istoric. Dacă sursa e un log arhivat pe object storage, ești bine. Dacă sursa e „snapshot-ul de ieri al unui tabel care e suprascris zilnic”, nu ești.

Idempotența. Backfill-ul rescrie partiții care au deja date. Un job care face append duplichează la re-rulare; un job care face partition-overwrite e în siguranță. Lecția 38 a acoperit asta în detaliu. Relevanța aici e că un job care nu e idempotent nu poate fi backfilled fără intervenție manuală, iar un job „mai mult sau mai puțin idempotent” e o capcană pentru că modurile de eșec apar doar în condițiile neobișnuite ale unui backfill.

Impactul downstream. Tabelul corectat e citit de dashboard-uri, pipeline-uri ML, exporturi, autorități de reglementare. Unii consumeri fac caching; alții sunt job-uri batch care vor vedea date noi apărând în partiții istorice și vor deveni confuzi; alții vor folosi în tăcere numerele vechi (încă greșite) pentru că ultimul refresh s-a întâmplat înainte de backfill. Raza de acțiune a „rescriu șase luni dintr-un tabel” e mai mare decât tabelul.

Un backfill, cu alte cuvinte, nu e o re-execuție a unui job. E un mic proiect de migrare, cu un plan, o poveste de rollback și un fir de comunicare.

Tiparele care fac backfill-urile rutină

Șase tipare, fiecare util independent, care se compun când le ai pe toate.

Păstrează datele brute pentru totdeauna, sau aproape. Object storage în 2026 costă aproximativ 0,02 USD per GB pe lună pentru cold storage. Un petabyte de evenimente brute pentru un an e de ordinul a un sfert de milion de dolari anual. Sunt bani reali, și sunt mult mai puțin decât costul de inginerie al imposibilității de a face backfill când ai nevoie. Arhitectura Lambda, articulată de Nathan Marz în jurul anului 2011, a făcut asta explicit: un strat batch care recalculează rezultatele derivate dintr-un log brut imuabil la cerere. Păstrează datele brute, în forma lor brută, în storage ieftin imuabil, cu o politică de retenție suficient de lungă pentru a face backfill din ea. Orice ai derivat a fost calculat din ceva; păstrează acel ceva.

Job-uri idempotente. Fiecare job batch ar trebui să fie sigur de re-rulat. Lecția 38 a dat tiparele: semantica partition-overwrite, output-uri deterministe, fără mutație pe loc. Un job care e idempotent pentru rularea normală de la noapte e idempotent pentru un backfill al oricărei zile istorice. Un job care nu e idempotent forțează un pas de curățare manuală de fiecare dată când faci backfill, ceea ce înseamnă că în practică nu faci backfill, scrii scuze.

Backfill-uri paralelizabile. Cele 180 de zile ale backfill-ului sunt independente. Transformarea pentru 2025-09-15 nu are nevoie ca 2025-09-14 să fi rulat. (Dacă are, job-ul tău are stare ascunsă peste zile; rezolvă asta întâi.) Când zilele sunt independente, backfill-ul le rulează în paralel, mărginit doar de capacitatea clusterului și de limitele de rată ale sursei. Un backfill de 180 de zile pe un cluster dimensionat pentru 30 de job-uri paralele se termină în 6 valuri batch, nu 180.

Output-uri versionate. Nu scrie datele corectate peste cele greșite pe loc. Scrie într-o versiune nouă a tabelului. Unele echipe folosesc un nume cu sufix de dată (fct_revenue_v2_20260123), unele un snapshot Iceberg sau Delta, unele un swap la nivel de partiție. Forma e aceeași: tabelul nou stă lângă cel vechi, validezi și faci cutover atomic. Versiunea veche rămâne pe o fereastră de retenție în caz că trebuie să faci rollback.

Resetări de watermark. Majoritatea pipeline-urilor batch rulează pe un model high-water-mark: job-ul își amintește ultima partiție pe care a procesat-o. Pentru a face backfill, resetezi watermark-ul pentru fereastra afectată. Sună trivial dar nu e, pentru că watermark-ul e adesea răspândit pe mai multe sisteme (orchestratorul, starea proprie a job-ului, watermark-urile consumerilor downstream). Un plan de backfill are nevoie de o listă de watermark-uri de resetat și o listă de avansat din nou la final.

Pune în pauză sau proiectează pentru toleranța downstream. Ori pui în pauză job-urile downstream care citesc tabelul pe care-l rescrii, ori le proiectezi să tolereze update-uri pe partiții istorice. Pauza e operațional simplă și politic costisitoare. Toleranța e arhitectural mai grea și mai durabilă: job-ul downstream e el însuși idempotent, prinde datele noi la următoarea trecere și raportează propria completitudine astfel încât consumerii să știe când să aibă încredere în răspuns. Dacă te aștepți să faci backfill regulat, construiește pentru toleranță.

Replay, vărul din streaming

Replay e versiunea sistem-de-streaming a backfill-ului. Vocabular care merită introdus aici, deși modulul 6 e cel unde trăiește.

Într-un sistem de streaming, input-ul e un log de evenimente (Kafka, Kinesis, Pulsar) cu o fereastră de retenție, iar consumerii citesc de la offset-uri din acel log. A face replay înseamnă a reseta offset-ul unui consumer la o poziție istorică și a reprocesa de acolo. Dacă retenția e șapte zile și bug-ul tău a fost introdus acum șase zile, replay-ul e ieftin: derulează consumer-ul, lasă-l să reproceseze, iar output-ul corectat apare în timp real. Dacă bug-ul a fost introdus acum un an, replay-ul doar din log e imposibil; ai nevoie de stratul batch pentru a recalcula din arhiva brută durabilă.

Kafka face replay-ul structural ieftin pentru că log-ul e storage-ul durabil, consumerii sunt doar bookmark-uri, iar derularea unui bookmark e un singur apel de API. Arhitectura Lambda originală trata batch-ul și stream-ul ca două căi spre același răspuns; varianta modernă (uneori numită Kappa) le colapsează pe cele două dând stratului de streaming suficientă retenție încât replay-ul să acopere majoritatea cazurilor pentru care era nevoie de batch. Modulul 6 parcurge compromisurile.

Punctul relevant aici: replay e backfill într-un vocabular diferit. Tiparele se transferă. Păstrează evenimentele brute. Fă consumerii idempotenți. Versionează output-urile. Coordonează downstream.

Parcurgerea unui plan real

Exemplu concret. Raportul zilnic de venituri are un bug în join-ul cu cursul FX. Pentru 90 de zile, tranzacțiile în alte monede decât USD au fost convertite folosind cursul greșit (cursul pentru ziua precedentă, nu cursul pentru ziua tranzacției). Tabelele downstream sunt fct_daily_revenue (90 de partiții zilnice afectate) și trei rapoarte construite peste el (dashboard executiv, export de reconciliere finance, pachet board lunar).

Planul, în ordinea în care se execută:

  1. Îngheață rapoartele afectate. Notifică consumerii (finance, exec ops, board prep) că dashboard-ul va citi „de ieri” pentru următoarele 48 de ore în timp ce o corecție e în zbor. Adaugă un banner pe dashboard.
  2. Verifică integritatea datelor sursă. Tabelul de cursuri FX merge înapoi doi ani, deci cursurile istorice sunt disponibile. Tabelul de tranzacții e partiționat după zi și reținut trei ani, deci input-urile pentru cele 90 de zile afectate sunt încă acolo. Bine.
  3. Resetează watermark-ul pentru fct_daily_revenue. Orchestratorul stochează high-water-mark-ul într-un tabel de metadate; îl setăm înapoi cu 90 de zile pentru job-ul afectat.
  4. Backfill într-un tabel versionat. Job-ul scrie în fct_daily_revenue__backfill_20260123 în loc de fct_daily_revenue. Clusterul e dimensionat să ruleze 15 zile în paralel, deci backfill-ul de 90 de zile durează 6 valuri batch, aproximativ 6 ore de timp real.
  5. Validează. Un job de reconciliere compară totalurile între tabelul vechi și cel nou pentru fiecare zi afectată. Majoritatea diferențelor ar trebui să fie mici (corecția cursului FX), iar semnul și magnitudinea ar trebui să se potrivească cu ce ne așteptăm de la analiza bugului. Tragem un eșantion aleator de tranzacții și verificăm cursurile corectate manual față de sursa FX.
  6. Cutover. O singură operație atomică schimbă partițiile afectate ale fct_daily_revenue să indice spre datele noi. În Iceberg sau Delta ăsta e un swap de metadate, nu o mutare de date. Datele vechi rămân pe disc pentru fereastra de rollback.
  7. Declanșează rebuild-urile downstream. Rapoartele citesc din tabelul corectat; sunt ele însele idempotente și se reconstruiesc pentru fereastra afectată.
  8. Comunică. O notă pentru finance, exec ops și echipa de board prep care rezumă problema, soluția, perioada afectată și magnitudinea corecției. Fără pasul ăsta, munca de inginerie a fost tehnic corectă și organizațional invizibilă, ceea ce e același lucru cu a n-o fi făcut.
  9. Dezafectează. După o fereastră de retenție (să zicem, două săptămâni) tabelul vechi __backfill_ e eliminat.

Diagramă de creat: o diagramă de flux orizontală care arată cei nouă pași de mai sus ca un swim lane. Banda de sus: acțiuni de data engineering (freeze, reset watermark, backfill, cutover, decommission). Banda de jos: acțiuni de comunicare (notifică consumerii, validează cu finance, trimite rezumatul). Săgețile între benzi arată punctele de coordonare.

flowchart LR
    A[Freeze reports] --> B[Verify source data]
    B --> C[Reset watermark]
    C --> D[Backfill to versioned table]
    D --> E[Validate against source]
    E --> F[Atomic cutover]
    F --> G[Rebuild downstream]
    G --> H[Communicate]
    H --> I[Decommission old version]

Planul nu e exotic. E aceeași carte de joc spre care converge fiecare echipă care rulează infrastructură batch serioasă, pentru că alternativele (rescrieri pe loc, fix-uri SQL manuale, „o să fim mai atenți data viitoare”) au fost toate încercate și au eșuat toate.

Cost-beneficiul lui „stochează brut pentru totdeauna”

Cea mai consecventă alegere arhitecturală pentru backfilling e dacă păstrezi input-urile brute. Echipele care pierd lupta asta o fac din motive previzibile: storage-ul pare scump în abstract, compliance împinge spre retenție mai scurtă, iar nimeni nu vrea să apere un an de date brute de evenimente la review-ul de facturi.

Contraargumentul e bug-ul pe care nu l-ai descoperit încă. Fiecare echipă pe care am văzut-o operând infrastructură batch serioasă a avut cel puțin un moment în care diferența dintre „putem repara asta” și „trebuie să scriem o scuză autorității de reglementare” a fost dacă datele brute mai erau acolo. Object storage la prețurile din 2026 e cea mai ieftină asigurare pe care platforma de date o vinde.

Excepțiile legale și de privacy sunt reale și au nevoie de un plan. PII-ul are obligații de retenție mai scurte decât cele pe care le-ai vrea pentru backfilling; răspunsul e să păstrezi log-ul de evenimente brut cu câmpurile PII tokenizate sau marcate ca tombstone după fereastra legală de retenție, în timp ce păstrezi părțile non-PII mai mult timp. Mai multă muncă decât „păstrează totul pentru totdeauna”, dar conservă capacitatea de backfill pentru majoritatea datelor în timp ce respectă regulile pentru părțile sensibile.

Modulul 5 se închide aici, aproape

Lecția următoare e studiul de caz care pune tot Modulul 5 în mișcare la scară de petabyte. Netflix rulează una dintre cele mai mari și mai publice platforme de date batch din industrie, iar citirea arhitecturii lor în lumina abstracțiunilor acestui modul e cel mai curat mod de a vedea cum se potrivesc piesele când mizele sunt reale și datele sunt enorme. După aia, Modulul 6 începe pe streaming.

Citate ș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 articulat arhitectura Lambda și principiul „păstrează log-ul brut pentru totdeauna”.
  • Jay Kreps, “Questioning the Lambda Architecture”, O’Reilly Radar, 2014, https://www.oreilly.com/radar/questioning-the-lambda-architecture/ (consultat 2026-05-01). Replica de tip arhitectură Kappa care argumentează că un strat de stream cu retenție lungă subsumează stratul batch.
  • Apache Iceberg documentation, “Maintenance and snapshot expiration”, https://iceberg.apache.org/docs/latest/maintenance/ (consultat 2026-05-01). Mecanica tabelelor versionate și a swap-urilor atomice de snapshot care fac posibile backfill-urile sigure.
  • “Designing Data-Intensive Applications” (Martin Kleppmann, O’Reilly, 2017), capitolul 10. Referința standard pentru procesare batch, idempotență și realitățile operaționale pe care lecția asta le rezumă.
Caută