Modulul 6 a construit modelul de streaming: motoarele (lecția 41), topologiile (lecția 42), state stores (lecția 43). Toată mașinăria aceea era despre cum un stream processor mută și își amintește datele. Lecția asta este despre o problemă pe care mașinăria singură nu o rezolvă, cea care derailează majoritatea primelor încercări de agregare pe ferestre: timpul.
Există două timpuri în orice sistem de streaming și nu sunt același timp. Dacă nu le separi în cap, restul streaming-ului te va trăda în liniște. Bug-ul nu este zgomotos. Dashboard-ul tău nu se prăbușește. Pur și simplu afișează numere care arată rezonabile și sunt subtil greșite, iar tu afli trei luni mai târziu, când finance-ul le reconciliază cu sursa de adevăr și pune întrebări politicoase.
Cele două timpuri
Event time este când s-a întâmplat evenimentul în lume. Un utilizator a apăsat un buton la 10:00:00. Un senzor a citit o temperatură la 14:23:17. O plată a trecut la 02:11:09. Timestamp-ul este o proprietate a evenimentului însuși, setată de dispozitivul sau serviciul care l-a produs, încorporată în payload-ul mesajului.
Processing time este când stream processor-ul a văzut evenimentul. Același click care s-a întâmplat la 10:00:00 ar putea sosi la procesor la 10:00:03 din cauza latenței normale de rețea, la 10:00:30 pentru că broker-ul a avut un mic backlog, la 11:30:00 pentru că telefonul utilizatorului era offline în metrou și SDK-ul a tamponat evenimentele timp de nouăzeci de minute, sau, în cazul cu adevărat patologic, la 02:00 dimineața următoare pentru că o pană regională a ținut o coadă ore în șir și s-a drenat când alerta a sunat în sfârșit.
Decalajul între event time și processing time se numește skew. Într-un sistem calm, skew-ul este de secunde. Într-un sistem real, cu clienți mobili, retry-uri, rebalanțări de partiții și incidentul ocazional de broker la mijlocul zilei, skew-ul are o coadă lungă. Percentila 99 este de minute. Percentila 99.9 este uneori de ore.
O agregare pe ferestre trebuie să aleagă unul dintre aceste timpuri pentru a-l folosi la încadrarea în bucket, iar alegerea schimbă totul.
De ce ferestrele de processing time sunt tentante și greșite
Windowing-ul după processing time este default-ul ușor. Fiecare eveniment este pus într-un bucket după ceasul de pe perete în momentul în care procesorul îl vede. Nu există date târzii, prin definiție: o fereastră se închide când ceasul de pe perete trece de capătul ei, iar orice eveniment care sosește după aceea este pur și simplu pus într-o fereastră ulterioară. Implementarea înseamnă câteva linii.
E și greșit pentru orice agregare corectă din punct de vedere de business, iar motivul este skew-ul. Imaginează-ți un dashboard „click-uri pe minut”. La 10:00:30 un nod CDN regional are un sughiț și 20% din evenimente sunt întârziate cu 90 de secunde. Sosesc la procesor între 10:02:00 și 10:02:15. Într-o fereastră de processing time, evenimentele acelea sunt acum numărate în bucket-ul de 10:02, deși s-au întâmplat în 10:00. Bucket-ul de 10:00 numără sub. Bucket-ul de 10:02 numără peste. Niciunul nu se potrivește cu ce au făcut utilizatorii de fapt.
Pentru un dashboard care este aproximativ indicativ, ai putea tolera asta. Pentru billing, detecție de fraudă, praguri de anomalie, rezultate de A/B test sau orice compari cu un agregat din baza de date, nu poți. Agregatele trebuie să fie în event time, pentru că event time este singurul timp care înseamnă ceva în afara sistemului de streaming.
Ferestre de event time și problema completitudinii
Folosește windowing pe event time și încadrarea în bucket este corectă: click-ul de la 10:00:00 este întotdeauna în bucket-ul de 10:00, indiferent când sosește. Problema se mută în altă parte. Când poți emite rezultatul pentru bucket-ul de 10:00?
Dacă aștepți la nesfârșit, poți fi sigur că niciun eveniment târziu nu va schimba vreodată răspunsul, dar nu emiți niciodată. Dacă emiți la 10:01 în momentul în care ceasul de pe perete iese din fereastră, s-ar putea să ai dreptate și s-ar putea să ratezi 20% din evenimente din cauza acelui sughiț CDN. Ai nevoie de o noțiune de „probabil am văzut toate evenimentele cu event time până la T”, astfel încât ferestrele până la T să poată fi închise cu încredere.
Acea noțiune este un watermark.
Watermark-uri
Un watermark este estimarea motorului de streaming că „ceasul de event time a avansat cel puțin la T.” Nu este o garanție. Este o euristică, calculată din evenimentele care curg prin pipeline, care spune: pe baza a ceea ce am văzut, credem că niciun eveniment cu event time mai devreme de T nu va sosi de acum încolo. Ferestrele care s-au terminat înainte de T pot fi închise și emise în siguranță.
Compromisul este direct. Un watermark strâns, unul care avansează agresiv aproape de cel mai recent event time văzut, îți permite să emiți rezultate rapid. Înseamnă și că mai multe evenimente cu adevărat târzii cad în afara watermark-ului până când sosesc și trebuie fie aruncate, fie rutate către o cale specială. Un watermark larg, unul care rămâne în urmă cu o toleranță configurată, prinde mai multe evenimente târzii în interiorul ferestrelor unde aparțin, cu costul unei latențe mai mari de emitere.
Nu există un răspuns universal corect. Un semnal de fraudă în timp real are nevoie de rezultate în secunde și este dispus să arunce niște evenimente târzii. O agregare nocturnă de billing poate rămâne în urmă cu o oră și să capteze aproape totul. Politica de watermark este o alegere per pipeline, iar cea corectă depinde de consumator.
Motoarele majore calculează watermark-urile diferit. Flink lasă sursele să emită watermark-uri per partiție pe care runtime-ul le combină: watermark-ul efectiv al operatorului este minimul watermark-urilor de intrare, deci o partiție lentă ține tot job-ul în loc, ceea ce este corect, dar uneori dureros. Spark Structured Streaming folosește withWatermark("event_time", "10 minutes"), o politică per stream care spune „watermark-ul rămâne în urmă cu 10 minute față de event time-ul maxim văzut.” Cursul de PySpark din Modulul 9 acoperă pattern-urile specifice Spark în detaliu. Kafka Streams menține un timestamp per task derivat din înregistrările pe care fiecare task le-a consumat și îl folosește pentru a conduce emiterea ferestrelor și curățarea stării.
Mecanica diferă. Contractul este același: watermark-ul este cea mai bună presupunere a motorului că „probabil am văzut totul până aici”, iar tu, dezvoltatorul, configurezi cât de agresivă este acea presupunere.
Evenimente târzii și ce să faci cu ele
Indiferent cum este configurat watermark-ul, unele evenimente vor sosi după ce fereastra lor s-a închis. Există trei moduri de a le trata și de obicei le combini.
Aruncă-le. Cea mai simplă opțiune. Motorul logează că un eveniment târziu a fost aruncat, expune opțional o metrică și merge mai departe. Asta e corect când cazul de utilizare poate tolera o coadă mică de pierderi (monitorizare în timp real, telemetrie care are deja o reconciliere batch în spate) și incorect când nu poate.
Side output. Majoritatea motoarelor îți permit să rutezi evenimentele târzii către un stream separat în loc să le arunci. Flink numește asta „side output.” Spark expune asta printr-un sink personalizat. Stream-ul târziu poate fi persistat, batch-uit și procesat de un pipeline mai lent care nu trebuie să fie în timp real. Acesta este pattern-ul corect când evenimentele târzii sunt rare, dar importante: un eveniment de billing care a sosit cu trei ore întârziere trebuie totuși să aterizeze în factura corectă, doar nu în dashboard-ul în timp real.
Allowed lateness. Fereastra rămâne deschisă mai mult decât ar sugera watermark-ul. Când sosește un eveniment târziu, se alătură stării ferestrei, iar motorul emite un rezultat actualizat. Downstream-ul trebuie să fie pregătit să gestioneze actualizări în loc de o singură emitere finală, iar state store-ul trebuie să țină datele ferestrei în jur pentru durata allowed lateness, deci această opțiune este cea mai scumpă în memorie și cea mai exigentă pentru consumatorii downstream.
Alegerea cade din două întrebări. Cât de important este ca evenimentele târzii să fie reflectate? Cât de dispus este sink-ul downstream să actualizeze rezultate pe care deja le-a primit? Dacă răspunsurile sunt „foarte” și „da”, allowed lateness. Dacă „foarte” și „nu”, side output și un job batch de reconciliere. Dacă „nu foarte”, aruncă și metrică.
Un exemplu lucrat
Un stream de click-uri cu o fereastră tumbling de 1 minut în event time. Watermark-ul este configurat să rămână în urmă cu 30 de secunde față de event time-ul maxim văzut. Allowed lateness este zero: evenimentele târzii merg într-un side output.
Un click se întâmplă la event time 10:00:15 și sosește la procesor la processing time 10:00:18. Procesorul îl plasează în fereastra de 10:00. Watermark-ul, care este la 10:00:15 minus 30 de secunde, este la 09:59:45. Fereastra de 10:00 nu se închide încă.
Evenimentele pentru restul minutului 10:00 sosesc normal. Event time-ul maxim văzut atinge 10:00:58 la processing time 10:01:01. Watermark-ul este acum la 10:00:28. Încă în interiorul ferestrei de 10:00.
Până la processing time 10:01:32, evenimente cu event time până la 10:01:02 au fost văzute. Watermark-ul este la 10:00:32. Capătul ferestrei de 10:00 (exclusiv) este 10:01:00, deci watermark-ul l-a trecut acum. Fereastra emite rezultatul și se închide.
Un click care s-a întâmplat la event time 10:00:45, dar a fost tamponat pe un client mobil și sosește la processing time 10:01:35, aterizează după ce watermark-ul a trecut de 10:00:32 cu trei secunde. Fereastra de 10:00 s-a închis. Evenimentul este rutat către side output-ul de evenimente târzii.
Un click cu event time 10:00:50 care sosește la processing time 10:06:00, după o pană de rețea mobilă de cinci minute, aterizează și el în side output. În acel punct, watermark-ul este mult dincolo de 10:00. Dashboard-ul nu vede niciodată acest click. Procesorul de side output, rulând la o cadență mai lentă, îl va plia într-un final într-un total reconciliat.
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)
Disciplina
Două reguli acoperă cea mai mare parte a problemelor.
Prima: include întotdeauna timestamp-ul evenimentului în mesaj. Fiecare producer, în fiecare serviciu, încorporează timpul când evenimentul s-a produs. Nu când producer-ul îl trimite. Nu când broker-ul îl primește. Când utilizatorul a apăsat, când senzorul a citit, când tranzacția s-a întâmplat. Dacă controlezi producer-ul, asta este o schimbare de o linie. Dacă citești dintr-o sursă terță care nu include event time, tratează timestamp-ul de ingestie ca event time și documentează limitarea; downstream-ul va vedea oricare skew avea sursa.
A doua: decide politica de watermark înainte să construiești dashboard-ul, nu după. Alegerea watermark-ului este o întrebare de produs. Cât de repede trebuie să apară rezultatele? Cât de tolerant este consumatorul la actualizări târzii? Ce fracțiune de evenimente sosesc târziu și cât de târziu? Răspunsurile determină dacă arunci, dai side-output sau actualizezi și sunt mai ușor de pus la început decât de retrofitat când sosește prima plângere de inconsecvență.
Lecția despre exactly-once urmează și împărtășește o bucată de ADN cu asta: streaming-ul este onest cu privire la modurile de eșec pe care batch-ul le ascunde. Watermark-urile expun costul luării timpului în serios. Exactly-once expune costul luării duplicatelor în serios. Ambele sunt muncă. Ambele sunt inevitabile în orice pipeline ale cărui răspunsuri oamenii le folosesc pentru a lua decizii.
Citări și lectură suplimentară
- Tyler Akidau, Slava Chernyak, Reuven Lax, “Streaming Systems” (O’Reilly, 2018). Textul de referință despre event time, watermark-uri și modelul conceptual din spatele Apache Beam. Consultat 2026-05-01.
- Apache Flink documentation, “Event Time and Watermarks”,
https://nightlies.apache.org/flink/flink-docs-stable/docs/concepts/time/(consultat 2026-05-01). - Spark documentation, “Structured Streaming Programming Guide: Handling Late Data and Watermarking”,
https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html(consultat 2026-05-01). - Tyler Akidau, “The world beyond batch: Streaming 101” și “Streaming 102”, O’Reilly Radar (consultat 2026-05-01). Cele două eseuri care au introdus vocabularul de event-time și watermark folosit acum în industrie.