3. A propos de spark
• Spark est une plateforme de traitement distribué
désignée pour être :
– rapide : in-memory, shell interactif, traitement itératif
– « general-purpose » : un seul cœur (Spark) exposant des
APIs pour manipuler des collections distribuées de
données (RDD)
• Spark et hadoop :
– Spark peut être installé dans un cluster hadoop (mais pas
nécessairement)
– APIs haut niveau
– Spark remédie à plusieurs défauts de Mapreduce
4. Spark Stack
Spark core :
• fonctionnalités basiques de spark : scheduling des tâches, gestion de mémoire, fault
recovery, interaction avec le système de stockage
• fournit plusieurs API de définition et manipulation des RDD : abstraction primaire des
donnés sous forme de collection distribuée d'items
5. Spark Stack
Spark SQL:
• interface SQL qui s'intègre avec le spark core dans les mêmes applications :
• manipulation SQL des données structurées + analyse avancées des rdd avec scala python ou
java
6. Spark Stack
Spark MLIB:
• Classification, régression, clustering, collaborative filtering
• évaluation des modèles
• des primitives bas niveau : « gradient descent algo »
• toutes désignées pour le « scaling out » a travers plusieurs noeuds
7. Spark Stack
Spark Streaming:
• composant spark pour traiter les flux live de données : logfiles .. l'API streaming est proche de
celle de spark’s core RDD API. Ceci facilite la transition pour un développeur qui programme
des traitements sur des données en mémoire ou en disque , à des traitement en temps réel
8. Spark Stack
Spark Graphx:
• bibliothèque pour manipuler des graphes , étend l’ API spark RDD comme streaming et SQL
9. Cluster managers
• Spark peut continuer de « scaler » a travers un cluster de plusieurs milliers de nœuds.
• Il supporte plusieurs clusters managers :
– Hadoop yarn
– Apache mesos
– Standalone scheduler : empty set of machines
– local : tout les processus spark dans un seul thread
Spark Stack
10. Stockage
• Spark peut utiliser tous les systèmes de stockage supportés par l’API de Hadoop:
– HDFS
– S3
– Local FS
– Cassandra
– Hive
– Hbase
• INPUT et OUTPUT Format
– Spark peut accéder en lecture ou en écriture à tous les formats qui utilisent les interfaces InputFormat et
OutputFormat utilisées par Hadoop Mapreduce
11. Architecture maître-esclave
• Driver program:
– Exécution du main de la classe codée par le développeur
– Initialise un « spark context » : connexion à un « computing cluster »
• Spark Executors :
– Des nœuds esclaves gérés par le driver pour la durée de vie d’un « spark context »
– se chargent d’exécuter des tâches en parallèle
15. RDDs
• Un RDD est l’abstraction primaire des données sous SPARK
• Un RDD correspond à une collection non mutable distribuée d’objets, qui peuvent être de n’importe
quel type de données Python, Scala ou Java, ou des “user-defined class”
• Chaque RDD est constitué de plusieurs partitions distribuées sur le cluster.
• Spark distribue automatiquement les RDDs selon le nombre de partitions souhaitées (ou selon un nombre
par défaut)
• Les RDD sont “fault-tolerant”, et bénéficie d’un mécanisme de reconstitution en cas de perte d’une
partition donnée
• Il est possible de créer un RDD :
En chargeant un collection externe de données par le driver programme (depuis hdfs par exemple)
Ou en créant un nouveau RDD en appliquant une transformation sur un ancien RDD
Ou en distribuant une collection d’objets (List, Set..) dans le driver programme
16. RDDs opérations
• Il existe deux types d’opérations qu’on peut appliquer sur un RDD
– Les transformations : permettent de constituer un nouveau RDD (map– filter – groupByKey – join)
– Les actions : permettent de créer un résultat à partir d’un RDD pour le retourner au driver ou l’écrire en système de
stockage. (count – collect – saveAsTextFile)
• Example :
– A partir d’un RDD de CDRs, une transformation de filtrage permet de créer un nouveau RDD contenant uniquement
les types de « 0 »
– Une fois créée, on applique une action « take(3) » sur le RDD, pour récupérer au niveau du driver un tableau de 3
CDRs
18. RDDs operations - transformations
LAZY COMPUTING
• Principe :
– Au moment de la création d’un nouveau RDD, Spark se contente de garder en mémoire l’ensemble des étapes nécessaires pour calculer le RDD.
– Il ne le crée physiquement qu’on moment ou il doit être utilisé dans une action
• Avantages :
– Gestion efficace du disque et de la mémoire
– Optimisation des données à charger ( Exemple de succession des transformations : textFile + filter)
– Optimisation des traitements à effectuer pour exécuter une succession de transformations en les regroupant (exemple d’un succession de d’opération de
mapping)
– Corolaire du dernier point : Code plus léger, moins compact, et facilement compréhensible (par rapport à map reduce)
• Inconvénient :
– Si un RDD doit être utilisé dans n actions au sein d’une même application, il sera calculé n fois différentes
– Solution : Il est possible de persister un RDD en mémoire (ou en disque). Celui-ci sera calculé une seule fois, lors de l’exécution de la première action qui
utilise ce RDD, et sera gardé en mémoire de façon distribuée au niveau de tous les exécuteurs utilisés pour le calculer. Lors de l’éxécution d’une nouvelle
action qui utilise ce RDD, celui est accessible rapidement en mémoire, et ne sera pas recalculé.
// spark ne charge pas physiquement les données du RDD
// spark mémorise comment calculer second_RDD
// spark mémorise comment calculer Third_RDD
// spark charge physiquement les données du RDD pour établir
//first_RDD, calcule physiquement « second_RDD » et « third_RDD »
// Il effectue ensuite l’action de comptage, dont la valeur retour est
//retrounée au driver
19. Persistance
• Scala java – par défault : Stockage en mémoire sous forme d’objets non serialisés
• Python – par défault : Stockage en mémoire sous forme d’objets sérialisés
• Suite à une défaillance d’un noeud, Spark re-calcule les partitions perdues au besoin.
• Il est possible de répliquer les données persistées.
• Si les données à cacher dépassent les capacités de mémoire, Spark supprime les anciennes partitions persistées.
– En mémoire : il re-calcule les parititions supprimées au besoin
– En mémoire-et-disque : Il les écrira en disque
• Pour libérer la mémoire, on peut appeller la fonction “unpersist()”
RDDs opérations - transformations
20. Deux types de transformations
Il existe deux types de transformations sur les RDDs
• Narrow : tous les enregistrement dans une des partitions du RDD
fils , sont calculés uniquement à partir des enregistrements d’une
et une seule partition du RDD parent
• Wide : La constitution d’une partition fait intervenir plusieurs
partitions
Le shuffle qui intervient dans toutes les transformations de type
« wide » est une opération très consommatrice en bande passante
au sein du cluster et peut créer un goulot d’étranglement.
Règle pratique : Il est important d’effectuer toutes les opérations
de filtrage avant les transformations de type « wide », afin d’avoir
le minimum possible de données à « shuffler » entre les noeuds
RDDs opérations - transformations
21. RDDs opérations – basic actions
- Toutes ces opérations retournent des résultats au driver (sauf foreach() qui est exécuté directement au niveau des « executors » )
ATTENTION : le retour de la fonction « collect » doit être supporté par une seule machine (celle du driver), et peut
éventuellement causer l’echec du job
22. Plan d’exécution d’une application spark
Vocabulaire : Application/Job/Stage/Tâche
• Application : un programme qui peut consister en plusieurs transformations, et actions.
• Job : Quand une application Spark est lancée, chaque action sur une RDD engendre un job
• Stage : Un job contient N+1 stages séquentiels si N est le nombre de transformations de type « Wide ».
• Tâches : Chaque stage se déroule en plusieurs tâches, chacune opérant sur une partition de la RDD en entrée du
stage.
• A l’issue d’un shuffle, le nombre de tâches du stage suivant est défini par le développeur ( ou égal à une valeur par
défaut)
map groupByKey
RDDRDD
Stage 1 Stage 2
24. Mode distribué
• Composants d’une application spark
– Spark driver : s’exécute dans son propre process java.
– Spark executors : chacun s’exécute dans son propre java process
• La négociation des ressources se fait entre le driver et le cluster
manager (Yarn, Mesos, ..)
25. Le driver
- Le driver est un process java , dans lequel s’exécute le main du
programme codé par le développeur.
- Il exécute deux fonctions principales :
• Convertir un programme en un plan d’exécution
– À partir d’un graphe acyclique logique qui caractérise le programme …
– ...le driver établit un plan d’exécution physique sous forme de plusieurs stages,
contenant plusieurs tâches à faire exécuter par le cluster
• Ordonnancer l’exécution du plan d’exécution
– Les exécuteurs s’enregistrent auprès du driver
– Le driver se charge ensuite d’assigner les différentes tâches aux exécuteurs de
façon à maximiser le principe de proximité des données
– Le driver mémorise également les endroits de persistance des différentes
partitions des RDDs (quand elles sont persistées), afin d’optimiser l’assignation
des futures tâches qui accéderont à ces données
26. Les exécuteurs
• Un exécuteur spark est un process Java qui s’occupe de l’exécution des
tâches que lui assigne le driver. Il est lancé au début de l’application et
reste disponible pendant toute la durée du job. Si un exécuteur défaille,
l’application continue d’être exécutée et un mécanisme de récupération
de données et/ou de tâches est géré par Spark de façon transparente.
• Rappel : Un process java (ici une JVM) peut lancer parallèlement un
nombre maximal de threads (tâches), qui est égal au nombre de cœurs de
processeurs alloués pour le process.
Il remplit deux fonctions :
– Exécuter les tâches qui leurs sont assignées et retournent des résultats au driver
– Offrir du stockage en mémoire aux RDDs persistées en mémoire
27. Lancement d’un programme
1. L’utilisateur soumet une application spark par le biais de la
commande spark-submit
2. Le programme driver est lancé dans un process java , et invoque la
métode main du programme lancé par l’utisateur
3. Le programme driver contacte le cluster manager pour allouer des
ressources pour lancer des exécuteurs, coformément aux
ressources demandées par l’utilisateur
4. Le cluster manager lance les exécteurs pour le compte du driver
5. Le driver consitute le plan d’exécution et assigne des tâches
élémentaires aux exécuteurs
6. Le driver se termine quand le main() finit de s’exécuter ou suite à
un appel de SparkContext.stop().
7. Il arrête les éxécuteurs et libère les ressources auprès du cluster
manager.
28. Le spark-submit
• Le format général :
bin/spark-submit [options] <app jar | python file> [app options]
• Les options
30. Spark sur Yarn : architecture
Mode Yarn-client
Lorsque Yarn est utilisé comme
Cluster Manager, il faudrait bien
distinguer entre les deamons YARN
et les deamons SPARK
YARN
RM : responsable d’affectation
des ressources
Spark AM : demande les
ressources auprès du RM
Spark NM : nœuds contenant un
ou plus de container, chacun
pouvant accueillir un exécuteur
spark.
SPARK
Spark Driver : s’exécute chez le
client en mode Yarn-client , ou dans
l’application master en mode Yarn-
cluster
Spark Executor : s’exécutent dans
des containers YARN au sein du
node manager.
ATTENTION : deux exécuteurs spark peuvent
se retrouver dans deux containers Yarn
différents, mais au sein de la même machine
(YARN node manager)
31. Spark sur Yarn– Application/Job/Stage/Tâche
• Quand une application Spark est lancée, chaque action sur
une RDD engendre un job qui contient N+1 stages séquentiels
si N est le nombre de transformations de type « Wide ».
• Chaque stage se déroule en plusieurs tâches, chacune opérant
sur une partition de la RDD précédente.
• Au sein d’un stage(où toutes les transformations sont de type
« narrow »), le nombre de tâches est égal exactement au
nombre de partitions du dernier RDD constitué, qui lui-même
est (supérieur ou) égal au nombre de partitions du RDD initial
(nombre de blocs hdfs sous-jacents).
• A l’issue d’un shuffle, le nombre de tâches du stage suivant
est défini par le développeur ( ou égal à 1 par défaut)
• Une tâche est exécutée par un cœur processeur d’un
exécuteur spark (container Yarn) alloué pour l’application.
• Les tâches d’un stage se déroulent parallèlement si les
ressources allouées le permettent.
• Deux stages ou jobs indépendants peuvent être exécutés
parallèlement.
Règle : Afin de paralléliser entièrement les tâches d’un stage contenant des transformations de type « Narrow », le nombre d’exécuteurs
alloués * le nombre de cœurs par exécuteur DOIT ETRE SUPERIEUR OU EGAL au nombre de blocs HDFS du fichier initial
Règle : En général, Le nombre d’exécuteur alloués * le nombre de cœurs par exécuteur DOIT ETRE SUPERIEUR OU EGAL au nombre de
tâches du stage (ou des stages si ceux-ci sont indépendants et donc parallèles) contenant le plus grand nombre de tâches.
Règle : Si les ressources le permettent, il est préférable d’en allouer largement au dessus des seuils précédents, afin d’avoir plus de
chances d’appliquer le principe de proximité des données (chaque partition est traitée par un exécuteur lancé dans le même nœud hdfs
contenant le bloc correspondant)
map groupByKey
RDDRDD
Stage 1 Stage 2
33. Objectif
• A partir d’une table de CDRs sous forme d’un fichier CSV
stocké dans hdfs, calculer des agrégats (par msisdn)
– Voice total count
– Voice in total count
– SMS in total count
– Calls dispersion
– Cells disperion
– Voice intenational out
• Comparer ses compteurs à des valeurs seuils, pour filtrer les
comportements fraudeurs
34. 1. Read CDR decoded Data
Val data = sc.textFile(« path/to/files »)
2. Constructing a paired RDD
Val paired_data = filtered_data.map( row=> ( row(msisdn) , CDR_Schema_Apply(row) )
3. Grouping_by_key
Val grouped_by_key_data= paired_data.groupByKey(numOfPartitions)
4. Applying the rule()
Val fraud_list = grouped_by_key_data.filter( (key,value) => rule_func(value, json) )
5. The driver collects the results in an array of Strings , (before writing back to a Hive (or Hbase table))
Val result= fraud_list .collect()
Algorithme spark (code sous-jacent en scala)
DAG
35. Algorithme spark : Object_func
rule_funct( rule : Rule, cdrs : Iterable[Cdr] ) : Boolean
Return
condition_func(rule.condition(1), cdrs) rule.binaryOperators(1) condition_funct(rule.condition(2),cdrs)
rule.binaryOperators(2) condition_funct(rule.condition(3),cdrs) ……..
condition_func (condition : Condition , cdrs : Iterable[Cdr] ) : Boolean
Condition.comparison_op match {
case « ge » => return operation_funct( condition.operations(1) , cdrs) >= operation_funct( condition.operations(2) ,cdrs)
case « le » => return operation_funct( condition.operations(1) , cdrs) <= operation_funct( condition.operations(2) ,cdrs)
case « g » => return operation_funct( condition.operations(1) , cdrs) > operation_funct( condition.operations(2) , cdrs)
case « l » => return operation_funct( condition.operations(1) , cdrs) < operation_funct( condition.operations(2) , cdrs)
case « eq » => return operation_funct( condition.operations(1) , cdrs) == operation_funct(condition.operations(2) , cdrs)
case « not » => return operation_funct( condition.operations(1) , cdrs) == 0 }
operation_func(Operation : operation , cdrs : Iterable[Cdr] ) : Float
Operation.operation_type match {
case « ratio » => return Function_funct( operations.functions(1) , cdrs) / Function_funct( operations.functions(2) , cdrs)
case « product » => return Function_funct( operations.functions(1) , cdrs) *Function_funct( operations.functions(2) , cdrs)
case « filter » | « static_threshlod » | « counter » => return Function_funct( operations.functions(1) , cdrs)
function_funct (function : Function , cdrs : Iterable[Cdr] ) : Float
Function.function_type match {
case « count » => return cdrs.count(cdr => function.fields.contains(cdr.recordType))
case « static_value » => return function.fields(0).toFloat
case « filter » => return sc.textFile(function.fields(0)).collect().contains(cdr.msisdn)
case « disitinct_count » => countList
36. map groupByKeytextFile
Stage 1 Stage 2
HDFS csv file (or hive
table) of n blocks
data : RDD [String] : primary data
abstraction in Spark. It contains a
distributed set of lines (rows)
Grouped_by_key_data :
Paired_RDD [(key : String
,value : Iterable[Tuple
[String]])]
paired_data : Paired_RDD [
(key : String , value :
Tuple[String]) ]
Result :
Array
[Strings]
Fraud_list: Paired_RDD [(key : String
,value : Iterable[Tuple [String]])]
collect
(*) : source : Learning spark - O'Reilly Media
filter
4 blocks => 4 Tasks in
stage 1
num_executors *
executors_cores > N
!!!!!
numPartions
given as in
input to
groupByKey_
function=>
numPartions
Tasks in
stage 2
numOfPari
tions >
queue_total
_cores (=790
for
« HQ_IST »
queue » !!!
(*)
Plan d’exécution
• Supposons que le fichier en entrée est d’une taille de 1Go = 3 * 256Mo + 232Mo (4 blocs hdfs)
• L’application comporte plusieurs transformations sur le RDD et une seule action.
– Un Seul job est donc lancé
– Ce job comporte plusieurs transformations de type Narrow, et une seule transformation de type wide(groupByKey).
– Il comprend donc deux stages séparés par le shuffle
– Le premier stage se déroule sous forme de plusieurs tâches (4 tâches exactement)
– Dans une tâche de ce premier stage, la partition concernée du RDD initial subit toute les transformations du premier stage.
– Le second stage comprend un nombre de tâches exactement égal à la valeur de repartitionnement donnée en paramètre à la fonction
groupByKey (3 partitions dans l’illustration ci-dessous)
37. Allocation des ressources pour le programme
• Rappel : capacités du cluster de la PEI :
– 2 racks, 55 Nœuds actifs, <memory: 4.83 TB , vCores : 1320>
• Deux types de nœuds : < 58.88 GB , 24 cores > ou < 121.99 GB , 24 cores >
– Capacités maximales pour la queue « HQ_IST »
• Capacité maximale d’un container YARN : < 65,536 GB , 24 cores >
• Capacité maximale par utilisateur : < 1012,736 GB , 265 cores >
• Application master - capacité maximale : < 304,128 GB , 80 cores >
• Allocation des ressources
– Si le fichier en entrée est de l’ordre de N*256Mo + X (avec X<256Mo)
– Pour exécuter le stage 1 de façon entièrement parallélisé, il faudrait que « num_executors » *
« executor_cores » > N
– Il est recommandé de fixer le nombre de executor_cores à 5, pour ensuite définir le nombre
d’exécuteur à allouer selon la règle précédente. (allouer largement au dessus pour appliquer le
principe de proximité des données).
– Quand il n’y pas de RDD à cacher, 2 Go de RAM par cœurs de processeur est un bon ordre de
grandeur. Donc, il est recommandé d’allouer « exeutor-memory » = 10G.
– Le nombre de partitions à créer à l’issue du shuffle dépend du plan d’exécution et de la
répartition initiale des données, et de l’assignation des tâches effectuée en stage 1.
• Il est recommandé soit de créer autant de partitions que le nombre de cœurs disponibles, et continuer ensuite soit
d’augmenter, soit de diminuer jusqu’à ce que les performances ne continuent plus de s’améliorer
• ou de créer autant de partitions que le nombre de partitions du stage précédent. Continuer ensuite de multiplier ce
nombre par 1,5 jusqu’à ce que les performances s’arrêtent de s’améliorer.
38. • PARTIE 1 :
• Spark : généralités
• Spark : RDDs et plan d’exécution
• Spark : mode distribué et déploiement sur YARN
• Exemple : Bypass
• PARTIE 2 :
• Spark : PairedRDD
• Spark : tunning des algorithmes et de
l’allocation de ressources
• Annexe : tests de montée en échelle
Sommaire
40. Créer un RDD pair (implicite)
Principe de la conversion Implicite :
certaines transformations de RDD ne sont applicables que sur certains types particuliers de RDD. (ex : mean() and variance() sur
les RDD[Double] ou join() sur les RDDs “(clef,valeur)” .
Il s’agit en effet de types différents de RDD, chacun avec sa propre API. En appliquant la transformation groupByKey sur un RDD
contenant des tuples (clef/valeur), il s’agit de la méthode groupByKey de la classe « PairedRDD » qui s’exécute et non pas celle
de la classe « RDD » (car elle n’existe pas dans cette classe). Aucune déclaration explicite n’est nécessaire.
Aussi faudrait il faire attention à ne pas consulter la sclaladoc de la classe RDD pour chercher une transformation qui s’applique
uniquement sur les RDD pairs.
42. RDDs pairs : agrégations
• ReduceByKey, foldByKey, :
– Les transformations ReduceByKey, et foldByKey prennent en argument une fonction à appliquer successivement à
chaque deux éléments du RDD ayant la même clef.
– Cette fonction doit donc être associative.
• Différence par rapport à groupByKey
– Alors que ces transformations s’exécutent localement sur chaque machine avant d’engendrer un shuffle, la
transformation groupByKey lance directement une opération de shuffle, induisant un trafic important entre les
machines.
• Règle : Si la fonction à appliquer sur un ensemble de valeurs correspondant aux mêmes clefs
est associative, il faut privilégier les transformations ReduceBykey, et foldByKey
• CombineByKey:
– combineByKey(createCombiner, mergeValue,mergeCombiners,partitioner)
44. RDDs pairs : parallélisme
• Pour toutes les transformations sur les RDD pairs, il est possible de définir un second
paramètre (degré de parallélisme) qui définit le nombre de partitions du RDD groupé ou
agrégé par clé
Default parallelism « 10 »
Setting parallelism to « 3 »
repartitionnement
46. RDDs pairs : partitionnement
Cas commun
• Considérons une opération de jointure qui s’effectue chaque 5 minutes, entre un RDD
statique et volumineux UserData et un petit RDD events correspondant aux événements
reçus dans les 5 dernières minutes
• Par défault, cette opération effectue un hashing sur l’ensemble des clés des deux RDDs,
envoie ensuite sur le réseaux les éléments ayant la même clef hashé aux même nœuds. La
jointure est ensuite effectuée.
Les données sont hashés et shufllés à travers le réseau à chaque 5 minutes?
47. RDDs pairs : partitionnement
Cas commun- solution
• Il est possible au moment de la création du RDD statique userData, de la hash-partitionner.
Attention : Si le RDD n’est pas caché en mémoire, le hash-partitionnement devient inutile
48. RDDs pairs : partitionnement
Partitionnement par défaut
• Toutes les transformations opérant sur les RDDs pairs automatiquement génèrent un RDD qui est hash-
partitionné
Opérations qui bénéficient du partitionnement
• cogroup(), groupWith(), join(), leftOuterJoin(), rightOuterJoin(), groupByKey(), reduceByKey(), combineByKey(), and lookup()
• Pour les opérations qui s’exécutent sur un seul RDD (reduceByKey), si le RDD et pré-partitionné, il en résulte que l’aggrégration
s’effectue localement dans chaque noeud.
• Pour les opérations qui s’exécutent sur deux RDDs, le pré-partionnement des deux RDDs engendre le shuffle d’un seul des deux RDDs
au maximum.
• S’ils sont persistés dans la même machine, en appelant la fonction “mapValues” par exemple, ou si un des deux RDDs n’as pas encore
été créé, aucun shuffle ne s’exécute dans le réseau
Opérations qui affectent le partitionnement
• toutes les opérations sauf : cogroup(), groupWith(), join(), leftOuterJoin(), rightOuter Join(), groupByKey(), reduceByKey(),
combineByKey(), partitionBy(), sort(), mapValues() (if the parent RDD has a partitioner), flatMapValues() (if parent has a partitioner), and
filter() (if parent has a partitioner)
50. Configurer une application
1. En utilisant sparkConf:
2. Dynamiquement (dans le spark-submit)
3. Spark-default.conf (dans the spark directory par défaut ou dans un répertoire donné)
52. Application/Job/Stage/Tâche
• Application : un programme qui peut consister en plusieurs transformations, et actions.
• Job : Quand une application Spark est lancée, chaque action sur une RDD engendre un job
• Stage : Un job contient N+1 stages séquentiels si N est le nombre de transformations de type « Wide ».
• Tâches : Chaque stage se déroule en plusieurs tâches, chacune opérant sur une partition de la RDD en entrée du
stage.
• A l’issue d’un shuffle, le nombre de tâches du stage suivant est défini par le développeur ( ou égal à une valeur par
défaut)
map groupByKey
RDDRDD
Stage 1 Stage 2
53. Plan d’exécution : exemple
• Jusqu’ici aucune action n’a été appelée. Donc aucun job n’est lancée au sein de l’application.
Aussi, les RDDs sont paresseusement évalués.
• Pour évaluer calculer physiquement les RDDs, il est possible d’appeler une action dessus (collect), qui va donc lancer
un job.
54. Plan d’exécution : stages
• Le plan d’exécution de ce job est le suivant :
• Une seule transformation de type « wide » => donc deux stages séparés par le shuffle sont générés
• Si le RDD final a été persisté lors d’une première exécution du plan précédent, une nouvelle action « comme
count » par exemple engendrera uniquement un seul stage. S’il n’a pas été persisté, cette action engendrera
un stage supplémentaire en plus des stages précédents.
55. Plan d’exécution : tâches
• Chaque stage est décliné en plusieurs tâches, opérant chacune et de façon similaire sur une partition du RDD
initial
map
Stage
• Chaque tâche s’exécute de la façon suivante
56. Parallélisme
• Règle 1 : Chaque stage comprend autant de tâches que de partitions de la RDD en entrée
• Règle 2 : Input RDDs se base directement sur le stockage sous-jacent (bloc hdfs partition du RDD tâche)
• Règle 3 : le degré de parallélisme doit être optimal (pas trop petit par rapport aux ressources allouées, et pas
trop grand )
• Règle 4 : Il y a deux façons pour définir son degré de parallélisme (nombre des partitions)
• Degré de parallélisation donné en paramètre aux transformations qui engendrent un shuffle
• En utilisant la fonction repartition(numPartitions) ou coalesce(numPartitions)
• La fonction coalesce(numPartitions) ne fonctionnent que lorsqu’on réduit le nombre de partitions. Elle est
beaucoup plus optimal que la fonction repartition, car elle n’engendre pas de shuffle.
57. Sérialisation
• Quand Spark transfère les données dans le réseau ou les persistent en disque, il a besoin de sérialiser les
objets (classes) en des formats binaires.
• Spark utilise par défaut le Java’s built in serializer.
• Mais il supporte également l’utilisation d’un serializer tiers appelé « kyro »*
• Sérialisation plus rapide
• Représentation binaire plus compacte
• Attention : les « user-defined » classes doivent implémentée l’interface Java « Serializable ». Si non, une
exception de type « noptSerializableException » sera levée.
58. Gestion de la mémoire
• Au sein de chaque exécuteur, la mémoire est partagé entre trois fonctionnalités :
Stockage RDD Shuffle et buffers d’agrégation Le code utilisateur
When you call persist() or cache() on an
RDD, its partitions will be stored in
memory buffers.
When performing shuffle operations,
Spark will create intermediate buffers
for storing shuffle output data. These
buffers are used to store intermediate
results of aggregations in addition to
buffering data that is going to be
directly output as part of the shuffle
Memory which is necessary for the
spark code (allocation of arrays for
example)
spark.storage.memoryFraction spark.shuffle.memoryFraction
60% 20% 20%
• Règle : Ces pourcentages doivent être paramétrées en fonction du programme à lancer.
• Règle : MEMORY_AND_DISK storage level est préférable que le MEMORY_ONLY storage level pour la
persistance.
60. Tests de performances - Programme de test
• A partir d’une table de CDRs sous forme d’un fichier CSV
stocké dans hdfs, calculer des agrégats (par msisdn)
– Voice total count
– Voice in total count
– SMS in total count
– Calls dispersion
– Cells disperion
– Voice intenational out
61. Tests de performances - Algorithme spark (scala)
• Lecture du fichiers HDFS
val data = Sc.textFile(nom_fichier)
• Mapping (séparation des champs)
val table = data.map(ligne=>ligne.split(‘t’))
• Filtrage (CDR voix in/out et sms in uniquement)
val types_0_1_6 = table.filter(l=>l(1)==0 || l(1)==1 || l(1)==6)
• Mapping (champs qui nous intéressent uniquement)
Val ready_table = types_0_1_6 .map( l => (l(4), (l(1),l(5),l(22),l(4)))
• GroupByKey (regroupement par msisdn)
Val grouped_table= ready_table.groupeByKey()
• Mapping (calcul des compteurs pour chaque msisdn)
Val compteurs= grouped_table.map( l=> (l._1, fonct_calcul_compteur(l._2)))
• collect(récupération des résultats dans le programme du driver pour restitution stdout)
For (i<- compteurs.collect()) println(i)
filter groupByKey maptextFile map map take
62. Tests de performances - Plan d’exécution spark
filter groupByKey maptextFile map map take
filter groupByKey maptextFile map map take
Stage 1 Stage 2
63. filter groupByKey maptextFile map map take
Stage 1 Stage 2
Fichier HDFS stocké
de façon parallélisée
par blocs de 256 Mo
(ex : 15,4 Go = 60 *
256 Mo + 40 Mo ) RDD : abstraction primaire des
données sous spark correspondant
à une collection distribuée
d’enregistrements dont les
partitions correspondent aux
différents blocs hdfs sous-jacents.
RDD RDD RDDRDD RDD Array
Tests de performances - Plan d’exécution spark
64. • Supposons que le fichier en entrée est un fichier hdfs text (TextInputFormat)
d’une taille de 15,4 Go = 60 * 256 Mo + 40 Mo
• La lecture du fichier par l’application spark se fait avec la fonction
« textFile() », et permet de constituer un RDD (resilient distributed data).
• Le RDD est composé de 61 partitions :
– Chacune des partitions est physiquement stockée dans un des 61 nœuds contenant les blocs
HDFS du fichier
– Ces nœuds HDFS ne sont pas nécessairement les nœuds exécuteurs alloués pour exécuter les
traitement sur les données.
– Les nœuds exécuteurs chargeront donc ces données en mémoire (ou en disque dur à défaut
de ressources) lorsque le job sera lancé
• Plan d’exécution : L’application comporte plusieurs transformations sur le RDD
et une seule action.
– Un Seul job est donc lancé
– Ce job comporte plusieurs transformation de type Narrow, et une seule transformation de
type « wide »(groupByKey).
– Il comprend donc deux stages séparés par le shuffle
– Le premier stage se déroule sous forme de plusieurs tâches (61 tâches exactement)
– Dans une tâche de ce premier stage, la partition concernée du RDD initial subit toute les
transformations du premier stage.
– Le second stage comprend autant de tâche que le nombre de partitions constitués suite au
shuffle et qui est donné comme paramètre à la fonction « groupByKey »
Tests de performances - Plan d’exécution spark
65. Jeu de test
• Application du programme de calcul des compteurs
en faisant varier :
– La taille du Fichier CSV de CDRs
– Les ressources allouées
• Pour chaque combinaison (taille de fichier,
ressources allouées), le programme est exécuté plus
de 100 fois, avant de prendre la valeur minimale des
temps d’exécution
• Pour chaque taille du fichier en entrée donnée, la
configuration des ressources qui donne le temps
d’exécution le plus minimal est gardée
67. Résultats(2)
20
25
30
35
40
45
50
55
0 2 4 6 8 10 12 14 16
petits volumes
•Même pour le traitement d’un fichier d’une taille de l’ordre de quelques Ko, un temps d’initialisation important est requis (de l’ordre de
30s) pour l’allocation des ressources et l’assignation des tâches.
•Quand la taille du fichier est inférieure à la taille d’un bloc d’un fichier hdsf (256 Mo), une seule tâche est exécutée dans le premier
stage
•Cela ne sert à rien d’allouer plus de deux cœurs (nbr exec * nb cœurs/exec).
•Dans le cas précédent, si on repartitionne suite au shuffle en deux partitions, il faudrait allouer exactement deux cœurs du cluster pour
le job, car deux tâches seront exécutées dans le stage 2
• Dès que la taille du fichier en entrée dépasse 256 Mo, on peut commencer à bénéficier de la parallélisation dans le premier stage.
•Etant donné que le capacité maximale de la queue utilisée est de 790 cœurs, nous pouvons bénéficier de la parallélisation totale lors du
premier stage même avec un fichier d’une taille de 790*256 Mo = 200 Go.
•Ceci explique l’évolution logarithmique constatée à partir de 256 Mo.
68. Résultats(3)
35
45
55
65
75
85
95
105
0 100 200 300 400 500 600
volumes moyens
•Nous constatons que le temps d’éxécution continue d’évoluer logarithmiquement sur chaque segment de 200 Go.
•Au bout de chaque 200 Go, un saut au niveau du temps d’éxécution est constaté
•Cela est du au fait que sur un segment de 200 Go = 790 * 256 Mo , des cœurs sont toujours disponibles pour traiter d’autres tâches en
parallèle. Dès que les 200 Go sont atteints, la parralélisation est maximale.
•En augmentant ensuite la taille du fichier, il faudrait attendre que les 790 cœurs aient finis leur première tâche, pour en recevoir une
nouvelle. En continuant d’augmenter la taille du fichier de 200 Go, on ne fait que maximiser la parallélisation.
•Dès qu’on dépasse 200 Go à nouveau, un temps d’éxécution supplémentaire est ajouté qui est celui du traitement des blocs
supplémentaires par rapport aux n*790 blocs initiaux.
•Et ainsi de suite …
•Nous remarquons aussi que l’évolution globale peut être approximé par une évolution linéaire
69. Résultats(4)
90
140
190
240
290
340
390
440
490
540
0 1000 2000 3000 4000 5000 6000
gros volumes
•Dans le ce dernier graphique, nous ne prenons plus des intervalles de 200 Go.
•On constate bien que l’évolution globale du temps d’exécution constatés pour les trois fichiers testés en entrée, qui sont de 500 Go,
2To, 5To, est bien linéaire.
•Si nous testons plusieurs fichiers au sein de chaque segment de 200 Go, nous aurions bien constaté une évolution locale
logarithmique sur chaque segment, en plus de l’évolution linéaire globale constatée