Vai al contenuto principale
Stream Processing - immagine ufficiale della lezione su GinnyTech, creata da AD

Fondamenti di stream processing

Introduzione allo stream processing: differenza tra batch e real-time, architetture e pattern fondamentali.

AD
Creato da Andrii Dyshkantiuk
Lezione 120 / 216 Livello: Avanzato Durata: 22 min

Cosa imparerai

  • Comprendere il problema analitico e il contesto decisionale
  • Applicare esempi, metriche e controlli a casi reali

Collegamenti

Ingresso diretto nel modulo.

Fondamenti di stream processing

Un prodotto digitale genera eventi ogni secondo: click, pagamenti, errori, cambi di stato. Il problema non è solo elaborarli in fretta, ma decidere quali eventi richiedono una reazione immediata, quali possono aspettare il batch e quali garanzie servono perché il risultato sia credibile. Fondamenti di stream processing parte da questa distinzione tra velocità utile e fretta costosa.

Una scena da cui partire

Leggi la lezione come una scelta di architettura: event time o processing time, finestra tumbling o sliding, at-least-once o exactly-once. Ogni concetto va collegato a una domanda operativa: quanto ritardo posso tollerare, quanto errore posso accettare e quale costo sono disposto a pagare per correggerlo?

  • Contesto: Quale vincolo tecnico decide il disegno?
  • Metodo: Quale controllo ti direbbe che il risultato è affidabile?
  • Applicazione: Quale trade-off racconteresti prima di mettere in produzione?

Oltre la Latenza: La Riorganizzazione Concettuale dal Batch allo Streaming

Il confronto tra elaborazione batch e stream processing viene spesso ridotto a una semplice questione di velocità, ma questa è una semplificazione che ne occulta la vera portata. La distinzione fondamentale non è quantitativa (minuti contro millisecondi), bensì qualitativa e riguarda la natura stessa del dato. L’elaborazione batch opera su dati a riposo (data at rest): un insieme di dati finito, completo e delimitato. Immaginiamo il processo di un censimento nazionale: raccogliamo dati per mesi, li immagazziniamo, e solo alla fine del processo li analizziamo nella loro interezza per produrre un report. Il dataset è statico; possiamo rileggerlo più volte, garantendo risultati deterministici. Questo modello è perfetto per task come la fatturazione mensile, il training di modelli di machine learning su dati storici o la generazione di report finanziari trimestrali.

Lo stream processing, al contrario, opera su dati in movimento (data in motion): un flusso di eventi potenzialmente infinito, incompleto e non ordinato. L’analogia più calzante è quella di un controllore del traffico aereo. Non può attendere che tutti gli aerei della giornata siano atterrati per decidere come gestire le rotte; deve prendere decisioni immediate basate su un flusso continuo di informazioni parziali (posizione, velocità, meteo). Ogni evento—una transazione, un click su un sito, una lettura da un sensore IoT—viene processato individualmente o in piccole finestre temporali appena arriva. Questo impone un cambio di mentalità: non si interroga più un database, si reagisce a un flusso.

Questa dicotomia ha dato vita a due principali architetture ibride. L’Architettura Lambda, proposta da Nathan Marz, accetta la dualità e implementa due percorsi paralleli: un batch layer, lento ma accurato, che ricalcola periodicamente la verità assoluta sui dati storici, e uno speed layer, veloce ma approssimato, che fornisce una vista in tempo reale sugli eventi recenti. I risultati dei due layer vengono poi uniti per rispondere alle query. Sebbene robusta, la Lambda è complessa da mantenere, richiedendo la gestione di due codebase e due pipeline distinte. Più recentemente, l’Architettura Kappa, promossa da Jay Kreps, ha proposto una visione più radicale: eliminare il batch layer. L’idea è di utilizzare un unico sistema di stream processing per gestire tutto. La “verità” storica non è più in un data warehouse, ma nel log immutabile degli eventi (come un topic di Apache Kafka). Se è necessario ricalcolare qualcosa, si riprocessa l’intero stream dall’inizio. Questo approccio è stato reso praticabile dai moderni stream processor (come Apache Flink o ksqlDB) e dalla velocità dei sistemi di storage, semplificando notevolmente l’infrastruttura. La scelta non è più solo tra “veloce” e “lento”, ma tra paradigmi computazionali che definiscono come un’organizzazione percepisce e interagisce con il tempo.

La Sincronizzazione con la Realtà: Event Time vs. Processing Time

Nel mondo dello stream processing, il tempo non è un concetto monolitico. La sua gestione è forse la sfida più complessa e sottile, con implicazioni dirette sulla correttezza di qualsiasi analisi. Dobbiamo distinguere due dimensioni temporali fondamentali:

  1. Event Time: È il momento in cui un evento si è verificato nel mondo reale. Questo timestamp è intrinseco all’evento stesso e viene generato alla fonte. Ad esempio, il momento esatto in cui un utente clicca “play” su un video, registrato dal suo smartphone. L’Event Time è la rappresentazione più fedele della realtà.
  2. Processing Time: È il momento in cui l’evento viene ricevuto ed elaborato dal nostro sistema di analisi. Questo timestamp è generato dal server che processa il dato.

In un mondo ideale, la differenza tra i due sarebbe costante e trascurabile. Nella realtà, questa differenza, nota come skew, è variabile e può essere significativa. Immaginiamo un utente che guarda video su un treno con una connessione a intermittenza. Potrebbe compiere dieci azioni (play, pausa, like) mentre è offline. Quando il suo dispositivo si ricollega alla rete, invia tutti e dieci gli eventi in un’unica raffica. Se analizzassimo questi dati usando il Processing Time, sembrerebbe che l’utente abbia compiuto dieci azioni in un secondo, un comportamento anomalo che potrebbe inquinare le nostre metriche di engagement. Se invece usiamo l’ Event Time, possiamo ricostruire la sequenza temporale corretta delle sue azioni, indipendentemente dai ritardi di rete.

Lavorare con l’Event Time, tuttavia, introduce un problema: come facciamo a sapere quando abbiamo ricevuto tutti gli eventi relativi a un certo periodo? Se vogliamo calcolare il numero di utenti attivi alle 10:05, per quanto tempo dobbiamo attendere gli eventi con timestamp 10:05:xx che potrebbero essere in ritardo? Aspettare all’infinito non è un’opzione. Qui entra in gioco il concetto di Watermark. Un watermark è un meccanismo che il sistema di stream processing usa per stimare l’avanzamento del tempo degli eventi. È essenzialmente una dichiarazione della forma: “Con alta probabilità, non riceveremo più eventi accaduti prima del tempo T”. Quando il watermark supera il confine di una finestra temporale (ad esempio, supera le 10:05:59), il sistema considera quella finestra “chiusa” e ne emette il risultato aggregato. Un watermark è un compromesso tra correttezza e latenza: un watermark “aggressivo” (che avanza rapidamente) riduce la latenza ma rischia di escludere eventi in ritardo; un watermark “conservativo” (che avanza lentamente) è più completo ma aumenta la latenza. Gli eventi che arrivano dopo che il loro watermark è passato vengono considerati “late data” e possono essere scartati, inviati a un’altra pipeline per un’analisi separata, o usati per aggiornare un risultato già emesso, a seconda della logica di business.

Architetture di Stato e Finestre Temporali: Dare Memoria ai Flussi Infiniti

Un flusso di eventi, preso singolarmente, ha un valore informativo limitato. Il vero insight emerge quando mettiamo in relazione eventi diversi nel tempo. Per fare ciò, un sistema di stream processing ha bisogno di “memoria”. Questa memoria è chiamata stato (state). Lo stato è qualsiasi dato che il sistema deve mantenere e aggiornare durante l’elaborazione del flusso. Può essere semplice come un contatore (es. il numero di visualizzazioni di una pagina) o complesso come un modello di machine learning che si aggiorna con ogni nuovo dato. Senza stato, ogni evento sarebbe un’entità isolata; con lo stato, possiamo rilevare pattern, calcolare medie mobili e costruire profili utente dinamici.

Tuttavia, in un flusso infinito, non possiamo mantenere lo stato per sempre. Le risorse di memoria e calcolo sono finite. Dobbiamo quindi definire dei confini entro cui aggregare gli eventi e calcolare lo stato. Questi confini sono le finestre temporali (windows). Le finestre sono il meccanismo principale per trasformare un flusso illimitato in una serie di micro-batch finiti su cui eseguire calcoli. Esistono diverse tipologie di finestre, ognuna adatta a un diverso caso d’uso analitico:

  • Tumbling Windows (Finestre a Salto): Queste finestre dividono il tempo in segmenti di dimensione fissa e non sovrapposti. Ad esempio, una finestra a salto di 5 minuti aggregherebbe tutti gli eventi dalle 10:00:00 alle 10:04:59, poi un’altra dalle 10:05:00 alle 10:09:59, e così via. Ogni evento appartiene a una e una sola finestra. Sono ideali per la creazione di dashboard e report periodici (es. “vendite al minuto”).

  • Sliding Windows (Finestre Scorrevoli): Anche queste finestre hanno una dimensione fissa, ma si sovrappongono. Vengono definite da due parametri: la dimensione e l’intervallo di scorrimento. Ad esempio, una finestra di 5 minuti che scorre ogni minuto calcolerebbe un’aggregazione sulle 10:00-10:05, poi sulle 10:01-10:06, e così via. Un singolo evento può appartenere a più finestre. Sono perfette per calcolare medie mobili e rilevare trend in tempo reale (es. “media a 10 secondi del carico della CPU”).

  • Session Windows (Finestre di Sessione): A differenza delle precedenti, queste finestre non hanno una durata fissa. Raggruppano eventi in base all’attività, definendo una sessione come una sequenza di eventi separata da un periodo di inattività (gap). Ad esempio, si potrebbe definire una sessione utente come una serie di click, con la sessione che si chiude se non ci sono attività per 30 minuti. Sono cruciali per analizzare il comportamento degli utenti (es. “durata media della sessione sul sito”).

Ecco un esempio pratico in Flink SQL che calcola il numero di click per utente in finestre a salto di 10 secondi, basandosi sull’Event Time.

-- Definiamo una tabella sorgente che rappresenta un flusso di click da Kafka.
CREATE TABLE user_clicks (
    user_id STRING,
    event_timestamp TIMESTAMP(3),
    url STRING,
    -- Dichiariamo che 'event_timestamp' è il campo dell'Event Time
    -- e impostiamo un watermark che permette un ritardo massimo di 5 secondi.
    WATERMARK FOR event_timestamp AS event_timestamp - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'clicks',
    'properties.bootstrap.servers' = 'kafka:9092',
    'format' = 'json'
);

-- Definiamo una tabella di destinazione per i risultati.
CREATE TABLE click_counts (
    window_start TIMESTAMP(3),
    window_end TIMESTAMP(3),
    user_id STRING,
    clicks_count BIGINT
) WITH (
    'connector' = 'print' -- Per semplicità, stampiamo i risultati a console.
);

-- Query di aggregazione che usa una Tumbling Window.
INSERT INTO click_counts
SELECT
    -- La funzione TUMBLE raggruppa le righe in finestre.
    TUMBLE_START(event_timestamp, INTERVAL '10' SECOND) AS window_start,
    TUMBLE_END(event_timestamp, INTERVAL '10' SECOND) AS window_end,
    user_id,
    COUNT(url) AS clicks_count
FROM
    user_clicks
GROUP BY
    -- Il raggruppamento avviene per finestra e per chiave (user_id).
    TUMBLE(event_timestamp, INTERVAL '10' SECOND),
    user_id;

Questo codice non è pseudocodice. È una query Flink SQL funzionante che legge da un topic Kafka, gestisce l’Event Time e i watermark, applica una finestra a salto e calcola un’aggregazione in tempo reale.

Garanzie di Consistenza nel Caos Distribuito: Exactly-Once Semantics

I sistemi di stream processing sono, per loro natura, sistemi distribuiti. Gli eventi fluiscono attraverso molteplici nodi, da un message broker (come Kafka) a diversi worker di elaborazione, fino a un sistema di destinazione (sink) come un database o un data warehouse. In questo percorso, i guasti sono inevitabili: un server può riavviarsi, una rete può avere una partizione. La domanda critica è: cosa succede a un evento quando si verifica un guasto? Viene perso? Viene processato due volte? La risposta a questa domanda definisce le garanzie di elaborazione del sistema, note come semantics.

  1. At-most-once (Al massimo una volta): Questa è la garanzia più debole. Il sistema non fa alcuno sforzo per recuperare da un fallimento. Se un worker si arresta mentre sta processando un evento, quell’evento è perso per sempre. È una semantica a bassa latenza e ad alte prestazioni, accettabile solo per casi d’uso in cui la perdita di dati è tollerabile, come il conteggio di metriche non critiche (es. visualizzazioni di un banner pubblicitario, dove una piccola discrepanza non ha impatto sul business). La filosofia è: “meglio perdere un dato che rischiare di contarlo due volte”.

  2. At-least-once (Almeno una volta): Questa è la garanzia più comune. Il sistema garantisce che ogni evento verrà processato. Lo fa tramite meccanismi di acknowledgement: la fonte invia un evento e attende una conferma di avvenuta elaborazione. Se la conferma non arriva entro un certo timeout (magari a causa di un crash del worker), la fonte invia nuovamente lo stesso evento. Questo previene la perdita di dati, ma introduce un altro problema: i duplicati. Se il worker ha processato l’evento ma si è arrestato prima di inviare la conferma, riceverà lo stesso evento una seconda volta. Questa semantica è adatta quando il sistema a valle è in grado di gestire i duplicati (ad esempio, tramite operazioni idempotenti o deduplicazione). La filosofia è: “meglio contare un dato due volte che rischiare di perderlo”.

  3. Exactly-once (Esattamente una volta): Questa è la garanzia più forte e complessa da ottenere. Assicura che, nonostante i fallimenti, ogni evento venga elaborato e il suo effetto sullo stato del sistema venga registrato una e una sola volta. Raggiungere questo obiettivo richiede un coordinamento transazionale tra tutti i componenti della pipeline. Framework come Apache Flink lo implementano attraverso un meccanismo di checkpointing distribuito basato sull’algoritmo di Chandy-Lamport. A intervalli regolari, Flink “congela” lo stato dell’intera applicazione (i contatori, le finestre, gli offset di Kafka) e lo salva su uno storage persistente (come HDFS o S3). In caso di fallimento, l’applicazione riparte dall’ultimo checkpoint consistente, garantendo che nessun evento venga perso o riprocessato. Questo approccio, combinato con sorgenti e sink transazionali (come quelli offerti da Kafka >0.11), permette di costruire pipeline end-to-end con garanzie exactly-once, indispensabili per applicazioni critiche come sistemi di fatturazione, elaborazione di pagamenti e ledger finanziari.

Applicazioni sul Campo: Dalla Personalizzazione di Spotify al Dynamic Pricing di Bolt

La teoria dello stream processing prende vita quando osserviamo come le aziende leader la utilizzano per creare valore. Non si tratta di ottimizzazioni marginali, ma di abilitare interi modelli di business che sarebbero impossibili con un approccio batch.

Caso di Studio 1: Spotify e la Personalizzazione in Tempo Reale La homepage di Spotify e le sue playlist come “Discover Weekly” sono percepite come magicamente intuitive. Questa “magia” è alimentata da una massiccia infrastruttura di stream processing. Ogni interazione dell’utente—un play, uno skip entro i primi 30 secondi, l’aggiunta di una canzone a una playlist, una ricerca—viene catturata come un evento e inviata a un flusso Kafka. Questi eventi vengono consumati in tempo reale da decine di servizi. Un servizio potrebbe aggiornare il “profilo di gusto” a lungo termine dell’utente, mentre un altro, focalizzato sulla sessione corrente, cerca di capire l’umore o l’attività del momento (es. “allenamento”, “concentrazione”). L’architettura di Spotify, basata su un modello Kappa, utilizza Flink per processare questi flussi. Ad esempio, un job Flink potrebbe calcolare in una finestra di sessione quali artisti un utente sta ascoltando consecutivamente, per inferire una preferenza immediata. Questi insight vengono poi usati per alimentare modelli di machine learning che, a loro volta, aggiornano le raccomandazioni sulla homepage o nella funzione “Radio”. L’impatto è misurabile: dopo l’introduzione di queste pipeline real-time, Spotify ha registrato un aumento del 21% nell’engagement degli utenti misurato come “long-plays” (canzoni ascoltate per più di 30 secondi) provenienti da raccomandazioni, e una riduzione del 15% nello “skip rate” sulla homepage. Lo stream processing trasforma l’esperienza utente da statica a un dialogo continuo e reattivo.

Caso di Studio 2: Bolt e il Dynamic Pricing Geospaziale Il servizio di ride-hailing di Bolt (simile a Uber) deve bilanciare costantemente domanda (utenti che richiedono una corsa) e offerta (autisti disponibili) in un contesto geografico e temporale. Il dynamic pricing, o “surge pricing”, è lo strumento principale per raggiungere questo equilibrio. Un’architettura batch, che aggiornasse i prezzi ogni 5 minuti, sarebbe troppo lenta: in una grande città, la domanda può cambiare radicalmente in pochi secondi dopo la fine di un concerto o di un evento sportivo. Bolt utilizza un’architettura di stream processing per gestire questa complessità. I flussi di dati includono: le posizioni GPS di tutti gli autisti (inviate ogni pochi secondi), le richieste di corse da parte degli utenti e dati esterni come il traffico in tempo reale. La città viene suddivisa in una griglia di celle geografiche (es. S2 cells). I job di stream processing (spesso implementati con Flink o Spark Streaming) eseguono aggregazioni continue su queste celle. Per ogni cella, calcolano in finestre scorrevoli di 30-60 secondi il rapporto tra il numero di utenti che hanno aperto l’app (domanda latente), il numero di richieste effettive e il numero di autisti disponibili. Se questo rapporto supera una certa soglia, viene applicato un moltiplicatore di prezzo in quella specifica zona. Questo non solo massimizza le entrate, ma incentiva gli autisti a spostarsi verso le aree a alta domanda, riducendo i tempi di attesa per gli utenti. L’implementazione di questo sistema ha permesso a Bolt di ridurre il tempo medio di attesa del passeggero del 28% nelle ore di punta e di aumentare l’utilizzo della flotta (tempo in cui un autista ha un passeggero) di 19 punti percentuali.

Laboratorio Pratico: Analisi di un Flusso di Clickstream

Per consolidare questi concetti, proviamo a simulare un’analisi real-time su un flusso di eventi di clickstream. Gli esercizi sono progressivi e utilizzano strumenti comuni nell’ecosistema Kafka come ksqlDB, un database per lo streaming che permette di usare un dialetto SQL per interrogare flussi di dati.

Prerequisiti: Un’istanza di Kafka e ksqlDB in esecuzione (ad esempio, tramite Docker Compose).

Esercizio 1: Creazione e Popolamento di un Flusso Per prima cosa, creiamo un “flusso” (STREAM in ksqlDB, che corrisponde a un topic Kafka) per i nostri eventi di click e inseriamo qualche dato.

Laboratorio ed esercizi

Metti in pratica quanto appreso con esercizi a difficoltà crescente. Lavora su un dataset reale — se non hai accesso al tuo data warehouse aziendale, usa dataset pubblici come Google Analytics Sample su BigQuery o il dataset E-Commerce di Kaggle.

Esercizio 1 — Implementazione base. Riproduci la query o il modello descritto nella lezione, adattandolo al tuo dataset. Verifica che i risultati siano coerenti con le metriche attese: se il totale non quadra con una query di controllo, c’è un problema di grain.

Esercizio 2 — Estensione. Aggiungi una dimensione di analisi non coperta nella lezione: segmenta per paese, per device, per fascia oraria o per coorte. Dove emergono pattern inattesi? Cosa implicano per le decisioni operative?

Esercizio 3 — Automazione. Trasforma la query in una vista o in un modello dbt con test di integrità (unique, not_null) e documenta le colonne. Se il tuo stack lo permette, configura un alert che notifichi quando la metrica esce da 2 deviazioni standard dalla media mobile.

Problema reale

Nel dominio di real-time analytics, Fondamenti di stream processing serve a risolvere questo problema: rendere decisioni e alert rapidi senza sacrificare accuratezza, costo e stabilità del sistema. La lezione non va trattata come teoria isolata, ma come un modo per migliorare una scelta concreta con dati, assunzioni esplicite e controlli minimi.

Obiettivo operativo: Comprendere il problema analitico e il contesto decisionale; Applicare esempi, metriche e controlli a casi reali. Se alla fine non sai indicare quale decisione cambia, quale dato osservi e quale errore vuoi evitare, la lezione non è ancora diventata competenza applicata.

Modello concettuale

FaseCosa chiarireOutput
DomandaQuale scelta reale deve migliorare?Decisione da prendere
MisuraQuale segnale osservabile rappresenta il problema?Metrica o dato sorgente
ControlloQuale baseline rende il risultato interpretabile?Confronto credibile
AzioneChe cosa cambia dopo l’analisi?Prossimo passo operativo

Il modello concettuale è intenzionalmente semplice: decisione, dato, controllo, azione. Ogni approfondimento tecnico deve rafforzare almeno uno di questi quattro punti.

Formalizzazione rigorosa

Per rendere Fondamenti di stream processing analizzabile, definisci prima l’unità di lavoro: evento, finestra temporale, materialized view, alert o metrica live. Poi collega questa unità a una metrica osservabile: latenza, freshness, falsi positivi, throughput e costo query. Infine dichiara la decisione attesa: pipeline realtime, vista aggregata, alert o dashboard operativa.

ElementoSpecifica richiesta
Unità di analisievento, finestra temporale, materialized view, alert o metrica live
Segnale principalelatenza, freshness, falsi positivi, throughput e costo query
BaselinePeriodo precedente, gruppo comparabile, benchmark o scenario controfattuale
Decisionepipeline realtime, vista aggregata, alert o dashboard operativa
RischioScambiare un numero disponibile per una prova sufficiente

La formalizzazione e solida quando un altro analista può riprodurre la logica, criticare le assunzioni e ottenere la stessa decisione partendo dagli stessi dati.

Esempio o caso studio

Il team deve scegliere se portare in streaming il monitoraggio frodi: il batch giornaliero è stabile, ma arriva troppo tardi quando un attacco è già in corso. Il caso confronta latenza, completezza, duplicati e responsabilità operative prima di decidere se il real-time aggiunge valore o solo complessità.

Evidenza osservataLettura prudenteAzione consigliata
Il numero miglioraPotrebbe essere effetto reale o variazione normaleCercare confronto e segmento
Un segmento cambia più degli altriLa media aggregata nasconde una differenzaSeparare coorti o casi d’uso
Il costo cresce insieme al risultatoL’impatto va letto sul margineStimare trade-off e sostenibilità

Lab / esercizio

Livello base

Scrivi una scheda di una pagina per Fondamenti di stream processing: decisione da supportare, metrica primaria, baseline, rischio principale e azione se il segnale e confermato.

Livello intermedio

Costruisci una tabella con tre segmenti, periodi o scenari. Per ciascuno indica cosa cambia, quale spiegazione alternativa e plausibile e quale controllo useresti prima di raccomandare un azione.

Livello research-grade

Prepara un decision memo: ipotesi, dati richiesti, criteri di esclusione, controlli di qualità, soglia decisionale, rischio residuo e piano di monitoraggio dopo la decisione.

Dataset e materiali consigliati

Usa ClickHouse, stream eventi, CDC, metriche operative, dashboard realtime e log applicativi. Se non hai accesso a dati reali, crea un dataset sintetico con almeno 200 righe, una dimensione temporale, una dimensione segmento e una metrica di outcome.

Errore tipico da evitare

L’errore più comune e usare Fondamenti di stream processing come etichetta invece che come processo. Succede quando il team mostra un grafico senza decisione, una metrica senza baseline, o una conclusione senza indicare quale assunzione potrebbe invalidarla.

La domanda di controllo è: se questo risultato fosse instabile, quale scelta sbaglierei? Se la risposta non è concreta, manca ancora il collegamento tra analisi e azione.

Quiz o checkpoint

  1. Quale decisione concreta dovrebbe migliorare questa lezione?
  2. Quale unità di analisi rende il problema misurabile?
  3. Quale baseline useresti per evitare una lettura ingenua?
  4. Quale errore tipico potrebbe cambiare la conclusione?
  5. Quale output consegneresti a uno stakeholder non tecnico?

Riepilogo operativo

Fondamenti di stream processing diventa utile quando produce una decisione più chiara, non quando aggiunge terminologia. Usa il framework problema, modello, formalizzazione, esempio, lab e checkpoint per trasformare la lezione in pratica verificabile. Categoria: Tecnico. Difficoltà: advanced. Tempo stimato: 22 min.