Cours
Avez-vous déjà essayé de déboguer un job Spark qui a soudainement échoué et compris que vous êtes complètement perdu à cause de la profondeur du trou de lapin de Spark ?
Lorsque j'ai travaillé pour la première fois avec Apache Spark, je pensais qu'il suffisait d'écrire quelques transformations PySpark et que Spark se mettrait " magiquement " à l'échelle du cluster. Je me suis trompé. Les performances de Spark dépendent entièrement de la compréhension de ce qui se passe en coulisses.
Ce guide s'adresse à tous ceux qui ne veulent pas traiter Spark comme une boîte noire. Nous verrons comment l'architecture de Spark est conçue, du modèle maître-ouvrier et du flux d'exécution, à sa gestion de la mémoire et à ses mécanismes de tolérance aux pannes.
Si vous souhaitez créer des applications big data rapides, tolérantes aux pannes et efficaces, vous êtes au bon endroit !
Architecture fondamentale d'Apache Spark
Avant que vous n'écriviez votre première ligne de PySpark, Spark a déjà pris certaines décisions architecturales pour vous. Spark n'est pas seulement rapide grâce au calcul en mémoire, mais parce qu'il est construit sur une architecture maître-ouvrier qui évolue et survit au chaos du monde réel, comme les crashs de nœudshes, les problèmes de machine virtuelle Java (JVM) et les volumes de données incohérents.
Décortiquons l'architecture de base de Spark et expliquons pourquoi il est toujours aussi puissant et présent dans les flux de travail big data modernes.
Paradigme du maître-ouvrier
Au cœur de Spark se trouve le modèle maître d'œuvre . Pensez-y comme suit :
- Pilote (maître): C'est le cerveau de Spark. Il exécute votre fonction
main()
, crée le contexte Spark, gère la planification du DAG et indique au cluster ce qu'il doit faire. - Exécutants (travailleurs) : Ce sont les muscles. Ils exécutent vos tâches, gardent les données en mémoire et rendent compte au conducteur.
Cette configuration vous permet de vous concentrer sur la définition des transformations, et Spark décide où et comment les exécuter en parallèle sur les exécuteurs.
Ce que j'apprécie dans cette conception, c'est qu'elle est agnostique en termes de déploiement. Le même code s'exécute, indépendamment de son déploiement sur votre machine locale, dans Kubernetes ou Mesos. Il est donc facile de le développer et de le tester localement, puis de le faire évoluer vers des clusters sans réécrire votre code.
Et voici un autre avantage puissant de la séparation conducteur-travailleur de Spark : Il améliore l'isolation des fautes. Si un nœud de travailleur meurt pendant l'exécution d'une tâche, Spark peut réaffecter cette tâche à un autre travailleur sans faire planter votre application.
Composantes essentielles
Décortiquons ce qui se passe à l'intérieur du pilote et des nœuds.
Architecture Spark. Image de l'auteur.
Le contexte Spark
Lorsque vous appelez SparkContext()
ou utilisez SparkSession.builder.getOrCreate()
, vous ouvrez la porte à toute la magie interne de Spark.
Le contexte Spark :
- Se connecte à votre gestionnaire de cluster
- Attribution des exécuteurs testamentaires
- Suivi de l'état d'avancement des travaux et des plans d'exécution
Spark construit un graphe acyclique dirigé (DAG) des transformations en coulisse. Ce DAG est décomposé en étapes et en tâches, puis exécuté en parallèle.
Le planificateur DAG détermine les tâches qui peuvent être exécutées ensemble, et le planificateur de tâches les affecte aux exécuteurs. Pendant ce temps, le gestionnaire de blocs veille à ce que les données soient mises en cache, mélangées ou rechargées en fonction des besoins.
Cette conception en couches rend Spark incroyablement flexible, car vous pouvez ajuster la mémoire, le stockage et le calcul indépendamment.
Si vous travaillez avec les transformations Spark ou l'ingénierie des fonctionnalités, consultez le cours Feature Engineering with PySpark pour voir cette architecture en action.
Durée d'exécution de l'exécuteur
Les exécutants sont là où le travail est fait.
Chaque exécuteur s'exécute :
- Une ou plusieurs tâches (threaded)
- Un morceau de mémoire pour la mise en cache des données et le brassage des sorties
- Sa propre instance de JVM, isolée des autres
Vous pouvez configurer la quantité de mémoire dont dispose chaque exécuteur, le nombre de cœurs qu'il utilise et s'il doit écrire sur le disque lorsque la mémoire est épuisée.
Mais attention : Si vous n'allouez pas suffisamment de mémoire, vous serez constamment confronté à des erreurs de mémoire. Cependant, vous devez également éviter d'allouer trop de mémoire, car cela gaspille des ressources. Le suivi et l'ajustement sont essentiels ici.
Flux de travail d'exécution : Du code à la grappe
L'écriture du code PySpark est assez simple. Vous filtrez un DataFrame, effectuez une jointure, agrègez quelque chose et lancez l'exécution. Mais derrière cette API épurée, Spark met discrètement en place un moteur d'exécution capable de répartir le travail sur plusieurs nœuds.
Voyons ce qui se passe en coulisses.
Conversion du plan logique en plan physique
Voici ce que la plupart des utilisateurs de Spark ne réalisent pas au premier abord : Lorsque vous écrivez du code PySpark, vous n'exécutez rien immédiatement. Vous élaborez un plan, et le Catalyst Optimizer de Spark prend ce plan et le transforme en une stratégie d'exécution efficace.
Il fonctionne en quatre phases :
- Analyse : Spark résout les noms de colonnes, les types de données et les références aux tableaux, en s'assurant que tout est valide.
- Optimisation logique : C'est ici que Spark applique des règles telles que le pushdown des prédicats et le constant folding. Il optimise les filtres et combine les projections.
- Aménagement du territoire : Spark prend en compte plusieurs stratégies d'exécution et choisit la plus efficace (en fonction de la taille des données, du partitionnement, etc.)
- Génération de codes : Enfin, il utilise la génération de code par étapes pour produire le bytecode de la JVM.
Optimiseur de catalyseur de Spark. Image par Databricks.
Cette chaîne de .select()
, .join()
et .groupBy()
ne se déroule donc pas simplement ligne par ligne. Il est analysé, optimisé et compilé en quelque chose qui fonctionne rapidement sur un cluster.
Consultez l'aide-mémoire PySpark si vous voulez une aide-mémoire pour les commandes PySpark les plus utiles.
Planificateur DAG et création d'étapes
Lorsque le plan est terminé, l'ordonnanceur DAG prend le relais.
Il décompose le travail en étapes basées sur les frontières de brassage, où Spark décide de ce qui se passe séquentiellement et de ce qui peut être exécuté en parallèle.
Il existe deux grands types d'étapes :
- ShuffleMapStage : Cela implique un brassage, qui est généralement causé par des transformations importantes telles que
groupBy()
oujoin()
. Les données sont ensuite divisées et envoyées sur le réseau. Ce type d'étape est nécessaire pour calculer l'étape de résultat. - Étape du résultat : Ces étapes produisent des résultats, comme l'écriture sur le disque ou le renvoi des résultats au pilote.
Une chose essentielle que j'ai apprise est de minimiser les mélanges. Un brassage doit avoir lieu avant la fin d'une étape et est coûteux. Vous devez comprendre où ils se produisent dans votre DAG et si vous pouvez optimiser votre code pour réduire le nombre de mélanges.
Cycle de vie de l'exécution des tâches
Une fois que l'ordonnanceur DAG a créé toutes les étapes, celles-ci peuvent être exécutées sur les différents exécuteurs.
Le cycle de vie de l'exécution de la tâche ressemble à ceci :
- Sérialisation des tâches : Le pilote sérialise les instructions de tâches et les envoie aux exécuteurs.
- Phase d'écriture aléatoire : Spark écrit la sortie partitionnée sur le disque local.
- Phase de recherche : Les exécuteurs de l'étape suivante récupèrent les fichiers de brassage pertinents auprès d'autres exécuteurs de la grappe.
- Désérialisation et exécution : Les exécuteurs désérialisent les données, exécutent votre logique et mettent éventuellement les résultats en cache ou les écrivent.
- Collecte des ordures ménagères : La JVM récupère automatiquement la mémoire qui n'est plus utilisée par les applications Spark. Cette étape est essentielle pour éviter les fuites de mémoire et garantir le bon fonctionnement des applications Spark.
Un petit conseil tiré de ma propre expérience : si votre travail Spark se bloque alors qu'il s'était bien déroulé auparavant, c'est souvent à cause du garbage collection ou des retards de shuffle fetch. Vérifiez toujours votre code et assurez-vous de comprendre l'architecture de Spark afin d'optimiser efficacement ces sujets.
Architecture de gestion de la mémoire
La gestion de la mémoire de Spark est un sujet très complexe et peut vous coûter des heures de débogage si vous ne la comprenez pas.
Voyons donc comment Spark gère la mémoire sous le capot afin que vous en soyez conscient et que vous puissiez éviter des heures de débogage de code lent ou d'erreurs hors mémoire.
Modèle de mémoire unifié
Avant Spark 1.6, la mémoire était strictement divisée entre l'exécution (pour les mélanges et les jointures) et le stockage (pour la mise en cache). Cela a changé avec la version 1.6 de Spark, qui a introduit le modèle de mémoire unifiée.
Dans le modèle de mémoire unifiée, les données sont réparties en trois groupes principaux :
- Mémoire réservée : Une petite quantité de mémoire est utilisée pour les internes de Spark et le système.
- Mémoire Spark : Il est utilisé pour stocker les données d'exécution et pour la mise en cache. Il est partagé de manière dynamique. Si votre travail a besoin de plus de mémoire pour les mélanges et de moins pour la mise en cache (ou vice versa), Spark s'adapte.
- Mémoire de l'utilisateur : Espace pour les structures de données définies par l'utilisateur et nécessaires à l'exécution du code utilisateur au sein des applications Spark.
Le pool de mémoire de Spark est lui-même divisé en deux pools :
- Mémoire de l'exécuteur : Stocke les données temporaires nécessaires pendant les étapes des tâches de traitement (par exemple, les mélanges, les jointures, les agrégations, ...).
- Pool de mémoire de stockage : Utilisé pour la mise en cache des données et le stockage des structures de données internes.
Cette élasticité permet à Spark d'être plus flexible avec des volumes de données imprévisibles.
Toutefois, cela signifie aussi que vous perdez un peu le contrôle lorsque vous ne savez pas ce qui se passe. Par exemple, si vous cache()
un DataFrame de grande taille mais que vous avez également des agrégations coûteuses dans la même étape, Spark pourrait évincer vos données mises en cache pour faire de la place pour le mélange.
Stockage hors tas et en colonnes
Dans le stockage hors tas et en colonnes de Spark, c'est le moteur Tungsten qui entre en jeu.
Tungsten a introduit plusieurs optimisations qui ont permis d'améliorer les performances de Spark :
- Gestion de la mémoire en dehors du tas : Spark stocke désormais certaines données en dehors du tas de la JVM, ce qui réduit la surcharge liée au ramassage des ordures et rend la gestion de la mémoire plus prévisible.
- Stockage au format binaire : Les données sont stockées sous une forme binaire compacte, adaptée à la mémoire cache, ce qui améliore l'utilisation de l'unité centrale et permet une exécution vectorisée.
- Algorithmes tenant compte de la mémoire cache : Spark peut désormais utiliser plus efficacement les caches du processeur, en évitant les lectures inutiles à partir de la RAM ou du disque.
Et si vous travaillez avec des DataFrame, vous utilisez déjà ces optimisations sous le capot. C'est l'une des raisons pour lesquelles je pousse les gens à utiliser les DataFrame et les API SQL plutôt que les RDD bruts. Vous bénéficiez de toute la puissance de Catalyst et de Tungsten sans aucun réglage supplémentaire.
Si vous travaillez avec des pipelines de nettoyage de données, vous verrez cela en action dans Cleaning Data with PySpark.
Mécanismes de tolérance aux pannes
Si vous travaillez avec des systèmes distribués, vous êtes sûr d'une chose : Ils échouent. Les nœuds se bloquent. Des erreurs de réseau se produisent. Les exécuteurs manquent de mémoire et s'arrêtent.
Mais Spark est conçu pour gérer ces problèmes et garantit que vos emplois réussissent quand même.
Plongeons plus profondément dans la façon dont Spark s'assure que vos travaux réussissent toujours, même si certaines instabilités se produisent.
Le cursus RDD
Les ensembles de données distribués résilients (RDD) constituent la structure de données fondamentale de Spark. Et ce n'est pas pour rien qu'on les appelle résilients.
Spark utilise le lignage pour s'assurer que chaque RDD peut être recalculé en cas de défaillance d'un nœud et de perte de données.
Ainsi, lorsqu'un nœud tombe en panne, Spark recalcule simplement les données perdues à l'aide du graphe de lignage.
Voici comment cela fonctionne en pratique :
- Les dépendances étroites (comme
map()
oufilter()
) : Spark n'a besoin que de la partition perdue pour recalculer. - Les grandes dépendances (telles que
groupBy()
oujoin()
) : Spark peut avoir besoin de récupérer des données sur plusieurs partitions, car il peut avoir besoin de la sortie de plusieurs étapes.
Lineage évite d'avoir à gérer manuellement les défaillances. Cependant, si votre graphe de lignée devient trop long, car il peut contenir des centaines de transformations, le recalcul des données perdues devient coûteux. C'est là que le point de contrôle entre en jeu.
Points de contrôle et journaux en avance sur l'écriture
Lorsque vous rencontrez des flux de travail complexes ou des tâches en continu, Spark ne peut pas dépendre uniquement du lignage. C'est là que le point de contrôle entre en jeu.
Vous pouvez appeler rdd.checkpoint()
pour conserver l'état actuel du RDD dans un emplacement de stockage fiable (comme HDFS).
Spark tronque ensuite la lignée. En cas d'erreur, il recharge directement les données au lieu de les recalculer.
Dans le cadre de la diffusion en continu structurée, Spark utilise également des journaux en avance sur l'écriture (WAL) pour s'assurer que les données ne sont pas perdues en cours de route.
C'est ce qui le rend si stable :
- Récepteurs fiables : Ils écrivent les données entrantes dans des journaux avant de les traiter.
- Les battements de cœur de l'exécuteur : Ces signaux réguliers confirment que les exécuteurs sont vivants et en bonne santé.
- Répertoires de points de contrôle : Pour les travaux en continu, ils conservent les décalages, les métadonnées et l'état de la sortie afin que vous puissiez reprendre là où vous vous êtes arrêté.
Le point de contrôle est facultatif pour les travaux de traitement par lots, mais obligatoire pour les pipelines de diffusion en continu.
Supposons qu'un travail Spark ait échoué après 10 heures d'exécution, mais que vous puissiez reprendre là où vous vous étiez arrêté, grâce aux points de contrôle et aux WAL.
Caractéristiques architecturales avancées
À ce stade, vous avez vu comment Spark traite les jobs et gère la mémoire et les échecs.
Dans cette section, nous allons nous pencher sur certaines des améliorations architecturales avancées qui rendent Spark plus dynamique, plus en temps réel et plus adaptable.
Exécution adaptative des requêtes (AQE)
L'AQE est introduit dans Spark 3.0 et améliore les performances des requêtes en ajustant dynamiquement les plans d'exécution au moment de l'exécution en fonction des statistiques collectées pendant l'exécution.
Les caractéristiques de l'AQE sont les suivantes
- Changez dynamiquement de stratégie de jonction : Si votre jointure de diffusion ne tient pas dans la mémoire, l'AQE passe à une jointure de tri et de fusion.
- Coalesce shuffle partitions : Fusionner les petites partitions de brassage en partitions plus grandes, ce qui réduit la charge de travail.
- Traiter les données asymétriques : L'AQE peut diviser les partitions asymétriques pour équilibrer le temps d'exécution.
Cette fonction change la donne, car elle permet d'adapter en temps réel des travaux qui nécessitaient auparavant des réglages manuels et des essais-erreurs.
Veillez simplement à l'activer explicitement via la configuration (spark.sql.adaptive.enabled = true
). Et si vous utilisez Spark 3.0+, il n'y a aucune raison de ne pas le faire.
Architecture de flux structuré
Structured Streaming reprend le moteur de Spark et l'étend au domaine du temps réel, sans vous obliger à apprendre une toute nouvelle API.
En coulisses, il applique toujours le micro-batching. Mais il est maniable :
- Gestion des compensations : Spark suit exactement les données qui ont été lues depuis votre source (Kafka, socket, fichier, etc.). Cela permet d'obtenir de solides garanties d'intégrité lorsque la configuration est correcte.
- Filigrane : Avec les agrégations basées sur le temps, Spark utilise des filigranes pour décider quand les données tardives sont trop tardives pour être incluses. Ceci est essentiel pour le traitement des événements.
- Magasins d'État : Lorsque vous effectuez des agrégations fenêtrées ou des jointures en continu, Spark maintient l'état à travers les micro-lots. Cet état est stocké sur le disque et fait l'objet d'un point de contrôle afin d'éviter toute perte de données.
Ce qui est frappant ici, c'est que la diffusion en continu ressemble à la mise en lots. Vous écrivez un groupBy()
ou un filter()
et Spark s'occupe de tout le reste, ce qui rend l'analyse en continu accessible sans chaîne d'outils spécialisée.
Architecture de sécurité
Si vous utilisez Spark en production, en particulier dans les domaines de la finance, de la santé ou d'autres secteurs d'activité similaires, vous devez savoir comment Spark gère l'authentification, le chiffrement et l'auditabilité.
Nous allons donc nous pencher plus en détail sur ces sujets et sur la manière dont Spark s'en occupe.
Authentification et cryptage
Spark possède de nombreuses fonctions de sécurité que vous devez d'abord activer. Mais une fois activé, Spark offre une solide boîte à outils pour la communication et l'authentification sécurisées :
- Authentification (SASL) : Spark utilise la couche d'authentification et de sécurité simple (SASL) pour vérifier que seuls les utilisateurs et les services autorisés peuvent soumettre des travaux ou se connecter au cluster.
- Chiffrement en transit (AES-GCM, SSL/TLS) : Spark chiffre les communications entre les nœuds à l'aide d'AES-GCM (chiffrement authentifié) ou de TLS. Les données relatives aux travaux sont ainsi protégées contre le reniflage, ce qui est particulièrement important dans les environnements multi-tenant ou cloud.
- Intégration de Kerberos : Si vous travaillez sur Hadoop/YARN, Spark s'intègre à Kerberos pour une authentification sécurisée des utilisateurs. Vos emplois Spark sont ainsi directement liés aux systèmes de gestion de l'identité et de l'accès de l'entreprise.
- Contrôle d'accès à l'interface utilisateur : L'interface Web de Spark peut laisser filtrer des informations sensibles (comme les journaux, les chemins d'entrée, les requêtes SQL). Définissez donc
spark.acls.enable=true
etspark.ui.view.acls
etspark.ui.view.acls.groups
pour la restreindre.
Vous pouvez vérifier toutes les caractéristiques de sécurité dans la documentation officielle de Spark. Consultez-le et assurez-vous d'activer les fonctionnalités dont vous avez besoin pour sécuriser vos applications Spark.
Audit et conformité
Il est également essentiel d'enregistrer qui a fait quoi et quand.
Spark prend en charge :
- Enregistrement des événements : Lorsqu'il est activé (
spark.eventLog.enabled=true
), Spark enregistre chaque tâche, étape et événement de tâche sur le disque. Vous pouvez utiliser ces journaux pour rejouer l'historique des tâches ou répondre à des exigences d'audit. - Contrôle d'accès basé sur les rôles (RBAC) : Spark ne fournit pas de RBAC, mais si vous utilisez Spark par le biais d'une plateforme comme Databricks, EMR ou OpenShift, vous obtiendrez généralement un RBAC au niveau de la couche d'infrastructure. Spark soumet les travaux à l'aide d'une identité définie, qui contrôle l'accès aux données et aux ressources de calcul.
- Masquage des données et contrôle d'accès à la source : Spark lit à partir de nombreuses sources(Parquet, Delta Lake, Hive, etc.), et votre contrôle d'accès doit être appliqué à cet endroit.
Modèles d'optimisation des performances
Spark est assez puissant et rapide, et il peut être optimisé pour être encore plus rapide si vous savez où faire les ajustements nécessaires.
Il existe plusieurs domaines dans lesquels vous pouvez essayer d'optimiser pour tirer le meilleur parti de Spark. Nous allons donc nous pencher sur chacun de ces domaines.
Optimisation de la lecture aléatoire
Si Spark a un point faible, c'est bien le brassage. Les mélanges se produisent lorsque les données doivent être déplacées entre les partitions, généralement après des transformations importantes telles que groupByKey()
, distinct()
ou join()
.
Et lorsque les mélanges ne se déroulent pas correctement, vous pouvez obtenir des E/S de disque massives, de longues pauses de collecte de déchets ou des tâches asymétriques qui ne se terminent jamais.
Voici comment vous pouvez améliorer les mélanges :
- Préférez
reduceByKey()
àgroupByKey()
:reduceByKey()
agrège localement avant de mélanger.groupByKey()
envoie tout sur le réseau. - Répartissez intelligemment : Utilisez
.repartition(n)
pour augmenter le parallélisme ou.coalesce(n)
pour le réduire. Ne vous en remettez pas au partitionnement par défaut de Spark. - Utilisez les joints de diffusion (à bon escient) : Si un ensemble de données est suffisamment petit, diffusez-le à tous les travailleurs. Définissez
spark.sql.autoBroadcastJoinThreshold
pour contrôler la limite de taille. - Éviter
collect()
: Évitez-le dans la mesure du possible, car le transfert de données vers le conducteur nuit aux performances.
Lignes directrices pour la configuration de la mémoire
Le réglage de la mémoire de Spark peut être une véritable science, mais vous pouvez utiliser la liste de contrôle ci-dessous pour vous faciliter la tâche :
- Allouez suffisamment de mémoire : Commencez avec au moins 6 Go de mémoire pour le cluster Spark et ajustez en fonction de vos besoins spécifiques.
- Considérez la fraction de mémoire de Spark : Par défaut, 60% est la fraction de mémoire dans Spark. Augmentez-la si vos applications reposent fortement sur des opérations DataFrame/Dataset ou si vous avez besoin de plus de mémoire utilisateur.
- Utilisez le nombre correct de cœurs par exécuteur : En général, 3 à 5 est la meilleure solution. Un nombre insuffisant entraîne une sous-utilisation, tandis qu'un nombre trop élevé conduit à une contestation des tâches.
- Activez l'allocation dynamique (si elle est prise en charge) : Spark peut faire évoluer les exécuteurs vers le haut/bas en fonction de la charge de travail.
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=20
- Ajustez la fraction de stockage : Si vous avez besoin de plus de cache, augmentez la valeur de
spark.memory.storageFraction
. - Surveillez et profilez l'utilisation de la mémoire : Utilisez des outils tels que l'interface utilisateur Spark ou VisualVM pour suivre la consommation de mémoire et repérer les goulets d'étranglement.
L'ajustement de la configuration de la mémoire peut s'avérer très utile. J'ai déjà réduit un travail de 30 minutes à 8 minutes en adaptant la configuration de la mémoire, sans changer une seule ligne de code.
Formules de dimensionnement des grappes
C'est la partie où la plupart des équipes se trompent, parce qu'elles devinent la taille de la grappe au lieu de l'estimer correctement.
Mais vous pouvez faire mieux en utilisant les formules ci-dessous :
- Déterminez le nombre de partitions :
- Calculez le nombre de partitions nécessaires en fonction de la taille de vos données et de la taille de partition souhaitée.
- Une ligne directrice standard est d'avoir une partition par 128 Mo à 256 Mo de données non compressées.
- Formule : Nombre de partitions = arrondir (taille totale des données ÷ taille des partitions).
- Calculez le nombre total de cœurs :
- Le nombre de cœurs nécessaires doit être suffisant pour traiter toutes les partitions en parallèle.
- Formule : Nombre total de cœurs = arrondir (nombre de partitions ÷ partitions par cœur).
- Déterminez la mémoire par exécuteur :
- Calculez la quantité de mémoire dont chaque exécuteur a besoin en fonction de ses cœurs, de la taille de sa partition et de ses frais généraux.
- Formule : Mémoire par exécutant = Mémoire de base × (1 + pourcentage de frais généraux).
- Calculez le nombre d'exécuteurs testamentaires :
- Déterminez le nombre d'exécuteurs en fonction du nombre total de cœurs et de cœurs par exécuteur.
- Formule : Nombre d'exécuteurs = arrondir (nombre total de cœurs ÷ nombre de cœurs par exécuteur).
- Calculer la mémoire totale :
- Calculez la mémoire totale nécessaire pour la grappe en fonction du nombre d'exécuteurs et de la mémoire par exécuteur.
- Formule : Mémoire totale = Nombre d'exécuteurs × Mémoire par exécuteur.
Par exemple :
- Entrée : 500GB de données et une taille de partition de ~128MB
- Partitions : ~4 000 partitions
- Cœurs : 4 000 partitions / 4 partitions par cœur = 1 000
- Mémoire par exécuteur : Supposez 8 Go par exécuteur et 20 % de frais généraux. 8 GB * 1,20 = 9,6 GB
- Exécuteurs testamentaires : 1 000 cœurs / 4 cœurs par exécuteur = 250 exécuteurs
- Mémoire totale : 250 exécuteurs * 9,6 Go = 2 400 Go
Mais n'oubliez pas : Il s'agit uniquement d'une estimation. Vous pouvez l'utiliser comme point de départ et l'optimiser ensuite grâce au profilage.
Tendances architecturales émergentes
Spark existe depuis une dizaine d'années, mais il reste tout à fait d'actualité. Elle évolue plus rapidement que jamais, grâce aux plateformes cloud-natives, à l'accélération GPU et à une intégration plus étroite du ML.
Si vous utilisez Spark aujourd'hui de la même manière qu'il y a trois ans, vous laissez probablement des performances sur le tableau et vous passez à côté de nouvelles fonctionnalités intéressantes.
Jetons un coup d'œil à quelques-uns des plus récents d'entre eux.
Moteur Photon (Databricks)
Si vous travaillez avec Databricks, vous avez probablement déjà travaillé avec Photon et en avez entendu parler.
Si vous souhaitez en savoir plus sur Databricks, je vous recommande le cours Introduction à Databricks.
Photon est le moteur de nouvelle génération de la plateforme Lakehouse de Databricks qui fournit des performances de requête rapides à faible coût. Il est compatible avec les API de Spark, vous n'avez donc pas besoin d'adapter votre code Spark pour l'utiliser.
Il permet d'améliorer considérablement votre code SQL et PySpark.
Photon comprend les fonctionnalités suivantes :
- Exécution vectorisée : Photon traite les données par lots en colonnes, en s'appuyant sur les instructions SIMD (Single Instruction, Multiple Data) du processeur pour effectuer des opérations sur plusieurs valeurs simultanément. Spark traditionnel utilise l'exécution ligne par ligne et s'appuie fortement sur la JVM pour l'allocation de la mémoire et le ramassage des ordures.
- Exécution en C++ (pas de surcharge de la JVM) : Pas de garbage collection Java, qui peut être un goulot d'étranglement dans les gros travaux Spark. La mémoire est gérée avec précision en C++.
- Optimisation des requêtes : Photon s'intègre profondément à l'optimiseur Catalyst de Spark, mais il inclut également ses optimisations lors de l'exécution (comme le filtrage à l'exécution, les chemins de code adaptatifs, les optimisations de jointure et d'agrégation).
- Accélération matérielle : Prise en charge du matériel moderne (comme les GPU NVIDIA, les jeux d'instructions AVX-512 pour les CPU Intel, les processeurs Graviton (ARM) sur AWS).
Spark sans serveur
Serverless est fantastique, car cela signifie que vous n'avez pas à gérer des clusters, à pré-provisionner des ressources, et que vous ne payez que pour le temps d'exécution de Spark.
Et le serverless pour Spark est déjà en direct dans des services comme Databricks Serverless, AWS Glue et GCP Dataproc Serverless.
Et voici pourquoi c'est incroyable :
- Mise à l'échelle automatique : La plateforme adapte le calcul en fonction des besoins réels de votre travail, ce qui signifie que vous n'avez pas besoin de deviner le nombre de nœuds dont vous avez besoin.
- Le rapport coût-efficacité : Vous ne payez que ce que vous utilisez. Plus besoin de payer pour des serveurs inactifs.
- Simplicité : Vous n'avez pas besoin de vous occuper de l'installation, de la configuration ou de la maintenance des clusters, car ces tâches sont prises en charge pour vous.
- Performance : Des temps d'exécution plus rapides sont possibles, car la configuration et l'installation sont optimisées pour vous.
Spark sans serveur est idéal pour les analyses interactives, les tâches ad hoc ou les charges de travail imprévisibles.
Mais attention : les pipelines cohérents et de longue durée peuvent toujours être moins coûteux sur les clusters fixes. Mesurez toujours le coût et la latence.
Intégration de MLflow
Si vous faites de l'apprentissage automatique à grande échelle et que vous visez la mise en production de modèles, Spark seul ne suffit pas. Vous avez besoin des principes MLOps, tels que le cursus des expériences, le versionnage des modèles et la reproductibilité. C'est là que MLflow intervient.
MLflow s'intègre désormais à Spark et apporte une pile complète de MLOPs à vos pipelines.
Vous pouvez le faire :
- Expériences de cursus : Journaliser les paramètres, les métriques et les artefacts des jobs Spark ML à l'aide de
mlflow.log_param()
etmlflow.log_metric()
. - Modèles de versions : Enregistrez les modèles à partir de
pyspark.ml
ousklearn
directement dans le registre de modèles de MLflow. - Servez des modèles : Déployer des modèles entraînés vers des points d'extrémité REST en utilisant le service de modèle de MLflow.
Vous n'avez pas besoin de changer d'outil. Vous continuez à utiliser Spark pour l'entraînement, l'ingénierie des fonctionnalités et le scoring, tout en utilisant MLflow pour les tâches de MLOPs.
Conclusion
Si vous ne connaissez pas bien Spark, il ressemble à une gigantesque boîte noire. Vous écrivez du code PySpark, vous appuyez sur run et vous espérez que cela fonctionne.
Parfois, cela fonctionnait bien pour moi, parfois cela menait à de longues sessions de débogage et à la recherche de ce qui n'allait pas.
Ce n'est que lorsque j'ai commencé à regarder derrière les coulisses que les choses ont pris un sens pour moi. Il m'a fallu un certain temps pour comprendre ce qui se passait.
Voici ce sur quoi je me concentrerais si je devais repartir de zéro :
- Découvrez comment Spark décompose votre code en jobs, étapes et tâches.
- Comprendre la mémoire.
- Attention aux mélanges.
- Commencez modestement et gérez les choses en mode local. Salissez-vous les mains.
C'est précisément ce que nous avons appris dans cet article.
Si vous souhaitez continuer à apprendre, voici quelques ressources pour débutants que je vous recommande :
- Introduction à PySpark: Un excellent point de départ pratique si vous n'êtes pas encore à l'aise.
- Nettoyage de données avec PySpark: Apprenez à nettoyer les données, car les données du monde réel sont toujours désordonnées.
- Les 20 meilleures questions d'entretien avec Spark: Il ne s'agit pas seulement d'interviews, mais d'approfondir votre compréhension.
- Les 4 meilleures certifications Apache Spark en 2025: Si vous souhaitez faire reconnaître vos compétences par le biais de certifications.
Apprendre PySpark à partir de zéro
FAQ
Comment choisir le bon gestionnaire de cluster pour mon déploiement Spark ?
Spark prend en charge plusieurs gestionnaires de clusters (YARN, Mesos, Kubernetes et autonome). Votre choix dépend de l'infrastructure existante, des besoins de partage des ressources et de l'expertise opérationnelle : YARN s'intègre bien aux clusters Hadoop, Kubernetes offre une portabilité conteneurisée et Mesos excelle dans l'isolation multi-tenant.
Qu'est-ce que le service de brassage externe et comment améliore-t-il les performances ?
Le service de mélange externe découple le service de fichiers de mélange des cycles de vie des exécuteurs, ce qui permet une allocation dynamique et réduit la perte de données lors de l'éviction de l'exécuteur. Les fichiers de mélange restent disponibles même après l'arrêt des exécuteurs, ce qui permet d'accélérer les tentatives d'exécution et d'économiser les entrées/sorties de disque en cas de forte charge.
Comment les jointures de diffusion fonctionnent-elles en interne et quand dois-je les utiliser ?
Pour les jointures par diffusion, Spark envoie un petit tableau de consultation à chaque exécuteur afin d'éviter les mélanges complets de données. Utilisez-les lorsqu'un côté de la jointure est inférieur à spark.sql.autoBroadcastJoinThreshold
(10 Mo par défaut), car ils réduisent considérablement les E/S réseau et accélèrent les jointures sur les distributions de clés asymétriques.
Quelles sont les meilleures pratiques pour régler le garbage collection de la JVM dans Spark ?
Surveillez les pauses GC via l'interface utilisateur Spark ou des outils comme VisualVM et préférez le collecteur G1GC pour ses faibles temps de pause. Allouez la mémoire de l'exécuteur avec une marge de manœuvre pour les frais généraux (spark.executor.memoryOverhead
) et réglez -XX:InitiatingHeapOccupancyPercent
pour déclencher le GC plus tôt, afin d'éviter les longues pauses d'arrêt du monde.
Comment puis-je tirer parti de l'accélération GPU pour accélérer les travaux Spark ?
Utilisez l'accélérateur NVIDIA RAPIDS pour Apache Spark afin de décharger de manière transparente les opérations SQL et DataFrame sur les GPU. Il se branche sur le moteur d'exécution de Spark, remplaçant les opérateurs basés sur le CPU par des équivalents accélérés par le GPU et offrant un traitement jusqu'à 10× plus rapide pour les charges de travail adaptées.
Quelle est la différence entre l'allocation statique et dynamique des ressources dans Spark ?
L'allocation statique fixe le nombre d'exécuteurs pour la durée de vie du travail, ce qui offre une certaine prévisibilité au prix de ressources potentiellement inutilisées. L'allocation dynamique permet à Spark de demander ou de libérer des exécuteurs en fonction des tâches en attente et de la charge de travail, ce qui améliore l'utilisation du cluster pour les tâches fluctuantes, ce qui est idéal pour les environnements partagés.
Comment dois-je configurer Spark pour obtenir des performances optimales sur les systèmes de stockage dans le cloud tels que S3 ?
Activez l'accélération du transfert S3, réglez spark.hadoop.fs.s3a.connection.maximum
et utilisez la vue cohérente (S3A v2) pour gérer la cohérence éventuelle. Regroupez les petits fichiers avant de les écrire et tenez compte des auteurs de S3A pour réduire la charge de travail liée à l'utilisation de la liste et améliorer le débit d'écriture.
Comment puis-je sécuriser les communications Spark avec Kerberos et TLS ?
Activez TLS pour RPC (spark.ssl.enabled
) et configurez SASL/Kerberos (spark.authenticate and spark.kerberos.keytab
) pour renforcer l'authentification mutuelle. Stockez les informations d'identification dans une keytab sécurisée, accessible par HDFS, et limitez l'accès à l'interface utilisateur de Spark via des paramètres ACL afin d'empêcher l'exposition de données non autorisées.
Que sont les UDF Pandas et quand sont-ils plus efficaces que les UDF classiques ?
Les UDF Pandas (UDF vectorisés) utilisent Apache Arrow pour échanger des données par lots entre la JVM et Python, ce qui réduit considérablement les frais généraux de sérialisation. Ils sont plus performants que les UDF traditionnels ligne par ligne pour les opérations numériques complexes, en particulier lors du traitement de grands lots de données en colonnes dans PySpark.
Quels sont les avantages de l'API DataSource V2 par rapport à la V1 pour les sources de données personnalisées ?
DataSource V2 offre une interface plus propre et plus modulaire qui prend en charge les filtres déroulants, l'élagage des partitions et les sources de streaming en mode natif. Il permet un contrôle fin de la lecture/écriture et une meilleure intégration avec l'optimiseur Catalyst de Spark, ce qui se traduit par des performances plus élevées et une maintenabilité plus facile pour les connecteurs sur mesure.
Je suis un ingénieur cloud avec de solides bases en génie électrique, en apprentissage automatique et en programmation. J'ai commencé ma carrière dans le domaine de la vision par ordinateur, en me concentrant sur la classification des images, avant de passer aux MLOps et aux DataOps. Je suis spécialisé dans la construction de plateformes MLOps, le soutien aux data scientists et la fourniture de solutions basées sur Kubernetes pour rationaliser les flux de travail d'apprentissage automatique.