Aller au contenu

Partie 1 – Structured Streaming : traiter les données qui arrivent

  • 9h20 – 10h30
  • Théorie 35 min · TP 9 35 min
  • Slides 6 à 13

Et si votre tableau de bord se mettait à jour toutes les 5 secondes, sans relancer aucun job ? Hier soir, combien de temps aurait-il fallu pour savoir qu'une panne avait lieu ? Avec un batch toutes les nuits : le lendemain.

t = 1 t = 2 t = 3
┌────────────┐ ┌────────────┐ ┌────────────┐
│ lignes │ │ lignes │ │ lignes │
│ │ │ + nouvelles│ │ + nouvelles│
└────────────┘ │ │ │ + nouvelles│
même requête └────────────┘ └────────────┘
Manhattan : 2 400 même requête même requête
Manhattan : 4 850 Manhattan : 7 300

La table d’entrée est « non bornée » : chaque micro-batch ajoute des lignes, et Spark met le résultat à jour de façon incrémentale, sans tout recalculer. Il garde un état : ici, le total courant par quartier. Analogie : un compteur de supporters au stade. On n’en recompte pas 40 000 à chaque entrée, on ajoute ceux qui passent le portique.

DataFrame statiqueDataFrame dynamique
Créationspark.readspark.readStream
NatureBorné, calculé une foisNon borné, calculé en continu
Testdf.isStreaming == Falsedf.isStreaming == True

C’est le modèle mental de Structured Streaming : la même API DataFrame s’applique aux jeux de données statiques (fichiers déjà présents) et dynamiques (fichiers qui apparaissent au fil du temps).

Le même calcul (courses par arrondissement) écrit deux fois. Dans les deux cas, schema est pris sur les données statiques et zones est le référentiel data/taxi_zone_lookup.csv, lu comme un DataFrame ordinaire : le JSON du flux ne contient que PULocationID, l’arrondissement vient de la jointure.

# Batch, comme au jour 1 (bonus du corrigé TP 9)
schema = spark.read.parquet("data/trips.parquet").schema
zones = spark.read.csv("data/taxi_zone_lookup.csv", header=True, inferSchema=True)
ref = zones.select(F.col("LocationID").alias("PULocationID"), "Borough")
df = spark.read.schema(schema).json("stream/courses")
res = df.join(ref, "PULocationID").groupBy("Borough").count()
res.write.mode("overwrite").parquet("out/res")
# Streaming, jour 3 (TP 9) : seules les lignes marquées ← changent
df = spark.readStream.schema(schema).json("stream/courses") # ← readStream, schéma obligatoire
res = df.join(ref, "PULocationID").groupBy("Borough").count() # identique
q = (res.writeStream # ← writeStream
.outputMode("complete") # ← quoi écrire à chaque micro-batch
.format("console")
.option("checkpointLocation", "out/checkpoints/tp9") # ← la mémoire de la requête
.trigger(processingTime="5 seconds") # ← quand déclencher
.start())

Quatre différences, pas plus :

DifférencePourquoi
readStream / writeStreamLe reste de l’API ne change pas
Schéma obligatoireLe flux est infini : Spark ne peut pas le lire en entier pour deviner les types
outputModeQuoi écrire à chaque micro-batch (détaillé en partie 2)
triggerQuand déclencher un micro-batch
TriggerComportementUsage
processingTime="5 seconds"Un micro-batch toutes les 5 sNos TP en direct
availableNow=TrueTraite tout ce qui est disponible, puis s’arrêteJobs planifiés, nos corrigés reproductibles
realTime="5 minutes" (mode temps réel, Spark 4.1 ; mot-clé PySpark depuis 4.2, Trigger.RealTime en Scala/Java avant)Traitement continu, latence de quelques millisecondes ; la durée est celle d’un run avant redémarrageSources Kafka, mode update, opérations sans état au départ

start() rend la main tout de suite : la requête tourne en arrière-plan. q.awaitTermination(), q.stop(), q.lastProgress, q.recentProgress la pilotent. Le checkpoint est obligatoire dès qu’on sort de la console : c’est la mémoire de la requête (partie 2).

Sources : d’où vient le fluxSinks : où vont les résultats
Fichiers (CSV, JSON, Parquet…) : un dossier surveillé. Nos TPFichiers (Parquet, Delta…) et Kafka
Kafka : la référence en production, rejouableforeachBatch : du code batch libre pour chaque micro-batch
Rate et socket : pour tester uniquementConsole et memory : pour le développement
Une source doit pouvoir être rejouée pour la tolérance aux pannesLe sink décide de la garantie : exactly-once ou at-least-once

Kafka est un journal distribué, partitionné, rejouable ; le connecteur spark-sql-kafka est un package à ajouter (--packages). foreachBatch sert à écrire dans plusieurs destinations, faire un MERGE dans une table ou appeler une API : on l’utilise au TP 11.

L’héritage Flume : agents, sources, channels, sinks, Avro RPC

Section intitulée « L’héritage Flume : agents, sources, channels, sinks, Avro RPC »
logs des serveurs ──► ┌─ Agent Flume 1 ─────────┐ Avro RPC ┌─ Agent Flume 2 ─────────┐ ──► HDFS
│ Source → Channel → Sink │ ───────────► │ Source → Channel → Sink │
└─────────────────────────┘ └─────────────────────────┘
Brique FlumeRôleÉquivalent moderne
SourceReçoit les événements (logs, syslog, HTTP)Producteur Kafka
ChannelTampon entre source et sink (mémoire ou fichier)Topic Kafka
SinkÉcrit vers la destination (HDFS, un autre agent)Consommateur Kafka
Avro RPCSérialisation binaire compacte, avec schéma, entre agentsAvro reste très utilisé : schémas Avro dans Kafka, from_avro / to_avro dans Spark
Spark Streaming (DStreams)Structured Streaming
Point d’entréeStreamingContext(sc, 5)SparkSession, API DataFrame et SQL
ModèleUn flux = une suite de RDD (un par micro-batch)Une table non bornée, optimisée par Catalyst comme le batch
TempsTemps de traitement seulementTemps d’événement, watermark, exactly-once
StatutLegacy : plus aucune mise à jourSpark 4.1 : real-time mode, latence en millisecondes
# À savoir lire dans du code existant, pas à écrire
ssc = StreamingContext(sc, 5)
lignes = ssc.textFileStream("dir")
lignes.count().pprint()
ssc.start()

TP 9 – Votre premier stream (35 min) : le chiffre d’affaires par quartier, en direct, mis à jour à chaque micro-batch.

Pause de 15 minutes ensuite : arrêtez les requêtes (q.stop()) et le simulateur (Ctrl+C). Puis la partie 2 : fenêtres et watermark.

Dépôt des participants : formations2026/formation-spark-ml (branche participants)

Contenu pédagogique © Ambient IT · 140 Avenue Jean Lolive, 93500 Pantin · formation@ambient-it.net · +33 (0)1 48 10 09 46

Organisme de formation enregistré sous le numéro 11 75 54743 75. Cet enregistrement ne vaut pas agrément de l'État.