Module 1 — Architecture Spark : pilote, exécuteurs, partitions
Avant d'écrire une seule ligne de PySpark, il faut voir ce qui tourne où. Un travail Spark n'est pas un script Python qu'on lance plus fort : c'est une architecture à deux étages, avec un chef d'orchestre et une flotte d'ouvriers, et les malentendus les plus coûteux viennent presque tous d'une confusion entre les deux.
Deux composants, deux mémoires
Le pilote (driver) est le processus qui exécute votre programme
Python. Il tient la SparkSession, construit le plan d'exécution, décide
quelles tâches envoyer, et récupère les résultats agrégés. Il ne traite
jamais lui-même les gros volumes : sa mémoire correspond à celle du
portable ou du nœud maître, elle est petite face au jeu de données.
Les exécuteurs (executors) sont les processus JVM qui font le
travail réel. Chacun tient une partie des données en mémoire, exécute les
tâches qu'on lui envoie, et rend son résultat. Ils tournent sur les nœuds
de calcul de la grappe (YARN, Kubernetes, Databricks) et, en mode local
avec local[4], ce sont quatre fils d'un même processus.
La différence pratique tombe dès la première erreur. Un collect() qui
ramène cinq millions de lignes vers le pilote fait exploser la mémoire du
pilote, pas celle des exécuteurs. Un filter mal indexé fait au contraire
travailler les exécuteurs pendant des minutes sans jamais toucher au
pilote. Savoir où tourne quoi guide déjà la moitié du débogage.
Étapes, tâches, partitions
Un travail Spark se décompose en étapes (stages), séparées par un brassage de données (shuffle, voir module 3). À l'intérieur d'une étape, Spark découpe le travail en tâches (tasks), une par partition. Une partition est un morceau du DataFrame, typiquement 128 Mo à 256 Mo, que Spark manipule comme une unité.
C'est ce grain-là qui gouverne le parallélisme. Avec 200 partitions et 20
cœurs d'exécuteurs disponibles, Spark exécute vingt tâches en parallèle,
puis les vingt suivantes, jusqu'à épuiser les 200. Trop peu de partitions
et vous laissez des cœurs oisifs ; trop de partitions et l'ordonnancement
coûte plus que le calcul. Le paramètre par défaut spark.sql.shuffle.partitions
vaut 200 — il est presque toujours à retoucher, voir module 8.
from pyspark.sql import SparkSession
spark = (
SparkSession.builder
.appName("vols-retards")
.master("local[4]")
.config("spark.sql.shuffle.partitions", "16")
.getOrCreate()
)
vols = spark.read.parquet("data/vols.parquet")
print(vols.rdd.getNumPartitions()) # nombre de partitions à la lecture
Quand Spark est en fait disproportionné
Le réflexe de sortir Spark dès qu'on entend « big data » est le premier
piège. Spark paie un coût fixe non négligeable : démarrage de la JVM,
sérialisation Java-Python (pour PySpark), planification, brassages. Sur un
jeu qui tient en mémoire, pandas ou polars sont plus rapides d'un
ordre de grandeur, tout en offrant une API plus directe.
Une règle de terrain : en dessous de quelques gigaoctets et si un seul serveur suffit, préférer un outil monomachine. Spark devient légitime quand le jeu ne tient plus sur un nœud, quand la lecture doit paralléliser sur du stockage colonne partitionné, ou quand la chaîne s'inscrit déjà dans un environnement Spark (Databricks, EMR, table Delta). Notre jeu de vols de plusieurs dizaines de millions de lignes coche ces cases ; un extrait d'un an sur une seule compagnie ne les cocherait pas.
En local[*], Spark tourne dans un seul processus JVM, sur votre machine.
C'est parfait pour développer et tester, mais les mesures de performance
n'y valent presque rien : pas de réseau, pas de vraie sérialisation
inter-nœuds, un ramasse-miettes qui n'a rien à voir. Ne concluez pas d'une
mesure locale qu'un travail « ira vite en production ».
Le petit tour d'une exécution
Quand vous appelez vols.groupBy("compagnie").count().show(), il ne se
passe rien tant que show() n'est pas atteint. Le pilote construit alors
un plan logique, l'optimise par Catalyst (module 2), le découpe en étapes
et en tâches, puis expédie ces tâches aux exécuteurs. Chaque exécuteur lit
sa partition de Parquet, applique le groupBy local, renvoie ses agrégats
partiels. Un brassage regroupe ensuite les compagnies sur les mêmes
exécuteurs pour l'agrégat final, et le pilote reçoit le petit résultat à
afficher.
Cette séquence — plan, découpe, envoi, brassage, retour — se répète pour
chaque action. Elle est visible dans l'interface Spark UI, à l'adresse
http://localhost:4040 en mode local. Prendre l'habitude de l'ouvrir dès
le premier travail évite des heures de conjecture ; on y verra le module 8.
En résumé
- Le pilote exécute le programme, planifie et récupère les résultats agrégés ; les exécuteurs manipulent les données par partitions.
- Une partition est l'unité de parallélisme ; chaque tâche traite une partition, et le nombre par défaut de 200 est presque toujours à ajuster.
- Sortir Spark pour un jeu qui tient en mémoire coûte plus qu'il ne
rapporte ;
pandasoupolarsrestent plus directs sous quelques gigaoctets. - Le pilote est petit, les exécuteurs sont gros : un
collect()ramène tout au pilote et fait exploser sa mémoire, jamais celle des exécuteurs.
Module suivant : les trois abstractions historiques de Spark (RDD, DataFrame, Dataset), et pourquoi le DataFrame est aujourd'hui le défaut.