Il Modulo 6 ha costruito gradualmente il modello dello streaming: gli engine (lezione 41), le topologie (lezione 42), gli state store (lezione 43). Tutto quel macchinario riguardava il come uno stream processor sposta e ricorda i dati. Questa lezione riguarda un problema che il macchinario da solo non risolve, quello che fa deragliare la maggior parte dei primi tentativi di un’aggregazione a finestra: il tempo.
Ci sono due tempi in qualunque sistema di streaming, e non sono lo stesso tempo. Se non li separi nella tua testa, il resto dello streaming ti tradirà silenziosamente. Il bug non è rumoroso. La tua dashboard non crasha. Mostra solo numeri che sembrano ragionevoli e che sono sottilmente sbagliati, e te ne accorgi tre mesi dopo quando finance li riconcilia con la source of truth e fa domande gentili.
I due tempi
Event time è quando l’evento è accaduto nel mondo. Un utente ha cliccato un pulsante alle 10:00:00. Un sensore ha letto una temperatura alle 14:23:17. Un pagamento è stato processato alle 02:11:09. Il timestamp è una proprietà dell’evento stesso, settato dal device o dal servizio che lo ha prodotto, incorporato nel payload del messaggio.
Processing time è quando lo stream processor ha visto l’evento. Lo stesso click che è accaduto alle 10:00:00 potrebbe arrivare al processor alle 10:00:03 a causa della normale latenza di rete, alle 10:00:30 perché il broker aveva un piccolo backlog, alle 11:30:00 perché il telefono dell’utente era offline in metro e l’SDK ha bufferizzato gli eventi per novanta minuti, oppure, nel caso davvero patologico, alle 02:00 del mattino dopo perché un disservizio regionale ha tenuto bloccata una coda per ore e si è svuotata quando l’allarme è finalmente scattato.
Il divario tra event time e processing time si chiama skew. In un sistema tranquillo lo skew è di secondi. In un sistema reale, con client mobile, retry, ribilanciamento delle partition e l’occasionale incidente del broker a metà giornata, lo skew ha una coda lunga. Il 99esimo percentile è di minuti. Il 99,9esimo a volte è di ore.
Un’aggregazione a finestra deve scegliere uno di questi due tempi su cui fare il bucketing, e la scelta cambia tutto.
Perché le finestre in processing time sono tentatrici e sbagliate
Fare windowing per processing time è il default facile. Ogni evento finisce nel bucket dell’orologio da parete nel momento in cui il processor lo vede. Non ci sono late data, per definizione: una finestra si chiude quando l’orologio supera la sua fine, e qualsiasi evento che arriva dopo finisce semplicemente in una finestra successiva. L’implementazione è di poche righe.
È anche sbagliato per qualunque aggregazione che debba essere business-correct, e la ragione è lo skew. Immagina una dashboard “click al minuto”. Alle 10:00:30 un nodo CDN regionale ha un singhiozzo e il 20% degli eventi viene ritardato di 90 secondi. Arrivano al processor tra le 10:02:00 e le 10:02:15. In una finestra in processing time, quegli eventi vengono ora contati nel bucket delle 10:02, anche se sono accaduti alle 10:00. Il bucket delle 10:00 sotto-conta. Il bucket delle 10:02 sopra-conta. Nessuno dei due rispecchia ciò che gli utenti hanno realmente fatto.
Per una dashboard che è grosso modo indicativa, potresti tollerarlo. Per il billing, la fraud detection, le soglie di anomalia, gli outcome degli A/B test, o qualunque cosa tu confronti con un aggregato di database, non puoi. Gli aggregati devono essere in event time, perché l’event time è l’unico tempo che ha un significato fuori dal sistema di streaming.
Le finestre in event time e il problema della completezza
Fai windowing per event time e il bucketing è corretto: il click delle 10:00:00 sta sempre nel bucket delle 10:00, non importa quando arriva. Il problema si sposta altrove. Quando puoi emettere il risultato per il bucket delle 10:00?
Se aspetti per sempre, puoi essere sicuro che nessun evento in ritardo cambierà mai la risposta, ma non emetti mai. Se emetti alle 10:01 nel momento in cui l’orologio da parete esce dalla finestra, potresti avere ragione e potresti perdere il 20% degli eventi a causa di quel singhiozzo del CDN. Ti serve una nozione di “abbiamo probabilmente visto tutti gli eventi con event time fino a T” in modo che le finestre fino a T possano essere chiuse con fiducia.
Quella nozione è un watermark.
Watermark
Un watermark è la stima dello streaming engine di “il clock dell’event time è avanzato almeno fino a T”. Non è una garanzia. È un’euristica, calcolata dagli eventi che attraversano la pipeline, che dice: in base a ciò che abbiamo visto, crediamo che da ora in poi non arriverà nessun evento con event time precedente a T. Le finestre che si sono chiuse prima di T possono essere chiuse ed emesse in sicurezza.
Il trade-off è diretto. Un watermark più stretto, che avanza in modo aggressivo vicino all’event time più recente visto, ti permette di emettere risultati rapidamente. Significa anche che più eventi davvero in ritardo cadono fuori dal watermark nel momento in cui arrivano, e devono essere o droppati o instradati su un percorso speciale. Un watermark più lasco, che resta indietro di una certa tolleranza configurata, cattura più eventi in ritardo dentro le finestre dove appartengono, al costo di una latenza di emissione più alta.
Non c’è una risposta giusta universale. Un segnale di fraud in tempo reale ha bisogno di risultati in pochi secondi ed è disposto a droppare qualche evento in ritardo. Un aggregato di billing notturno può restare indietro di un’ora e catturare quasi tutto. La policy del watermark è una scelta per pipeline, e quella giusta dipende dal consumer.
I principali engine calcolano i watermark in modo diverso. Flink lascia che le source emettano watermark per partition che il runtime combina: il watermark effettivo dell’operator è il minimo dei watermark dei suoi input, quindi una partition lenta tiene indietro tutto il job, il che è corretto ma a volte doloroso. Spark Structured Streaming usa withWatermark("event_time", "10 minutes"), una policy per stream che dice “il watermark resta indietro di 10 minuti rispetto al massimo event time visto”. Il corso PySpark nel Modulo 9 copre in dettaglio i pattern specifici di Spark. Kafka Streams mantiene un timestamp per task derivato dai record che ogni task ha consumato, e lo usa per pilotare l’emissione delle finestre e la pulizia dello stato.
I meccanismi differiscono. Il contratto è lo stesso: il watermark è la migliore congettura dell’engine su “abbiamo probabilmente visto tutto fino a qui”, e tu, lo sviluppatore, configuri quanto aggressiva è quella congettura.
Eventi in ritardo e cosa farne
Per quanto sia configurato il watermark, alcuni eventi arriveranno dopo che la loro finestra si è chiusa. Ci sono tre modi per gestirli, e di solito si combinano.
Droppali. L’opzione più semplice. L’engine logga che un evento in ritardo è stato droppato, opzionalmente espone una metrica, e va avanti. Questo è corretto quando il caso d’uso può tollerare una piccola coda di perdita (monitoring in tempo reale, telemetria che ha già una riconciliazione batch alle spalle) e scorretto quando non può.
Side output. La maggior parte degli engine ti lascia instradare gli eventi in ritardo verso uno stream separato invece di dropparli. Flink lo chiama “side output”. Spark lo espone tramite un sink custom. Lo stream dei late può essere persistito, raggruppato in batch, e processato da una pipeline più lenta che non deve essere real-time. Questo è il pattern giusto quando gli eventi in ritardo sono rari ma importanti: un evento di billing arrivato con tre ore di ritardo deve comunque atterrare nella fattura giusta, solo non nella dashboard real-time.
Allowed lateness. La finestra resta aperta più a lungo di quanto il watermark suggerirebbe. Quando arriva un evento in ritardo, si unisce allo stato della finestra, e l’engine emette un risultato aggiornato. Il downstream deve essere pronto a gestire aggiornamenti invece di una singola emissione finale, e lo state store deve mantenere i dati della finestra per la durata della allowed lateness, quindi questa opzione è la più costosa in memoria e la più esigente per i consumer downstream.
La scelta cade da due domande. Quanto è importante che gli eventi in ritardo siano riflessi? Quanto è disposto il sink downstream ad aggiornare risultati che ha già ricevuto? Se le risposte sono “molto” e “sì”, allowed lateness. Se “molto” e “no”, side output e un job batch di riconciliazione. Se “non molto”, drop e metrica.
Un esempio svolto
Uno stream di click con una finestra tumbling di 1 minuto in event time. Il watermark è configurato per restare indietro di 30 secondi rispetto al massimo event time visto. La allowed lateness è zero: gli eventi in ritardo vanno in un side output.
Un click accade all’event time 10:00:15 e arriva al processor al processing time 10:00:18. Il processor lo piazza nella finestra delle 10:00. Il watermark, che è a 10:00:15 meno 30 secondi, sta a 09:59:45. La finestra delle 10:00 non si chiude ancora.
Gli eventi del resto del minuto delle 10:00 arrivano normalmente. Il massimo event time visto raggiunge 10:00:58 al processing time 10:01:01. Il watermark è ora a 10:00:28. Ancora dentro la finestra delle 10:00.
Per il processing time 10:01:32, sono stati visti eventi con event time fino a 10:01:02. Il watermark è a 10:00:32. La fine (esclusiva) della finestra delle 10:00 è 10:01:00, quindi il watermark l’ha ora superata. La finestra emette il suo risultato e si chiude.
Un click che è accaduto all’event time 10:00:45, ma è stato bufferizzato su un client mobile e arriva al processing time 10:01:35, atterra dopo che il watermark ha superato 10:00:32 di tre secondi. La finestra delle 10:00 si è chiusa. L’evento viene instradato verso il late side output.
Un click con event time 10:00:50 che arriva al processing time 10:06:00, dopo un disservizio della rete mobile di cinque minuti, atterra anch’esso nel side output. A quel punto il watermark è ben oltre le 10:00. La dashboard non vede mai questo click. Il processor del side output, che gira a una cadenza più lenta, alla fine lo integrerà in un totale riconciliato.
sequenceDiagram
participant Source
participant Processor
participant Window10 as Window 10:00 to 10:01
participant SideOut as Late side output
Source->>Processor: click(et=10:00:15) at pt=10:00:18
Processor->>Window10: add to state
Source->>Processor: click(et=10:00:58) at pt=10:01:01
Processor->>Window10: add to state
Note over Processor: watermark = max_et - 30s = 10:00:28
Source->>Processor: click(et=10:01:02) at pt=10:01:32
Note over Processor: watermark = 10:00:32 > 10:01:00? not yet
Source->>Processor: click(et=10:01:08) at pt=10:01:38
Note over Processor: watermark = 10:00:38, still less than 10:01:00
Source->>Processor: click(et=10:01:31) at pt=10:02:01
Note over Processor: watermark = 10:01:01 > 10:01:00, close 10:00 window
Processor->>Window10: emit result, close
Source->>Processor: click(et=10:00:45) at pt=10:01:35
Note over Processor: 10:00 window already closed
Processor->>SideOut: late event(et=10:00:45)
La disciplina
Due regole coprono la maggior parte dei guai.
La prima: includi sempre il timestamp dell’evento nel messaggio. Ogni producer, in ogni servizio, incorpora il tempo in cui l’evento è avvenuto. Non quando il producer lo invia. Non quando il broker lo riceve. Quando l’utente ha cliccato, il sensore ha letto, il trade è avvenuto. Se controlli il producer, è un cambio di una riga. Se stai leggendo da una source di terze parti che non include l’event time, tratta il timestamp di ingestione come event time e documenta la limitazione; il downstream vedrà qualunque skew aveva la source.
La seconda: decidi la policy del watermark prima di costruire la dashboard, non dopo. La scelta del watermark è una questione di prodotto. Quanto velocemente devono apparire i risultati? Quanto è tollerante il consumer agli aggiornamenti in ritardo? Che frazione di eventi arriva in ritardo, e quanto in ritardo? Le risposte determinano se droppi, fai side output, o aggiorni, e sono più facili da porre all’inizio che da retro-adattare quando arriva la prima lamentela di inconsistenza.
La lezione su exactly-once è la prossima, e condivide un pezzo di DNA con questa: lo streaming è onesto sui modi di fallimento che il batch nasconde. I watermark espongono il costo di prendere il tempo sul serio. Exactly-once espone il costo di prendere i duplicati sul serio. Entrambi sono lavoro. Entrambi sono inevitabili in qualunque pipeline le cui risposte vengano usate dalle persone per prendere decisioni.
Citazioni e letture di approfondimento
- Tyler Akidau, Slava Chernyak, Reuven Lax, “Streaming Systems” (O’Reilly, 2018). Il testo di riferimento su event time, watermark, e il modello concettuale dietro Apache Beam. Consultato 2026-05-01.
- Documentazione di Apache Flink, “Event Time and Watermarks”,
https://nightlies.apache.org/flink/flink-docs-stable/docs/concepts/time/(consultato 2026-05-01). - Documentazione di Spark, “Structured Streaming Programming Guide: Handling Late Data and Watermarking”,
https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html(consultato 2026-05-01). - Tyler Akidau, “The world beyond batch: Streaming 101” e “Streaming 102”, O’Reilly Radar (consultato 2026-05-01). I due saggi che hanno introdotto il vocabolario di event time e watermark ora usato in tutto il settore.