Retrouvez-nous le 14 mai au Google Cloud Summit à l'Accor Arena - Paris !

logo saagie red

Qu’est-ce que Spark et comment l’utiliser pour la programmation fonctionnelle ?

Spark est l’une des technologies web les plus en vue dans le domaine du big data. Développé initialement à Berkeley, Spark est maintenant un projet de la fondation Apache. Sa première version a été présentée en mai 2014, et depuis, il est devenu un outil incontournable pour qui veut analyser des petabytes de données.

Qu'est-ce que Apache Spark ?

Spark est un framework de calcul distribué. Ce n’est donc pas un langage de programmation, c’est un ensemble d’outils informatiques écrits initialement en Scala, mais maintenant disponibles également en Python, R, Java et SQL.

Le calcul distribué consiste en la réalisation d’opérations sur des données qui ne sont pas stockées en un seul endroit, mais éparpillées au sein d’un réseau de différentes machines (un « cluster »). Un framework de calcul distribué comme Spark veille à la bonne mise en œuvre et l’orchestration de ces calculs, permettant ainsi d’assurer la cohérence des résultats. 

Attention, Spark ne gère pas les données lui-même : il s’occupe uniquement du calcul en s’appuyant sur une infrastructure de données distribuées pour gérer leur stockage au sein du cluster. Hadoop est l’infrastructure de données distribuées avec laquelle Spark a été développé, il est cependant possible de compiler Spark avec des alternatives à Hadoop.

Pourquoi le calcul distribué ?

En effet, le calcul distribué pose des problèmes supplémentaires comparé à des opérations menées sur un seul serveur. Alors pourquoi s’embêter avec plusieurs serveurs ?

Au début des années 2000 et avec la démocratisation d’internet, la quantité de données informatiques disponibles a explosé. Avec le matériel d’époque, il était impossible d’analyser toutes ces données sur un seul serveur, pour différentes raisons : 

  • stockage insuffisant, que ce soit en RAM ou sur disque dur ;
  • capacité réseau limitée (souvenez-vous de la vitesse d’internet à cette époque…) ;
  • puissance de calcul insuffisante (analyser les données météorologiques pour prévoir la météo de demain n’a de sens que si le calcul aboutit avant demain…).

Quelques entreprises faisant face à ces défis, les développeurs outils ont alors travaillé sur la création de certains outils pour répondre à ce besoin, tel MapReduce chez Google, qui donnera ensuite naissance à Hadoop.

Aujourd’hui, malgré les progrès colossaux depuis cette époque en matière de puissance de calcul et de capacité de stockage, ces problématiques sont encore d’actualité. Cependant, comme mentionné plus haut, le calcul distribué soulève des problèmes non triviaux. Et avant d’apprendre à utiliser les outils comme Spark, qui ont permis d’apporter des solutions, il est utile d’aborder la programmation fonctionnelle.

La programmation fonctionnelle est un paradigme de programmation au même titre que la programmation orientée objet, par exemple. Les règles de ce paradigme donnent au code des propriétés qui s’avèrent très utiles lorsque l’on veut faire du calcul distribué.

La programmation fonctionnelle pour répondre aux défis du calcul distribué

Commençons par voir quelques-uns des défis soulevés par le calcul distribué : 

  • Risque de panne élevé : en travaillant avec des dizaines ou centaines de serveurs, il est probable d’avoir des erreurs matérielles et/ou réseaux avec l’un des serveurs.
  • Des états différents : chaque serveur possède des données différentes et donc se trouve dans des états différents.
  • La répartition des données parmi les serveurs n’est pas unique, il en existe plusieurs. Par conséquent, cette répartition ne doit pas influencer le résultat final, sinon le calcul est caduc.
  • La programmation fonctionnelle permet de pallier ces problèmes. 

En programmation fonctionnelle, les données sont immuables et le code développé ne possède pas d’état, il consiste uniquement en l’évaluation de fonctions mathématiques.

C’est un style de programmation plutôt abstrait et assez déroutant qui a les conséquences suivantes : 

  • Les données sont immuables, il n’y a pas de variable dont l’état change durant l’exécution.
  • Le résultat d’une fonction est toujours le même pour une combinaison donnée des paramètres d’entrée, c’est-à-dire qu’il n’y a pas d’effet de bord dû par exemple à une variable dont l’état pourrait changer et qui pourrait influencer le comportement de la fonction.
  • Une fonction est considérée au même titre qu’une variable, donc il est possible de passer une fonction comme argument à une autre fonction, ou encore renvoyer une fonction comme résultat d’une fonction.
  • La récursivité est utilisée en lieu et place des boucles de type « for » pour itérer sur des données.

L'intérêt de Spark en big data

L’exécution du programme ne crée pas d’effet de bord, donc exécuter le calcul sur une ligne n’influencera pas le calcul sur une autre ligne. Ainsi, on peut faire tourner les calculs en parallèle, sur des blocs de données séparés. Quelle que soit la répartition de ces blocs, le résultat obtenu pour chaque ligne et chaque bloc est valide. 

Un autre avantage du point précédent est qu’en cas de panne, il suffit de relancer le calcul pour les données stockées uniquement sur le serveur fautif ; les résultats obtenus par les autres serveurs restent valables et exploitables. On ne repart pas de zéro.

Enfin, comme le résultat d’une fonction est toujours le même pour une combinaison d’entrées donnée, il est possible de mettre son résultat en cache en étant certain que ce résultat reste valable, c’est un avantage considérable lorsque l’on traite des petabytes de données.

Voilà les raisons pour lesquelles la programmation fonctionnelle est un type de programmation qui revient à la mode avec l’émergence du big data.

Il est utile de connaître les grands concepts de ce paradigme de programmation avant de se lancer dans l’apprentissage d’outils comme Spark, qui ont adopté l’état d’esprit de la programmation fonctionnelle.

Les concepts de base de Spark

Spark Context

La première chose à faire lorsque l’on veut utiliser Spark est de créer un objet SparkContext, généralement assigné à une variable nommée sc dans le code. 

Cet objet indique à Spark comment accéder au cluster de serveurs que vous souhaitez utiliser. Le cluster peut être un cluster local (pour faire des tests en simulant plusieurs processus parallèles comme s’il s’agissait de serveurs différents) ou bien un cluster distant, réellement composé de plusieurs serveurs.

Resilient Distributed Dataset (RDD)

Il s’agit de la structure de données de base utilisée par Spark. C’est elle qui rend possible la mise en cache de résultats intermédiaires dans la RAM et la réalisation d’opérations en parallèle tout en tolérant de potentielles erreurs sur le cluster, assurant donc la rapidité et la cohérence des opérations.

Il est intéressant de noter qu’un RDD est en lecture seule, donc lorsque vous effectuez des opérations dessus, un nouveau RDD est créé. 

Deux autres structures de données Spark sont basées sur les RDD, il s’agit des DataFrame et des Dataset. On préférera utiliser ces dernières la majorité du temps ; manipuler directement des RDD est rarement nécessaire.

Transformation et actions

On distingue deux types d’opérations avec Spark : les transformations et les actions.

Une transformation crée un nouveau dataset à partir d’un dataset existant. Une transformation n’entraîne pas directement de remontée d’information, les données et les résultats des transformations restent sur leurs serveurs respectifs. 

L’évaluation d’une transformation est dite « paresseuse », c’est-à-dire que la transformation est effectivement réalisée lorsque c’est nécessaire et non pas là où elle est définie dans le code. 

Mais alors, comment faire des calculs si ceux-ci ne sont pas exécutés lorsqu’on les définit dans notre code ? Il faut pour cela utiliser une action !

Une action est une opération permettant à l’utilisateur de récupérer un résultat. Une action est généralement effectuée à la suite de plusieurs transformations et puisque qu’une action génère un résultat, elle déclenche l’évaluation effective de toutes les transformations qui la précèdent.

Les principales opérations

Il est important de comprendre la différence entre une transformation et une action. Si cela vous semble clair, voici une liste des principales transformations et actions dont vous pouvez vous servir dans Spark.

Transformations

map : Applique une fonction à chaque élément de départ et crée un nouveau dataset avec les résultats

filter : Applique une fonction à chaque élément de départ et crée un nouveau dataset contenant uniquement les éléments pour lesquels la fonction a renvoyé True

flatmap : Idem que map, mais la fonction appliquée peut renvoyer plusieurs éléments en sortie pour un élément en entrée (ex. : texte en entrée, liste de mots en sortie)

sample : Crée un dataset en sélectionnant aléatoirement une fraction des éléments de départ

union : Réalise l’union de deux datasets (garde les éléments communs aux deux datasets, en supprimant les doublons). Crée un nouveau dataset avec le résultat de cette union 

distinct : Crée un dataset en omettant les doublons parmi les éléments de départ

groupByKey : Crée un dataset dans lequel les éléments de départ partageant une valeur commune dans une colonne donnée sont regroupés 

Si appliqué à un dataset contenant des éléments (K, V), alors le résultat sera un dataset de la forme (K, seq[V]) où seq[V] est la liste des valeurs, V appartenant aux éléments partageant un même K

reduceByKey : Réalise dans un premier temps la même opération que groupByKey, mais applique ensuite une fonction (définie en argument) à seq[V]

sortByKey : Crée un dataset en triant les éléments de départ

join : Crée un dataset en rassemblant les éléments de deux datasets existants. Les éléments partageant une valeur commune dans une colonne donnée sont regroupés ensemble

Si appliqué aux datasets contenant les éléments (K, V) et (K, W), alors le résultat sera de la forme (K, [V, W])

cogroup : Cette transformation est en quelque sorte la fusion de join puis de groupByKey : si appliqué aux datasets contenant les éléments (K, V) et (K, W), le résultat sera de la forme (K, Seq[V]), Seq[W]), où Seq[V] et Seq[W] sont les listes des valeurs, V et W appartenant aux éléments partageant un même K

cartesian : Crée un dataset en appliquant un produit cartésien aux éléments contenus dans deux datasets existants, formant ainsi toutes les combinaisons possibles de paires entre ces datasets

Actions

reduce : Applique une fonction de réduction à un dataset, réduisant ce dataset à une seule ligne (ex. : la somme des chiffres contenus dans les colonnes du dataset). Cette fonction doit être associative et commutative !

collect : Permet simplement de lancer l’évaluation effective des transformations en amont. Utile après une transformation filter par exemple, si le résultat obtenu est suffisamment petit pour être stocké sur une seule machine 

count : Compte le nombre d’éléments dans le dataset

first : Renvoie le premier élément du dataset

take : Renvoie les n premiers éléments du dataset, n étant un argument

takeSample : Renvoie n éléments sélectionnés aléatoirement dans le dataset

saveAsTextFile : Sauvegarde le dataset sous forme de fichier texte

saveAsSequenceFile : Sauvegarde le dataset sous forme de fichier Hadoop SequenceFile (cette option est plus restrictive quant au dataset qu’il est possible de sauvegarder sous cette forme)

countByKey : Pour chaque valeur distincte contenue dans une colonne donnée du dataset, renvoie au nombre d’éléments partageant cette valeur

Si appliqué à un dataset (K, V), renvoie un résultat de la forme (K, Int) ou Int est un entier égal au nombre d’éléments partageant un même K

foreach : Applique une fonction à chaque élément du dataset, sans créer de nouveau dataset. Cette fonction est généralement utile pour mettre à jour une variable externe à notre dataset, mais dépendant des valeurs contenues dans ce dernier

Vous avez ici un bon aperçu des différentes opérations couramment utilisées dans Spark. Il en existe beaucoup d’autres ; pour plus de détails et en fonction des langages que vous utilisez, vous pouvez vous reporter à la doc Spark. 

Sachez que Saagie vous permet d’intégrer des scripts Spark à vos projets data ; n’hésitez pas à nous contacter pour plus d’informations. 

En intégrant Spark à Saagie, notre plateforme DataOps, les utilisateurs bénéficient d’une plateforme robuste et évolutive pour gérer les projets de traitement des données à grande échelle. Spark offre des fonctionnalités avancées de transformation, d’analyse et de calcul parallèle, permettant ainsi de traiter des volumes massifs de données de manière efficace et rapide.