Architettura di dati e sistemi, dalle fondamenta Lezione 48 / 80

Caso reale: le pipeline real-time di Uber (Marmaray, l'origine di Hudi)

L'evoluzione di Uber da batch-only a streaming-first, il problema dell'ingestion dei dati, e il progetto Hudi che ne è venuto fuori.

Il Modulo 6 finisce nel modo in cui è finito il Modulo 5: con una sola azienda, un decennio, e post di engineering pubblici che trasformano le astrazioni del modulo in macchinari concreti. Il Modulo 5 si è chiuso su Netflix, dove la storia interessante era una piattaforma batch alla scala di petabyte. Il Modulo 6 si chiude su Uber, dove la storia interessante è il passaggio da batch-only a streaming-first, il problema di ingestion che ne è derivato, e il progetto Hudi che ne è venuto fuori. Entrambe le storie riguardano la stessa superficie architetturale (un lakehouse più diversi engine più un layer di orchestrazione), e la differenza è cosa ha guidato la costruzione: la pressione di Netflix era scala e costo; la pressione di Uber era latenza.

L’inquadratura della lezione 47 sta dietro a questa lezione. Uber non ha scelto deliberatamente Kappa rispetto a Lambda; hanno percorso il sentiero che l’industria ha percorso, in pubblico, con le loro conclusioni visibili in progetti open-source. Marmaray e Hudi sono i due artefatti che contano per questo modulo. Marmaray è il framework di ingestion. Hudi è il layer di storage che ha reso lo streaming-into-lake una cosa vera invece di un workaround.

La scala e il problema

I talk pubblici e i post di engineering di Uber danno la forma approssimativa. Decine di milioni di trip al giorno attraverso centinaia di paesi, ogni trip che produce decine di eventi: requested, matched, started, location updates, paid, rated. Centinaia di migliaia di driver e milioni di rider che generano segnali da fare ingestion, joinare, modellare, scorare, e su cui agire. Petabyte nel lake, migliaia di servizi, centinaia di team. Un piccolo insieme di decisioni deve avvenire in real time; una lunga coda può girare di notte.

L’Uber Engineering blog (https://www.uber.com/blog/engineering/data/, consultato il 2026-05-01) è stato insolitamente generoso con post pubblici sull’architettura. Il riassunto che segue cuce insieme post su Marmaray, Hudi, la streaming platform, e lo stack di dati più ampio, con le citazioni alla fine.

Lo stack pre-2014 era batch-only. Job Hadoop giornalieri aggregavano gli eventi dei trip per analytics, finance, e machine learning offline. Andava bene per l’analytics, ma i casi d’uso operativi (surge pricing, fraud detection, modelli di ETA, dashboard real-time per i centri operativi) avevano bisogno di risposte in secondi o minuti. Per la metà degli anni 2010 l’azienda stava attaccando pipeline streaming sul fianco dello stack batch, e il bolt-on stava diventando costoso.

Due dolori specifici hanno guidato i rewrite che hanno prodotto Marmaray e Hudi.

L’ingestion era custom per ogni source. I dati fluivano da Kafka, repliche MySQL, Cassandra, store interni schemaless, e una lunga coda di servizi. Ogni source aveva la sua pipeline su misura verso Hadoop, scritta da qualunque team avesse costruito quella source per primo. Gli schemi andavano alla deriva. I fallimenti venivano debuggati source per source. Aggiungere un nuovo sink significava riscrivere una nuova pipeline per ogni source da capo.

Il data lake non poteva assorbire gli update. I dati di Uber sono per lo più mutabili. Lo stato di un trip cambia man mano che procede. Il profilo di un driver viene aggiornato. Il metodo di pagamento di un rider viene corretto. Il modello append-only di file di Hadoop era una scelta povera, e i workaround (snapshot giornalieri completi, tabelle di slowly changing dimension in job notturni) davano risultati corretti una volta al giorno e dati vecchi di un’ora il resto del tempo. Non meglio dello stack batch-only che stavano cercando di superare.

Marmaray: il framework di ingestion

Marmaray è stato rilasciato in open source da Uber nel 2018 e descritto nel post sul blog di Uber Engineering “Marmaray: An Open Source Generic Data Ingestion and Dispersal Framework for Apache Hadoop” (https://www.uber.com/blog/marmaray-hadoop-ingestion-open-source/, consultato il 2026-05-01). Il pitch sta nel sottotitolo: un framework generico per spostare dati tra qualunque source e qualunque sink.

L’idea architetturale è il decoupling. Source e sink sono pluggable. Il framework fornisce l’impalcatura attorno: gestione degli schemi, gestione degli errori, retry, monitoring, dispersal dei dati derivati di nuovo verso altri sistemi. Il grosso del lavoro pesante gira su Spark.

I deployment in Uber coprivano Kafka verso Hadoop (il caso firehose), Cassandra e MySQL verso Hadoop (snapshot e CDC dagli store operativi), e il path inverso da Hadoop verso store operativi quando i modelli addestrati in batch dovevano essere serviti in produzione. Un framework, molte coppie source-sink, con il lavoro per coppia limitato ai connettori.

La lezione generalizzata: il valore di un framework di ingestion non sono i connettori. I connettori sono la parte facile. Il valore è la consistenza. Ogni pipeline passa attraverso lo stesso framework, con la stessa observability, gli stessi retry, la stessa gestione degli schemi, e la stessa storia operativa. Aggiungere una nuova pipeline diventa configurazione più un connettore, non un esercizio di design da capo. Alla scala di Uber questa era la differenza tra centinaia di pipeline su misura e una sola piattaforma.

Hudi: storage costruito per lo streaming-into-lake

Apache Hudi è iniziato in Uber nel 2016 come risposta al problema dell’upsert. È stato rilasciato in open source nel 2017, è diventato un progetto Apache Incubator, e ha raggiunto lo status top-level nel 2020. Il post originale del blog di Uber Engineering “Uber’s Big Data Platform: 100+ Petabytes with Minute Latency” (https://www.uber.com/blog/uber-big-data-platform/, consultato il 2026-05-01) espone la motivazione, e la documentazione di Apache Hudi (https://hudi.apache.org/, consultato il 2026-05-01) descrive il formato in dettaglio.

Hudi è stato progettato per una forma specifica di workload: stream di eventi, molti dei quali sono update a entità che esistono già nel lake. Un evento di trip arriva, e potrebbe essere il primo evento per quel trip (insert), o un update a un trip che è già in volo (update), o una correzione a un trip completato ieri (update tardivo). Il layer di storage doveva rendere efficienti e atomici tutti e tre i casi.

Hudi espone tre concetti chiave.

Tabelle Copy-on-Write (CoW). Ogni upsert riscrive i data file che contengono le righe interessate. Le read sono veloci (nessuno step di merge al read time), ma le write sono costose: cambiare una riga in un file Parquet da cento megabyte significa riscrivere l’intero file. CoW è la scelta giusta quando le read dominano e le write sono concentrate in un piccolo numero di file (le partizioni recenti), che è il pattern comune per la telemetria time-series.

Tabelle Merge-on-Read (MoR). Ogni upsert appende un delta a livello di riga a un file di log accanto al base Parquet. Le read fanno il merge tra base e delta al query time. Le write sono economiche (solo append di un delta), ma le read sono più lente perché pagano il costo del merge. Una compaction periodica riscrive i delta nei file base, ripristinando le performance di read. MoR è la scelta giusta quando le write dominano o quando i singoli record vengono aggiornati frequentemente attraverso la tabella, come con i CDC mirror dei database operativi.

Snapshot e time travel. Hudi traccia una timeline immutabile di commit. Ogni read sceglie uno snapshot su cui interrogare, e puoi chiedere la tabella a un timestamp o un commit specifico. Lo stesso macchinario alimenta le query incrementali: “dammi le righe che sono cambiate tra il commit T1 e il commit T2”, che è la primitiva naturale per le pipeline downstream che vogliono consumare solo cosa c’è di nuovo.

Hudi è stato il primo formato di storage open-source a prendere sul serio tutte e tre queste feature insieme. Quando Delta Lake (2019) e Iceberg (donato nel 2018) stavano maturando, Hudi aveva già deployment in produzione in Uber ed era la scelta naturale per i workload upsert-heavy. La lezione 37 ha coperto le format wars e dove l’industria si è posata.

La forma della pipeline di Uber

Mettere insieme Marmaray, Hudi, Kafka, e Flink dà lo stack streaming-first che i post pubblici di Uber descrivono. La forma è riconoscibilmente Kappa-flavoured (lezione 47), con un solo layer di storage che alimenta sia i consumer real-time che quelli batch.

flowchart LR
    SVC[Application services<br/>rides, payments, location]
    K[(Kafka<br/>event log)]
    F[Flink<br/>real-time aggregates]
    M[Marmaray<br/>ingestion]
    H[(Hudi tables<br/>on HDFS or S3)]
    P[Presto / Trino<br/>interactive queries]
    S[Spark<br/>batch analytics]
    OPS[Operations dashboards<br/>surge, ETA]
    BI[BI and finance]
    ML[ML training]
    SVC --> K
    K --> F
    K --> M
    F --> OPS
    M --> H
    H --> P
    H --> S
    P --> BI
    S --> ML
    S --> BI

Diagramma da creare: una versione rifinita del diagramma Mermaid sopra, con i servizi all’estrema sinistra, Kafka come spina dorsale centrale, i due path di consumer (Flink per il real-time, Marmaray per l’ingestion) che si dipartono dalla spina dorsale, Hudi come layer di storage al centro, i tre query engine (Flink, Presto, Spark) come compute sopra Hudi, e i consumer (dashboard operative, BI, ML) sulla destra. Il punto visivo è che c’è un solo event log che alimenta sia il mondo real-time sia quello batch, e un solo layer di storage che alimenta più query engine.

Un evento di trip fluisce dal servizio applicativo dentro Kafka. Da Kafka si divide in due strade. Flink lo legge per gli aggregati real-time che alimentano il surge pricing e le dashboard operative. Marmaray lo legge (insieme a tutto il resto da Kafka, MySQL, e Cassandra) e lo deposita in tabelle Hudi sul data lake. Presto e Spark leggono da Hudi per query interattive e batch analytics. Le stesse tabelle Hudi stanno dietro alle dashboard, ai report di finance, ai dataset di training ML, e al path di replay storico.

Da questa forma cadono fuori due proprietà architetturali.

Un solo layer di storage, più query engine. Le tabelle Hudi sono lo store fisico unico. La scelta del query engine è per workload: Presto per analytics interattive, Spark per ETL pesante, Flink per streaming. Cambiare engine non richiede di migrare i dati. Aggiungere un nuovo engine è un connettore più un benchmark. Questo decoupling è la vittoria architetturale, ed è il motivo per cui il lavoro sui formati di tabella aperti (lezione 37) conta così tanto: il formato è il contratto su cui gli engine sono tutti d’accordo.

Lo streaming-first cambia il ruolo del lake. Pre-streaming, il data lake era una destinazione batch principalmente in lettura. Post-streaming, è uno store transazionale con write continue e update atomici. Il contratto del lake è cambiato. Hudi (e Iceberg e Delta) sono come appare il nuovo contratto in pratica. I pattern che i data engineer usano per interagire con il lake (query incrementali, read di snapshot, time travel) sono i pattern che fluiscono da questo cambio di contratto.

Le lezioni

Cinque take-away, strutturati nel modo in cui il caso Netflix del Modulo 5 li ha strutturati.

Lo streaming-first cambia il ruolo del lake. Lo shift architetturale più grande nel viaggio di Uber non è “ora usiamo Kafka e Flink”. È che il data lake ha smesso di essere una destinazione batch ed è diventato uno store transazionale. I pattern di interazione con esso sono cambiati. Il formato ha dovuto cambiare per supportarlo. Il modello mentale ha dovuto cambiare. Questo è il passaggio da “leggiamo i dati di ieri” a “leggiamo dati continuamente aggiornati con semantica di snapshot”, ed è il passaggio per cui il Modulo 6 si è preparato. La domanda Lambda contro Kappa della lezione 47 è, a un certo livello, una domanda su se il tuo layer di storage può supportare entrambe le read insieme.

Open-source quello che costruisci. L’investimento di Uber in Hudi è stato ripagato in contributi della community, supporto di engine più ampio, e standardizzazione. La storia di Iceberg (lezione 40) in Netflix e la storia di Hudi in Uber sono della stessa forma: un’azienda risolve un problema interno difficile, fa l’open-source della soluzione, e cavalca l’onda dei miglioramenti della community in seguito. Il costo di fare open-source è reale; il beneficio (software durevole, hiring più facile, allineamento dell’ecosistema) è più grande alla scala. I team più piccoli dovrebbero adottare questi formati invece che inventarne di nuovi.

Un solo layer di storage, più engine. Il decoupling tra formato di tabella e query engine è il cambiamento moderno più consequente nello stack di dati. Un warehouse del 2010 possedeva storage ed engine insieme; un lakehouse del 2026 li separa. Hudi (o Iceberg o Delta) è lo storage; Spark, Presto, Flink, e una dozzina di altri sono gli engine. Questa separazione lascia che un singolo team faccia girare query interattive, ETL batch, e job di streaming contro gli stessi dati senza copiarli attraverso i sistemi.

Build contro buy alla scala. Uber ha costruito Hudi perché nel 2016 nessuna opzione off-the-shelf supportava i loro bisogni di upsert contro l’object storage. Quel calcolo era corretto per Uber. È sbagliato per quasi ogni altro team, ed è sempre più sbagliato adesso che Hudi, Iceberg, e Delta esistono e sono maturi. Il principio generale: costruisci componenti di piattaforma solo quando quelli standard genuinamente non si adattano, e accetta che la soglia per “build” è molto più alta di quanto sembri. La storia di Maestro di Netflix (lezione 40) ha fatto lo stesso punto sugli orchestrator.

La piattaforma assorbe la complessità così che i data engineer non lo facciano. Marmaray nasconde la complessità di ingestion dietro a un framework uniforme. Hudi nasconde la complessità di upsert dietro a un’astrazione uniforme di tabella. I data engineer che scrivono job Spark contro tabelle Hudi non vedono la meccanica del merge-on-read, lo scheduling della compaction, o la scadenza degli snapshot. Lo fa il team di piattaforma. L’obiettivo architetturale è rendere la cosa giusta la cosa facile per i team applicativi, assorbendo la complessità in componenti di piattaforma invece che chiedere a ogni team di gestirla in proprio.

Riferimenti incrociati di nuovo nel Modulo 6

Lo stack di Uber esercita ogni primitiva che il Modulo 6 ha introdotto. Kafka (lezione 42) è l’event log durevole, la spina dorsale del sistema. Gli stream processor (lezione 43) sono Flink, che computano gli aggregati real-time. Watermark e event time sono come quegli aggregati gestiscono le corse che arrivano in ritardo e gli update di location fuori ordine che i dati reali di ride-hailing inevitabilmente contengono. La scelta Lambda contro Kappa (lezione 47) è implicita: Uber fa girare streaming e batch come pipeline separate che condividono lo stesso storage, che è strutturalmente Kappa con due consumer. Hudi (introdotto nella lezione 37 nel contesto del lakehouse) è lo storage che fa funzionare l’intera forma.

I pattern si trasferiscono. Un team che fa girare una tabella Hudi da cento gigabyte contro un piccolo cluster Kafka sta facendo la stessa cosa che fa Uber sui petabyte. I diagrammi sono gli stessi, le preoccupazioni operative sono le stesse, il formato è lo stesso. Le costanti sono diverse. La forma no.

Il Modulo 6 si chiude qui, e cosa viene dopo

Il Modulo 6 ha camminato attraverso lo streaming dall’inizio alla fine. Perché lo streaming è strutturalmente diverso dal batch (lezione 41), Kafka e il log durevole (lezione 42), gli stream processor e gli operatori stateful (lezione 43), event time e watermark (lezione 44), semantica exactly-once nello streaming (lezione 45), CDC e l’unificazione dei dati operativi e analitici (lezione 46), Lambda contro Kappa come inquadratura architetturale (lezione 47), e ora Uber come case study che esercita tutti loro.

I Moduli 5 e 6 insieme sono la metà data-platform del corso. I Moduli da 1 a 4 hanno coperto le fondamenta architetturali. I Moduli 5 e 6 hanno coperto batch e streaming, le due metà di come i dati si muovono e vengono trasformati alla scala. I due case study (Netflix nel Modulo 5, Uber nel Modulo 6) sono la prova concreta che i pattern generalizzano.

Il Modulo 7 inizia una conversazione diversa. Codice, version control, CI/CD, e le pratiche di software engineering circostanti che trasformano i design architetturali in sistemi che girano. La piattaforma dati continuerà a comparire come esempio, ma il topic si sposta su come gli ingegneri davvero consegnano software, che è la pratica dentro cui sta tutta l’architettura. Il vocabolario cambia. La disciplina del “rendere la cosa giusta la cosa facile” no.

Citazioni e ulteriori letture

  • Uber Engineering Blog, “Marmaray: An Open Source Generic Data Ingestion and Dispersal Framework for Apache Hadoop”, 2018, https://www.uber.com/blog/marmaray-hadoop-ingestion-open-source/ (consultato il 2026-05-01). L’introduzione a Marmaray, con l’architettura e i casi d’uso in Uber.
  • Uber Engineering Blog, “Uber’s Big Data Platform: 100+ Petabytes with Minute Latency”, 2018, https://www.uber.com/blog/uber-big-data-platform/ (consultato il 2026-05-01). Il post che ha introdotto Hudi a un pubblico più ampio e ha spiegato la motivazione dello streaming-into-lake.
  • Uber Engineering Blog, post su data e platform indicizzati su https://www.uber.com/blog/engineering/data/ (consultato il 2026-05-01). La serie più ampia che copre il deployment di Kafka, la streaming platform, lo stack di analytics real-time, e le evoluzioni successive dell’architettura.
  • Documentazione di Apache Hudi, https://hudi.apache.org/ (consultato il 2026-05-01). Il riferimento canonico per il formato, i tipi di tabella (Copy-on-Write e Merge-on-Read), il modello di timeline, e la guidance operativa su compaction e clustering.
  • “Designing Data-Intensive Applications” (Martin Kleppmann, O’Reilly, 2017), capitoli 11 e 12. Il riferimento standard per i pattern di streaming che lo stack di Uber instanzia.
Cerca