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

CD pentru date: pattern-uri de deployment pentru batch și streaming

Blue-green, canary, dark launch. De ce job-urile de streaming au nevoie de pattern-uri de deploy diferite față de serviciile web și cum se deploy-uiesc job-urile de batch prin propriul lor schedule.

Continuous delivery pentru servicii web e un teritoriu bine bătătorit. Lansezi o versiune nouă de API, te uiți la rate-urile de erori și la latențe, și dacă ceva pare în neregulă faci rollback. Toată industria a convergent pe un set mic de pattern-uri: blue-green, canary, rolling deploys, feature flags. S-au scris cărți despre asta, comunitatea SRE s-a maturizat în jurul lor, iar majoritatea platformelor moderne (Kubernetes, ECS, Cloud Run, Lambda) livrează primitivele direct în cutie.

Pipeline-urile de date sunt diferite. Nu pentru că principiile s-ar schimba, ci pentru că forma workload-ului se schimbă. Un serviciu web se ocupă de milioane de cereri scurte și independente; un deploy prost produce un vârf scurt de 5xx-uri pe care îl poți da înapoi în câteva secunde. Un pipeline fie produce felii mari de date derivate pe un schedule, fie rulează continuu și ține state. Un deploy prost poate scrie rânduri greșite în warehouse ore în șir înainte ca cineva să observe, sau poate corupe state-ul unui job de streaming care rulează de săptămâni. Raza de explozie e mai mare; disciplina trebuie să fie pe măsură.

Lecția asta trece prin pattern-urile de deployment și cum se mapează fiecare pe batch și streaming. Ideea principală e că pattern-urile sunt aceleași, dar mecanica diferă suficient cât să nu poți copia pur și simplu manualul de servicii web.

Cum arată CD pentru un serviciu web

Ca să stabilim contrastul, iată flow-ul standard pentru un serviciu web. Faci merge pe main, CI construiește o imagine, imaginea e împinsă într-un registry, sistemul de deploy o lansează. Rolling deploy înseamnă că pod-urile noi pornesc cu imaginea nouă în timp ce cele vechi se golesc; în orice moment ceva trafic ajunge la cele vechi, ceva la cele noi. Canary înseamnă că rutezi un procent mic (5%, 10%) către versiunea nouă și te uiți la metrici înainte să promovezi. Blue-green înseamnă că ridici un mediu complet nou cu versiunea nouă, faci smoke test și schimbi traficul printr-un switch de load balancer.

Proprietatea unificatoare: o cerere e o unitate mică și izolată. Dacă versiunea nouă e proastă, încetezi să-i mai trimiți cereri și comportamentul prost se oprește. Nimic nu persistă între deploy-uri în afară de baza de date, pe care o împart ambele versiuni.

Ultima propoziție e exact locul unde lumea datelor începe să divergă. Pentru pipeline-uri, unitatea de deploy e un job, nu o cerere. Job-ul are state. Job-ul scrie în output-uri de care depind alte job-uri și oameni. Iar lucrurile pe care le poți face rollback variază în funcție de dacă job-ul e batch sau streaming.

Pipeline-uri batch: deploy prin schedule

Batch e cel mai blând dintre cele două. Un job de batch rulează pe un schedule (orar, zilnic, când aterizează datele de upstream). Când deploy-uiești cod nou, următoarea rulare programată îl preia. Nu există un proces lung în zbor cu care să te lupți.

Flow-ul de deploy implicit pentru un pipeline de batch arată cam așa:

  1. Faci merge unei modificări de cod în repo.
  2. CI construiește artefactul (o imagine Docker, un wheel, un JAR) și-l încarcă acolo de unde-l ia orchestratorul.
  3. Următoarea oară când job-ul e declanșat (următoarea rulare de 02:00, următorul trigger manual, următorul sensor care se aprinde), folosește codul nou.

Rollback-ul e simetric: faci revert la commit, redeploy-uiești artefactul, următoarea rulare se întoarce la comportamentul vechi. Dacă vrei să fii paranoic, oprește orice job care rulează în acel moment înainte de rollback, ca să nu termine cu cod vechi peste un output deja parțial scris cu cod nou.

Idempotența, din lecția 38, e cea care face curățenia. Dacă job-ul poate fi reluat în siguranță, poți face rollback și replay pe datele afectate fără probleme. Dacă nu poate, trebuie să cauți manual orice output greșit a scris versiunea proastă și să-l curăți de mână.

Capcana sunt migrările de schemă. Dacă codul nou se așteaptă la o coloană nouă pe care codul vechi nu o scria, rollback-ul codului fără rollback-ul schemei strică citirile. Mai rău, dacă ai făcut rollback pentru că schema nouă s-a dovedit greșită, poți avea output-uri în forma nouă pe care codul vechi nu le poate citi deloc.

Disciplina care face asta gestionabil e aceeași ca pentru serviciile online: păstrează modificările de schemă backwards-compatible. Adăugarea unei coloane e sigură; ștergerea sau redenumirea unei coloane nu e. Deploy-urile care au nevoie de o modificare de schemă ar trebui împărțite în două deploy-uri: mai întâi migrarea aditivă, apoi codul care o folosește. Dacă vreodată ai nevoie de modificarea distructivă (drop la o coloană, restrângerea unui tip), o faci ca pas separat și explicit, mult după ce a dispărut codul care depindea de forma veche.

Concluzia: deploy-urile de batch seamănă cu deploy-urile web dar încetinite la cadența schedule-ului. Pattern-ul care contează cel mai mult e să ții migrările și release-urile de cod independente.

Pipeline-uri de streaming: problema job-urilor cu durată lungă

Streaming-ul e locul unde povestea de deploy devine interesantă. Un job de Flink sau Spark Structured Streaming e un proces cu durată lungă. Rulează de săptămâni, ținând state (windows, joins, tabele de deduplicare, state-ul unui model de machine learning) care a luat un timp non-trivial să se acumuleze. Nu poți pur și simplu să lansezi cod nou cum ai face pentru un API stateless.

Pattern-ul standard pentru procesoarele de stream cu state e savepoint-ul. Flink, exemplul canonic, îți permite să declanșezi un savepoint în orice moment: un snapshot al întregului state al job-ului scris pe storage durabil. Flow-ul de deploy devine:

  1. Construiești codul nou.
  2. Declanșezi un savepoint pe job-ul care rulează.
  3. Oprești job-ul după ce savepoint-ul s-a terminat.
  4. Pornești job-ul nou din savepoint, folosind codul nou.

Job-ul nou pornește exact de unde a rămas cel vechi, cu state-ul intact. Offset-urile Kafka fac parte din savepoint, deci consumă același mesaj următor pe care l-ar fi consumat și job-ul vechi. Niciun reprocesare, niciun gap.

Asta funcționează atâta timp cât codul nou poate citi state-ul vechi. Dacă ai redenumit un câmp de state, i-ai schimbat tipul sau ai adăugat un operator nou care nu exista înainte, ai nevoie de un pas de migrare a state-ului. Flink suportă schema evolution pentru state dacă te ții de serializatori compatibili (POJO, Avro), dar din momentul în care începi să stochezi obiecte Java arbitrare fără schemă, ești pe cont propriu.

Job-urile de streaming stateless (un filtru simplu fără state, un enrichment fără state) sunt mult mai ușoare. Nu există state de migrat, deci poți face un rolling restart: pornești instanța nouă, o lași să prindă din urmă, o omori pe cea veche. Același pattern ca la serviciile web.

Diagrama de mai jos arată contrastul.

flowchart TB
    subgraph BATCH["Batch deploy"]
        B1[Merge code] --> B2[CI builds artefact]
        B2 --> B3[Next scheduled run]
        B3 --> B4[Uses new code]
    end
    subgraph STREAM["Streaming deploy with savepoint"]
        S1[Merge code] --> S2[CI builds artefact]
        S2 --> S3[Trigger savepoint on running job]
        S3 --> S4[Stop old job]
        S4 --> S5[Start new job from savepoint]
    end

Blue-green, canary, dark launch pentru date

Pattern-urile de servicii web se traduc pe pipeline-uri de date, cu ajustări.

Blue-green pentru date. Menții două pipeline-uri complete, blue și green. Blue e ce citesc consumatorii. Deploy-uiești versiunea nouă ca green, o rulezi în paralel cu blue și, odată ce ai încredere, schimbi pointer-ul consumatorului (un view, un alias de tabel, configul de input al unui job de downstream) pe green. Blue rămâne ca țintă de rollback.

Costul e real: rulezi două copii ale pipeline-ului și stochezi două copii ale output-ului. Pentru un pipeline mic e ok. Pentru un lakehouse de petabyte e prohibitiv. Pattern-ul e cel mai util pe căile critice unde nu poți tolera un incident cu output greșit, și accepți costul dublu drept asigurare.

Canary pentru date. Deploy-uiești versiunea nouă pe o felie din muncă. Pentru un job de streaming alimentat de Kafka, asta poate fi un subset de partiții sau un subset de topic-uri. Pentru un job de batch partiționat după tenant, un singur tenant. Te uiți la output-uri, le compari cu ce ar fi produs versiunea veche, promovezi pe rest dacă totul pare corect.

Partea grea e comparația. Ai nevoie ca ambele versiuni să producă output-uri comparabile pe care să le poți face diff, ceea ce înseamnă de obicei să scrii output-ul versiunii noi într-un side table și să calculezi diff-uri pe un sample. Ieftin dacă setezi tooling-ul o singură dată; obositor dacă o iei de la zero de fiecare dată.

Dark launches. Rulezi versiunea nouă în shadow mode: procesează aceleași input-uri ca versiunea veche, dar scrie într-un output separat. Nimic de downstream nu citește de acolo. Calculezi diff-uri față de output-ul vechi până ai încredere în versiunea nouă, apoi schimbi cutover-ul. Ăsta e pattern-ul canary fără risc în producție, pentru că output-ul nou nu e încă consumat.

Pentru un job de streaming cu state, dark launch-ul e scump pentru că rulezi două copii ale pipeline-ului care țin două copii ale state-ului. Pentru un job de batch, dark launch-ul e ieftin: versiunea nouă rulează doar o dată pe zi într-un tabel diferit.

Feature flags pentru pipeline-uri. Mai puțin frecvent decât în servicii web, dar util. Pipeline-ul citește un flag la runtime și își ramifică logica pe el. Poți întoarce flag-ul pentru un subset de partiții, tenanți sau medii. Avantajul e că o întoarcere de flag nu cere redeploy; dezavantajul e că codul pipeline-ului cară ramuri moarte și matricea de testare crește.

Problema „datele rămân pe veci”

Un deploy prost într-un serviciu web cauzează un vârf scurt de 5xx-uri. Utilizatorii văd erori câteva minute, faci rollback și viața merge mai departe. Efectul prost e mărginit în timp și ca scop.

Un deploy prost într-un pipeline scrie date greșite. Datele greșite stau într-un tabel pe care îl citesc pipeline-uri de downstream, îl afișează dashboard-uri și pe care antrenează modele de machine learning. Până observă cineva că numărul de utilizatori de ieri e diferit cu un factor de doi, datele proaste au fost deja consumate de o duzină de sisteme de downstream și s-au propagat în câteva dataset-uri derivate. Rollback-ul deploy-ului oprește sângerarea, dar nu anulează paguba.

De aceea disciplina în jurul CD pentru date trebuie să fie mai strânsă decât pentru servicii online, chiar dacă consecințele par mai puțin imediate. Un 500 web e zgomotos și vizibil; un rând greșit e tăcut și contagios. Pattern-urile de deployment care par exagerate (dark launches, comparație side-by-side, blue-green pe căile critice) sunt cele care se plătesc singure prima dată când prind un deploy prost înainte ca consumatorii de downstream să-l vadă.

Ce înseamnă asta în practică

Pentru o echipă mică care abia începe, flow-ul realist e:

  1. Job-urile de batch se deploy-uiesc prin orchestrator: merge pe main, CI livrează o imagine nouă, următoarea rulare programată o preia. Idempotența face rollback-ul sigur.
  2. Job-urile de streaming se deploy-uiesc prin savepoint-and-restart pentru muncă cu state, rolling restart pentru cele stateless. Disciplina savepoint-ului contează mai mult decât orice funcționalitate de framework.
  3. Migrările de schemă sunt un pas separat de deploy, aditive în mod implicit, niciodată cuplate cu un release de cod.
  4. Cele mai critice pipeline-uri (venit, raportare reglementară, orice e expus utilizatorului în produs) câștigă fie dark launches, fie blue-green. Cele mai puțin importante nu.
  5. Tot ce poate fi făcut idempotent ar trebui să fie. Idempotența e proprietatea care permite oricărui alt pattern de deployment să funcționeze.

Restul e tooling: orchestrator, CI, registry, observability, alerting pe diff-uri de output. Lecția 53 acoperă piesa de infrastructure-as-code, care e stratul ce face orchestratorul și pipeline-urile reproductibile. Lecția 54 acoperă piesa de container, care e unitatea sub care se livrează majoritatea job-urilor de date moderne.

Citații

  • „What Is Continuous Delivery?” (https://continuousdelivery.com/, consultat 2026-05-01).
  • Documentația Apache Flink, „Savepoints” (https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/savepoints/, consultat 2026-05-01).
  • „Schema Evolution and State Migration” în documentația Flink (https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/fault-tolerance/schema_evolution/, consultat 2026-05-01).
  • Google SRE Book, capitolul despre release engineering (https://sre.google/sre-book/release-engineering/, consultat 2026-05-01).
Caută