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

Orchestrare orientată pe asset-uri (lecția Dagster)

Modelarea tabelelor și fișierelor ca obiecte de prim rang. De ce această abordare se amortizează la scară și ce schimbă în felul în care echipele gândesc pipeline-urile.

Lecția anterioară a trecut în revistă cele patru orchestratoare mari și a notat că unul dintre ele (Dagster) face o alegere diferită despre la ce servește un orchestrator. Acea alegere merită propria lecție, pentru că este cea mai interesantă deplasare arhitecturală în orchestrarea de date de la Airflow încoace. Deplasarea are un nume (orchestrare orientată pe asset-uri), o unealtă concretă care o demonstrează (Dagster, din ce în ce mai mult Prefect, parțial și alte unelte) și consecințe care se manifestă în felul în care echipele vorbesc despre pipeline-urile lor.

Lecția aceasta este teoria. Explică modelul tradițional orientat pe task-uri, alternativa orientată pe asset-uri, ce se schimbă când adopți modelul orientat pe asset-uri și unde aterizează costurile. Argumentul nu este că orchestrarea orientată pe asset-uri e universal superioară. Este că la scară și complexitate suficiente, modelul orientat pe task-uri începe să pună întrebările greșite, iar modelul orientat pe asset-uri începe să răspundă la cele potrivite.

Modelul orientat pe task-uri

Airflow a definit ortodoxia. Un pipeline este un graf orientat aciclic de task-uri. Fiecare task este o unitate de muncă: o funcție Python, un query SQL, o comandă shell, o execuție de container. Orchestratorul programează task-urile, urmărește starea lor și impune dependențele între ele. Datele pe care le produc acele task-uri sunt implicite. Orchestratorul nu știe ce a scris task-ul, unde a scris sau cine în aval depinde de el.

Un DAG Airflow tipic ar putea arăta așa:

with DAG("customer_pipeline", schedule="@daily") as dag:
    extract = PythonOperator(task_id="extract_orders", ...)
    transform = PythonOperator(task_id="transform_orders", ...)
    load = PythonOperator(task_id="load_to_warehouse", ...)
    customer_metrics = PythonOperator(task_id="customer_metrics", ...)
    extract >> transform >> load >> customer_metrics

Orchestratorul știe: există patru task-uri, iată ordinea lor, iată când să ruleze DAG-ul. Orchestratorul nu știe: extract produce un tabel staging de comenzi, transform îl citește și scrie un tabel curățat, load scrie tabelul fact din warehouse, customer_metrics citește tabelul fact și scrie un tabel de metrici zilnice. Toate aceste date trăiesc în afara conștiinței orchestratorului, în path-uri S3 și tabele de warehouse care există doar pentru că task-urile s-au întâmplat să scrie în ele.

Asta e în regulă când sistemul e mic. Patru task-uri îți încap în cap; datele și orchestrarea se aliniază în mintea inginerilor care le-au construit. Modelul se rupe la scară, în trei locuri deodată.

Întrebarea de dependență devine grea. Când ai sute de DAG-uri care produc mii de tabele, întrebarea „ce depinde de tabelul customer_clv?” cere citirea codului task-urilor sau grep prin codebase. Orchestratorul nu poate răspunde la întrebare, pentru că nu știe despre tabele; știe doar despre task-uri.

Întrebarea de freshness devine grea. Întrebarea „este tabelul customer_clv proaspăt?” cere să știi care task l-a scris ultima oară și când, apoi să traduci asta în „tabelul are X ore vechime”. Orchestratorul îți poate spune când a reușit ultima oară task-ul customer_clv. Nu îți poate spune când a fost actualizat ultima oară tabelul, pentru că scrierile sunt un efect secundar pe care orchestratorul nu l-a observat.

Întrebarea de retry devine grosieră. Când un task din aval eșuează, întrebarea naturală este „ce asset-uri trebuie regenerate?” Orchestratorul orientat pe task-uri nu poate răspunde la asta, așa că oferă fie „rerulează acest task”, fie „rerulează tot DAG-ul”. Niciuna nu e precisă.

Aceste dureri sunt tolerabile la scară mică și din ce în ce mai intolerabile pe măsură ce platforma crește.

Modelul orientat pe asset-uri

Modelul orientat pe asset-uri inversează relația. În loc să declari task-uri și să lași datele să cadă ca un efect secundar, declari asset-uri, care sunt lucrurile pe care le produce pipeline-ul (tabele, fișiere, artefacte de model), iar orchestratorul derivă munca din graful de asset-uri.

Un echivalent Dagster al pipeline-ului de mai sus:

from dagster import asset

@asset
def orders_raw():
    return extract_from_source()

@asset
def orders_cleaned(orders_raw):
    return clean(orders_raw)

@asset
def orders_warehouse(orders_cleaned):
    return load_to_warehouse(orders_cleaned)

@asset
def customer_metrics(orders_warehouse):
    return compute_metrics(orders_warehouse)

Fiecare funcție declară un asset. Argumentele funcției declară dependențele (customer_metrics depinde de orders_warehouse, care depinde de orders_cleaned, și așa mai departe). Orchestratorul citește acest graf și știe: există patru asset-uri, asta este structura lor de dependență, iată cum arată fiecare, iată când a fost materializat ultima oară fiecare.

Schimbarea de încadrare este subtilă și consecventă. Echipa nu mai vorbește despre „task-ul customer_metrics” și începe să vorbească despre „asset-ul customer_metrics”. Orchestratorul nu mai e un job runner și începe să fie un catalog de date cu execuție atașată. Întrebările pe care le poți pune se schimbă. „Când a fost actualizat ultima oară customer_metrics?” devine acum un query de prim rang. „Ce depinde de orders_warehouse?” este un query de prim rang. „Customer_metrics trebuie să fie proaspăt la fiecare șase ore; dă-ți seama ce să rulezi ca să-l ții proaspăt” este o primitivă de scheduling de prim rang (Dagster numește asta freshness policy).

Un exemplu concret

Ia un pipeline de date despre clienți care produce, în ordine: un tabel raw de evenimente din stream-ul CDC, un tabel de evenimente sesionizate agregate la granularitate de sesiune, customer features pentru echipa ML și un artefact de model customer lifetime value.

Graful de asset-uri:

flowchart LR
    RE[raw_events] --> SE[sessionized_events]
    SE --> CF[customer_features]
    CF --> CLV[customer_clv_model]
    SE --> DM[daily_metrics_mart]
    CF --> CS[customer_segments]

Diagramă de creat: o versiune polisată a grafului de asset-uri de mai sus, organizată în trei straturi (raw, intermediate, mart). Punctul vizual este că graful este un graf de date, nu un graf de joburi. Fiecare cutie este un lucru care există în storage; fiecare săgeată este o dependență care îți permite să trasezi de unde au venit datele.

Ce știe acum orchestratorul, ce nu știa orchestratorul orientat pe task-uri:

  • Graful este un graf de lineage. Dacă raw_events este greșit, orchestratorul poate calcula exact ce asset-uri din aval sunt vechi: sessionized_events, customer_features, customer_clv_model, daily_metrics_mart, customer_segments.
  • Fiecare asset are un timestamp de ultimă materializare. Echipa poate vedea „customer_features a fost actualizat ultima oară acum trei ore” fără să citească logurile task-urilor.
  • Fiecare asset are o freshness policy. „customer_features trebuie să fie proaspăt la fiecare șase ore; dacă nu, ridică o alertă.” Orchestratorul rulează orice asset-uri din amonte sunt necesare pentru a satisface politica, în ordinea dependențelor.
  • Retry-ul este precis. Dacă customer_features a eșuat, orchestratorul știe că customer_clv_model și customer_segments sunt acum vechi și pot fi rematerializate independent de daily_metrics_mart, care nu este afectat.

Nimic din asta nu e teoretic imposibil în Airflow; este însă suprapus pe un model care nu înțelege nativ nimic din toate astea. În Dagster este modelul.

Beneficiile

Patru câștiguri ies din încadrarea orientată pe asset-uri.

Lineage gratuit. Graful de asset-uri ESTE graful de lineage. Lineage-ul într-un sistem orientat pe task-uri este ceva ce generezi parsând SQL sau scriind emițători OpenLineage care traduc evenimentele de task în evenimente de asset. Într-un sistem orientat pe asset-uri, lineage-ul este structura de date pe care orchestratorul deja o menține. UI-ul catalogului îl arată nativ. Întrebările între echipe („cine citește tabelul orders?”) devin un click în loc de un proiect de arheologie de cod.

Retry-uri mai inteligente. Când o eroare sau o schimbare de cod invalidează un asset, orchestratorul poate calcula raza precisă a impactului în aval și rematerializa doar ce e afectat. În sistemele orientate pe task-uri, asta fie nu se întâmplă (rerulezi tot DAG-ul), fie cere intervenție manuală (un inginer își dă seama ce să rerulează). La scară, această distincție este diferența dintre minute și ore de muncă de reparație.

Un model mental mai bun. Echipele vorbesc despre produse de date, nu despre joburi. Un produs de date este asset-ul customer_clv, nu jobul customer_clv_dag care îl rulează. Inginerii noi învață sistemul citind graful de asset-uri, care descrie ce există; în sistemele orientate pe task-uri învață citind fișierele DAG, care descriu ce rulează. Primul este mai scurt și mai util.

Integrarea dbt devine naturală. Modelele dbt sunt asset-uri. Graful orientat al modelelor proiectului dbt este el însuși un graf de asset-uri, cu dependențe declarate prin funcția ref(). Dagster (și din ce în ce mai mult alte unelte) citește proiectul dbt și îl integrează direct în graful de asset-uri: modelele dbt apar alături de asset-urile definite în Python, lineage-ul curge peste graniță, iar orchestratorul și dbt sunt de acord asupra aceluiași vocabular. Într-un sistem orientat pe task-uri, întreaga rulare dbt este un task opac, iar lineage-ul din interiorul dbt este o preocupare separată care trebuie reconstruită.

Costurile

Modelul orientat pe asset-uri nu este gratuit. Se aplică trei costuri reale.

Asumare conceptuală. Inginerii obișnuiți să gândească în task-uri și DAG-uri trebuie să reînvețe modelul. „Ce e acest DAG?” devine „ce e acest asset?” „Când rulează asta?” devine „când trebuie să fie proaspăt și ce îl produce?” Tranziția durează săptămâni de practică pentru o echipă și este o frecare reală. Inginerii juniori care învață orientat pe asset-uri din prima zi nu simt asta; inginerii cu cinci ani de memorie musculară Airflow o simt.

Lock-in pe unealtă. Dagster este unealta cea mai angajată pe asset-uri. Prefect 3.x are suport pentru asset-uri, dar modelul este mai puțin central. Airflow a ronțăit pe la margini cu API-ul Datasets (introdus în 2.4 și extins de atunci), dar este lipit pe un nucleu orientat pe task-uri. Argo nu îl are deloc. Odată ce o echipă se angajează la orchestrare orientată pe asset-uri, schimbarea uneltelor este o migrare reală, nu o schimbare de configurație.

Costul migrării de la sistemele existente. Un magazin cu o sută de DAG-uri Airflow care vrea să se mute la Dagster se înscrie pentru muncă serioasă. DAG-urile nu se traduc mecanic; granițele asset-urilor trebuie regândite, freshness policies proiectate, integrarea dbt configurată. Majoritatea echipelor care fac această mutare o fac incremental, asset cu asset, pe parcursul lunilor. Migrările big-bang rareori funcționează.

Aceste costuri sunt motivul pentru care orchestrarea orientată pe asset-uri nu este implicită pentru toată lumea. Este o alegere și, ca orice alegere arhitecturală, merită făcută deliberat.

De ce contează mai mult la scară

Avantajul modelului orientat pe asset-uri scalează cu dimensiunea sistemului.

Pentru o echipă mică cu o duzină de pipeline-uri și cincizeci de tabele, întrebările de dependență sunt ușor de răspuns citind codebase-ul. Întrebările de freshness sunt ușor de răspuns verificând logurile task-urilor. Întrebările de retry sunt ușor de răspuns rerulând DAG-ul afectat. Încadrarea orientată pe asset-uri este o conveniență drăguță, nu un câștig transformator.

Pentru o echipă de platformă cu o mie de pipeline-uri și zece mii de tabele, întrebările de dependență sunt intractabile manual. „De ce depinde tranzitiv tabelul customer_clv și ce depinde de el?” este genul de întrebare care trebuie răspunsă într-un UI de cineva care nu este autorul pipeline-ului. Întrebările de freshness cer dashboard-uri. Întrebările de retry cer precizie, pentru că rerularea a o mie de DAG-uri pentru că o schimbare în amonte a invalidat câteva asset-uri frunză nu este o strategie rezonabilă.

La acea scară, orchestratorul care știe despre asset-uri rezolvă problema potrivită. Orchestratorul care știe doar despre task-uri rezolvă problema greșită și forțează echipa să adauge lineage, urmărire de freshness și retry-uri precise ca sisteme separate.

Acesta este argumentul arhitectural pentru orchestrarea orientată pe asset-uri: pe măsură ce platforma de date crește, întrebarea care domină este „ce depinde de ce”. Orchestratorul orientat pe asset-uri răspunde la acea întrebare prin construcție. Orchestratorul orientat pe task-uri îi răspunde eventual, prin acumulare de unelte separate pe care le legi împreună.

Încotro se îndreaptă asta

Lecția de aici nu este doar despre Dagster. Încadrarea orientată pe asset-uri este o mișcare, iar alte unelte absorb bucăți din ea. API-ul Datasets de la Airflow, emițătorii de asset-uri ai Prefect, standardul OpenLineage (pe care lecția 59 îl acoperă în contextul observabilității) și convergența mai largă a orchestrării cu cataloagele de date arată toate în aceeași direcție. „Task-ul” a fost întotdeauna un mijloc; „asset-ul” a fost întotdeauna scopul. Uneltele ajung din urmă.

Pentru platformele noi în 2026, modelul orientat pe asset-uri este punctul de plecare implicit, dacă nu există un motiv bun altfel (infrastructură Airflow existentă, workload-uri ML grele mai bine servite de Argo, o preferință organizațională puternică). Pentru platformele legacy, întrebarea este dacă costul migrării se amortizează, ceea ce de obicei se întâmplă pentru magazinele suficient de mari încât să simtă durerea modelului orientat pe task-uri și nu pentru magazinele suficient de mici încât durerea să fie teoretică.

Lecția următoare rămâne în același cartier: observabilitatea pentru date, unde încadrarea orientată pe asset-uri întâlnește uneltele de lineage (Marquez, OpenLineage, DataHub) care transformă graful de asset-uri în ceva ce restul organizației poate naviga. Firul care leagă ambele lecții: o platformă de date pe care nu o poți vedea este o platformă de date pe care nu o poți opera, iar „a vedea” înseamnă mai mult decât starea task-urilor. Înseamnă să știi ce ai, de unde a venit, când a fost atins ultima oară și cine îl citește. Orchestratorul orientat pe asset-uri este jumătatea acelei imagini care trăiește unde se întâmplă munca; tooling-ul de lineage și observabilitate este jumătatea care trăiește unde privește restul organizației.

Citate și lecturi suplimentare

  • Documentația Dagster, secțiunea „Assets”, https://docs.dagster.io/concepts/assets/software-defined-assets (consultat 2026-05-01). Referința canonică pentru modelul software-defined asset.
  • Nick Schrock, „Introducing Software-Defined Assets” (blogul Dagster, 2022). Postarea originală de încadrare, care argumentează pentru asset-uri ca unitate de orchestrare.
  • Documentația Apache Airflow, secțiunea „Datasets”, https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/datasets.html (consultat 2026-05-01). Răspunsul Airflow la încadrarea orientată pe asset-uri, util ca punct de comparație.
  • „Data Pipelines Pocket Reference” (James Densmore, O’Reilly, 2021) și „Fundamentals of Data Engineering” (Joe Reis și Matt Housley, O’Reilly, 2022). Ambele tratează orchestrarea în context și discută încadrarea de produs de date pe care orchestrarea orientată pe asset-uri o formalizează.
Caută