Module 8 — Réglage : partitions, mémoire, brassage des données
Le pipeline tourne, les résultats sont bons, mais l'entraînement prend sept heures là où il devrait en prendre deux. Ce module est celui du réglage — pas le réglage d'hyperparamètres du modèle (module 6), mais celui de Spark lui-même : partitions, mémoire, brassage. Les gains y sont souvent de deux à cinq, sans changer une ligne du modèle.
Nombre de partitions
Deux nombres à distinguer. Le nombre de partitions à la lecture
dépend du fichier : Spark découpe chaque fichier Parquet ou CSV en
blocs, environ 128 Mo par défaut (spark.sql.files.maxPartitionBytes).
Un dossier de 40 Go se lit donc dans 300 à 400 partitions initiales.
Le nombre de partitions après brassage est piloté par
spark.sql.shuffle.partitions, qui vaut 200 par défaut. Ce chiffre
convient à un jeu de 20 Go sur un petit cluster ; il devient absurde
dans les deux directions extrêmes :
- Sur un jeu de 1 Go, 200 partitions font des morceaux de 5 Mo. Le coût de planification écrase le calcul utile. Descendre à 8 ou 16.
- Sur un jeu de 500 Go, 200 partitions font des morceaux de 2,5 Go chacune, souvent trop grosses pour la mémoire d'un exécuteur. Monter à 2 000 ou 4 000.
La règle simple : viser des partitions de 128 Mo à 512 Mo en sortie
de brassage. spark.conf.set("spark.sql.shuffle.partitions", "800") se
règle par travail, sans redémarrer la session.
repartition contre coalesce
Deux fonctions pour changer le nombre de partitions d'un DataFrame, avec des coûts très différents :
repartition(n)déclenche un brassage complet : chaque ligne est redistribuée uniformément surnpartitions. Coûteux, mais produit des partitions équilibrées. Sert pour augmenter le nombre de partitions, ou pour équilibrer une répartition biaisée.coalesce(n)fusionne les partitions existantes sans brassage, simplement en combinant plusieurs partitions en une. Très bon marché, mais ne peut que diminuer le nombre de partitions, et peut produire des tailles très inégales.
Cas d'usage classiques :
# Écriture finale d'un rapport agrégé : 12 lignes réparties sur 200 partitions
resultat.coalesce(1).write.parquet("resultat.parquet") # un seul fichier
# Répartition avant un algorithme dont on veut équilibrer la charge
donnees_equilibrees = donnees.repartition(64, "compagnie")
repartition(n, col) groupe par la colonne en n partitions selon un
hachage — utile pour joindre ensuite sur cette colonne sans nouveau
brassage.
L'asymétrie des clés
Le cas qui coûte le plus cher n'est pas d'avoir trop peu ou trop de
partitions : c'est d'avoir des partitions très inégales, autrement
dit une asymétrie (data skew). Un groupBy("compagnie") sur un
jeu où Air France pèse 40 % des vols produit une partition Air France
énorme, cinq partitions moyennes, et cent partitions minuscules. Les
exécuteurs traitant les petites partitions terminent en quelques
secondes ; celui qui a Air France travaille pendant deux heures. Le
travail entier attend cet exécuteur, gaspillant tout le reste de la
grappe.
Trois approches, à empiler si nécessaire :
- Activer l'exécution adaptative des requêtes (AQE, activée par
défaut depuis Spark 3.2), qui repère les partitions asymétriques et
les scinde automatiquement pendant l'exécution :
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true"). - Utiliser un broadcast join quand la table de droite d'une
jointure tient en mémoire d'un exécuteur (moins de quelques
centaines de Mo) :
F.broadcast(petite_table). Aucun brassage sur la grosse table. - En dernier recours, saler manuellement la clé : ajouter un suffixe aléatoire pour disperser les lignes du géant sur plusieurs partitions, puis regrouper après le calcul.
Mémoire des exécuteurs
Deux paramètres pilotent la mémoire d'un exécuteur JVM :
spark.executor.memory (mémoire heap) et spark.executor.memoryOverhead
(hors heap, pour le code natif et Python en PySpark). En PySpark, la
mémoire consommée par Python vit dans l'overhead — la sous-estimer
provoque des morts d'exécuteurs avec le message tristement célèbre
ExecutorLostFailure : Container killed by YARN for exceeding memory limits.
Règles utiles :
- Un exécuteur de 4 à 8 Go tourne mieux que trois exécuteurs de 32 Go : moins de pression sur le ramasse-miettes, meilleure parallélisation.
spark.executor.corestypiquement entre 2 et 5. Au-delà, la contention Java sur les allocations mémoire annule le gain.- En PySpark, prévoir 15 à 20 % de mémoire supplémentaire en overhead.
Cache, oui mais avec discernement
Rappel du module 3 : cache() ne se justifie que si le DataFrame est
relu au moins deux fois. Le cache lui-même consomme de la mémoire
d'exécuteur ; y stocker 40 Go pour n'en relire qu'une fois 500 Mo est
une nuisance. Toujours accompagner un cache() d'un unpersist()
lorsque son usage se termine, sinon la mémoire reste occupée jusqu'à la
fin de la session.
Lire l'interface Spark
L'interface web sur http://localhost:4040 (ou celle du cluster
Databricks / YARN) contient tout ce qu'il faut pour diagnostiquer. Les
onglets utiles au quotidien :
- Jobs : chaque action est un
Job; sa durée totale et sa décomposition en étapes. - Stages : c'est là qu'on voit l'asymétrie. Une étape avec un histogramme de durées de tâches où quelques barres dépassent d'un facteur cent est le symptôme évident.
- SQL / DataFrame : le plan physique de chaque requête, avec les tailles lues et les brassages.
- Storage : ce qui est actuellement en cache et combien de mémoire il consomme.
- Executors : mémoire utilisée par exécuteur, pertes éventuelles, logs d'erreur détaillés.
Un dossier Parquet écrit après un traitement contient parfois un fichier
par partition en sortie, soit 200 fichiers de quelques mégaoctets si
spark.sql.shuffle.partitions=200. La lecture aval devient lente à
cause du surcoût d'ouverture. df.coalesce(20).write.parquet(...) avant
l'écriture ramène à 20 fichiers de taille raisonnable.
En résumé
spark.sql.shuffle.partitions=200est un défaut qu'il faut presque toujours revoir ; viser des partitions de 128 Mo à 512 Mo.repartitionbrasse et équilibre ;coalescefusionne sans brasser mais peut déséquilibrer.- L'asymétrie des clés est le pire ennemi : AQE, broadcast join, salage manuel.
- Petits exécuteurs, cache mesuré, interface Spark comme premier réflexe de diagnostic.
Module suivant : sauvegarder le pipeline entraîné et écrire les résultats pour la consommation en aval.