INEX Informatique en Nuage : Expérimentations et Vérification Livrable n M2.1 ÉTUDE ET PARALLÉLISATION D ALGORITHMES POUR L ANALYSE DE GRANDS GRAPHES DE RÉSEAUX SOCIAUX Jérôme Richard Univ. Orléans, INSA Centre Val de Loire, LIFO EA 4022, France
2 INEX
1 ère année de Master informatique Université Orléans (2012-2013) É tude et paralle lisation d algorithmes pour l analyse de grands graphes de re seaux sociaux Jérôme Richard Tuteur de stage : Nicolas Dugué et Anthony Perez
Table des matières Introduction... 3 Présentation... 3 Environnement de travail... 3 Sujet du stage... 4 Analyse... 5 Pregel... 5 Giraph... 7 Exemple d'application... 7 MPI... 10 Travail réalisé... 11 Mise en place de l environnement de développement... 11 Applications Giraph... 11 Programmes complémentaires... 13 Programme MPI... 14 Fonctionnement... 14 Résultat des tests et comparatif... 15 Détail du contenu du SVN... 16 Bilan... 18
Introduction À la fin d'une première année de Master, chaque étudiant peut réaliser s'il le souhaite un stage facultatif. Cette expérience permet de mettre en application les connaissances acquises durant la formation, mais aussi de les étendre et de se confronter au monde du travail. J'ai eu la chance de pouvoir effectuer mon stage de troisième année de licence au LIFO avec l'équipe PaMDA et de pouvoir retenter l'expérience cette année au sein d'une autre équipe du laboratoire. Ce stage m'offre une nouvelle chance de travailler dans le domaine de la recherche, mais aussi de travailler sur de nouvelles branches de l'informatique telle que la manipulation de grand graphe dans un environnement distribué hétérogène. Présentation Environnement de travail Le LIFO accueille chaque année des étudiants qui effectuent des stages. Doctorants, professeurs et maîtres de conférences se partagent les bureaux. Au cours de mon stage, j ai travaillé en étroite collaboration avec mon tuteur Nicolas Dugué et avec l'aide d'anthony Perez. J'ai aussi travaillé avec d'administrateur système Sébastien Guibert qui m'a aidé à mettre en place des outils nécessaires au bon déroulement de mon stage sur les machines du LIFO. Au début de mon stage, on m'a confié des accès à un cluster de calcul et à un environnement Cloud installé et configuré me permettant par la suite de lancer d'effectuer des tests d'algorithmes répartis (pouvant parfois durer plusieurs heures à plusieurs jours). On m a aussi fourni un accès à un serveur de versionnage qui m'a permis de régulièrement sauvegarder mon travail et d'éviter ainsi des problèmes de pertes de données liés au stage. Les serveurs étant accessible depuis internet, j'ai pu effectuer une petite partie du stage chez moi où j'ai pu travailler dans un environnement plus calme et plus familier, communiquant par mail avec mon tuteur et l'administrateur réseau en cas de problèmes. Afin de réaliser mes tests sur des données réelles, on a mis à ma disposition des graphes de réseaux sociaux comme celui de Twitter ou le LiveJournal de Stanford.
Sujet du stage Depuis quelques années, dans les secteurs de l Internet, de l analyse décisionnelle ou encore de la génétique sont collectées et analysées des données de plus en plus volumineuses et complexes. Ce phénomène connu sous le nom de Déluge des données (ou très souvent sous l anglicisme Big Data) soulève de nombreuses problématiques. En particulier, être capable de stocker, partager et analyser de telles quantités de données constitue un enjeu d étude essentiel. Dans de nombreux cas, ces données peuvent être représentées en utilisant la théorie des graphes. Cette dernière est particulièrement appropriée pour étudier les réseaux sociaux, où les connexions entre utilisateurs peuvent facilement être représentées et analysées en utilisant des graphes, le plus souvent orientés. Cependant, le nombre d utilisateurs des réseaux sociaux sur Internet a littéralement explosé récemment, ce qui aboutit à la création de graphes de très grande taille, dont les propriétés structurelles sont difficiles à étudier. Afin de pouvoir analyser efficacement des réseaux d une telle taille, le parallélisme et la distribution des algorithmes au sein d un parc de serveurs ou d un Cloud devient nécessaire. Ainsi, de nombreux framework spécifiques au développement d algorithmes de graphes distribués inspirés de l approche Pregel sont nés : Apache Giraph, Apache Hama, HipG, Signal/Collect. Ceux-ci forcent à implémenter les algorithmes selon un modèle précis et donc à les repenser partiellement. En revanche, ils prennent en charge la parallélisation de l algorithme automatiquement. L objectif de départ dans lequel s inscrivait ce stage était le développement d une librairie parallèle d algorithmes destinée à l analyse de grands graphes de réseaux sociaux dont les axes étaient les suivants : Dans un premier temps, il s agissait d évaluer les frameworks spécifiques au développement d algorithmes de graphes distribués inspirés de l approche Pregel selon la facilité de programmation qu ils offrent, la souplesse de leur modèle et leur efficacité. Dans un second temps, je devais développer des algorithmes de graphe distribués destinés à l analyse de réseaux sociaux (détection de communautés, de structures de graphe, mesures de centralités, algorithme de flot). Enfin, je devais appliquer ces algorithmes sur grands graphes de réseaux sociaux et valider les résultats en terme d efficacité et de simplicité d implémentation.
Analyse Avant de parler du framework Giraph sur lequel j'ai principalement travaillé, il est nécessaire de détailler le modèle Pregel sur lequel il repose. En effet, de nombreux algorithmes de graphes supposent que l'ensemble du graphe traité réside en mémoire et traite ce graphe séquentiellement. Seulement, certains graphes sont tellement grands qu'il est impossible de les stocker en mémoire. De plus, le temps de calcul de ces algorithmes peut vite exploser sur ces grands graphes. Il est alors nécessaire de redéfinir l'algorithme pour qu'il fonctionne sur un environnement distribué. C'est-à-dire, décomposer le graphe en plusieurs partitions et les manipuler en parallèle sur plusieurs machines pour accélérer le traitement et réduire alors la limitation de la mémoire fixée par une seule machine. Il existe des framework se basant sur des modèles connus comme le MapReduce ou des dérivés qui ont de nombreux avantages comme celui de pouvoir traiter de gros volumes de données dans un environnement distribué de manière relativement simple. Néanmoins, ce modèle s'adapte difficilement à la manipulation de graphe en pratique. Afin de simplifier la programmation d'algorithme de graphe dans un environnement distribué, un nouveau modèle a été créé : Pregel. Pregel Dans le modèle Pregel, le plus petit élément manipulable est un nœud. Ce nœud possède un identifiant, des propriétés, des arcs sortants (avec des propriétés) qui pointe sur des nœuds (via leurs identifiants) et un état (actif ou non). Chaque nœud détient en plus une boite-aux-lettres de messages reçus, lui permettant de lire des messages provenant d autres nœuds qui peuvent être connecté. Un nœud peut aussi envoyer des messages à d autres nœuds. Seuls les nœuds actifs travaillent. Un nœud actif peut s'endormir, il devient alors inactif et sera réveillé lorsqu'il recevra un message. Le traitement du graphe prend fin lorsque tous les nœuds sont inactifs. Fondamentalement, chaque nœud actif va donc lire des messages dans sa boite-aux-lettres puis les traiter et finalement retransmettre d'autres messages à ces voisins si nécessaire.
Figure 1 Structure d un nœud dans le modèle Pregel Le graphe est décomposé en multiple groupe appelés partitions et chacune contient un grand nombre de nœuds et chaque partition est traitée de manière séquentielle. Le modèle d'exécution utilisé est le BSP (Bulk Synchronous Processing). Dans ce modèle, une multitude de tâche s'exécute en parallèle sous forme de séquences appelées supersteps. Dans chaque superstep, les tâches commencent par recevoir tous les messages provenant des tâches du superstep précédent, les traitent et peuvent envoyer d'autres messages. La transmission des messages se fait de manière asynchrone et simultanée sur toutes les tâches. Les messages envoyés lors d'un superstep ne seront pas accessible avant le superstep suivant. Lorsque toutes les tâches se sont terminées, un nouveau superstep peut être démarré et ainsi de suite jusqu'à ce qu'une condition d'arrêt soit vraie. Figure 2 - Fonctionnement du modèle BSP Dans le modèle Pregel, chaque tâche d'un superstep va traiter une partition du graphe.
Giraph Apache Giraph est un framework de traitement de graphe conçu pour être hautement scalable et donc pouvoir traiter de gros volumes de données. Giraph est une alternative open-source à Google Pregel et implémente le même modèle. Le framework ajoute quelques fonctionnalités supplémentaires à Pregel comme le contrôle du processus maître, des agrégateurs partagés, la possibilité de faire des traitements en dehors des limitations de la mémoire (out-of-core computation), etc. Il existe des alternatives à Giraph comme Neo4j et GraphLab ne se basant pas sur les mêmes framework ni sur les mêmes modèles, mais Giraph propose des fonctionnalités avancées et semble avoir de bonnes performances en pratique 1. Il est bon de noter que Giraph utilise plusieurs sous-projets dont Hadoop (infrastructure basée sur le modèle MapReduce) qui lui permet entre-autre de lire et écrire des fichiers via un système de fichier distribué nommé HDFS. De plus, cela permet aux applications Giraph d être lancé de la même manière que les applications Hadoop. Afin de simplifier l'explication du fonctionnement de Giraph, nous allons détailler un exemple d'application Giraph simple. Exemple d'application La recherche de composantes connexes est un des algorithmes les plus simples qui peut s'appliquer sur un graphe et qui peut se paralléliser sans difficulté. C est donc sur ce type d algorithme que nous allons nous orienter pour réaliser cet exemple. Pour commencer, nous allons supposer que nous disposons d'un graphe d'entrée contenant juste des arcs orientés et nœuds identifiés par des entiers écrits sous la forme de liste d'adjacence. Afin que Giraph puisse charger le graphe d entrée, nous devons lui spécifier comment le lire. Pour cela une classe de lecture de fichier doit être implémentée et doit hériter de «VertexInputFormat». 1 http://www.pds.ewi.tudelft.nl/fileadmin/pds/reports/2013/pds-2013-004.pdf
À la fin du traitement du graphe d entrée, un fichier de sortie va être créé par Giraph. Pour spécifier ce qu il doit contenir, une classe d écriture doit être implémentée elle aussi et doit hériter de «VertexOutputFormat». Les classes permettant de lire le graphe d entrée et écrire le fichier de sortie ne seront pas présentées ici car elles dépendent du type de fichier que l on veut lire/écrire et ne présentent pas un intérêt fondamental dans la compréhension d une application Giraph. Une fois la gestion des fichiers d entrées et de sorties terminée, il faut créer une classe qui va s occuper du traitement du graphe. Cette classe doit hériter de «Computation» et contiendra une méthode «compute» qui va appliquer une large partie du traitement à appliquer sur le graphe (dans le cas d algorithmes simples, c est ici que tout le traitement va se faire). Cette classe fournie des méthodes pratiques permettant par exemple de connaitre quel est le superstep en cours et la méthode «compute» à remplir fournie des informations complémentaires comme le nœud à traiter et les messages qui ont été reçus dessus. Ci-dessous, on peut voir un exemple de classe de traitement permettant de réaliser un test de connexité sur un graphe : public class VertexComputation extends Computation<LongWritable, LongWritable, NullWritable, LongWritable, LongWritable> { @Override public void compute(final Vertex<LongWritable, LongWritable, NullWritable> vertex, final Iterable<LongWritable> messages) { if(getsuperstep() == 0) { vertex.setvalue(vertex.getid()); sendmessagetoalledges(vertex, vertex.getid()); return; } long min = Long.MAX_VALUE; for(final LongWritable m : messages) if(m.get() < min) min = m.get(); if(vertex.getid().get() <= min) { vertex.votetohalt(); return; } } } final LongWritable newvalue = new LongWritable(min); vertex.setvalue(newvalue); sendmessagetoalledges(vertex, newvalue); Ici, chaque nœud va au début du traitement transmettre sa valeur à ses nœuds voisins directs et va s affecter comme valeur son identifiant (cela vient du fait qu il ne faut pas changer
l identifiant des nœuds, car c est le seul moyen de les identifier). Lors de l itération suivante, les nœuds vont recevoir des messages. Chacun de ces messages contient la valeur des voisins à une longueur N du nœud traité (où N est le numéro du superstep réalisé). On calcule la valeur minimum de ces voisins et on regarde ensuite si cette valeur est plus grande afin d affecter au nœud la nouvelle valeur plus grande si c est le cas. L itération suivante va ensuite retransmettre la nouvelle valeur dans le cas où le nœud est actif. En effet, la méthode «votetohalt» va permettre de rendre inactif le nœud et celui-ci ne sera donc pas traité à l itération suivante sauf s il reçoit un nouveau message. Cela évite ici de transmettre des messages inutilement et donc d accroitre les performances de l algorithme. Notez que les classes «Computation», «Vertex» et «Iterable» prennent des paramètres template. Ces paramètres sont des types devant étendre de l interface Writable. Les arguments pour ces trois classes sont définis de la manière suivante : Computation < VertexId, VertexData, EdgeData, IncomingMessage, OutgoingMessage > Vertex < VertexId, VertexData, EdgeData > Iterable < IncomingMessage > Où «VertexId» est le type d identifiant du nœud, «VertexData» est le type de donnée attachée au nœud, «EdgeData» est le type de donnée associée aux arcs sortants, «IncomingMessage» et «OutgoingMessage» sont le type de messages reçus et envoyés. Une dernière étape est nécessaire afin d avoir une application Giraph complète : programmer le processus maître. C est lui qui va s occuper du lancement des processus de traitement du graph (nommée Workers) et de la configuration générale de l application. Typiquement, cela se fait dans la fonction principale de l application. Ci-dessous, on peut voir un exemple de code utilisé pour réaliser notre application. Dans un premier temps, on extrait les arguments passés au programme. Puis, on crée une configuration Giraph où on définit quels sont les classes qui vont gérer le traitement, la lecture du graphe d entrée et le nombre de processus de traitement utilisé ainsi que le chemin où est stocké le graphe d entrée. Ensuite, on finit par créer une tâche Giraph configuré, nommée «Test Giraph» et on lui donne le chemin du fichier de sortie. On termine par lancer la tâche et quitter le programme en renvoyant 1 si le lancement ou le déroulement de la tâche a échoué et 0 dans le cas contraire.
public class Main { public static void main(string[] args) throws Exception { if(args.length!= 3) throw new IllegalArgumentException("This program needs 3 parameters: <input path> <output path> <number of workers>"); Path inputpath = new Path(args[0]); Path outputpath = new Path(args[1]); int nbworkers = Integer.parseInt(args[2]); GiraphConfiguration configuration = new GiraphConfiguration(); configuration.setcomputationclass(vertexcomputation.class); configuration.setvertexinputformatclass(vertexinputformat.class); configuration.setvertexoutputformatclass(vertexoutputformat.class); configuration.setworkerconfiguration(nbworkers, nbworkers, 100.f); GiraphFileInputFormat.addVertexInputPath(configuration, inputpath); GiraphJob job = new GiraphJob(configuration, "Test Giraph"); FileOutputFormat.setOutputPath(job.getInternalJob(), outputpath); if(!job.run(true)) System.exit(1); } } System.exit(0); Pour conclure, on peut voir que la programmation d une application Giraph est une tâche relativement simple. Le framework est vraiment adapté au traitement de graphe ce qui rend de nombreux algorithmes faciles à écrire. Cependant, il impose certaines contraintes en grande partie liées au modèle qui pousse généralement à repenser les algorithmes en partie ou totalement. MPI «MPI (The Message Passing Interface), conçue en 1993-94, est une norme définissant une bibliothèque de fonctions, utilisable avec les langages C, C++ et Fortran. Elle permet d'exploiter des ordinateurs distants ou multiprocesseur par passage de messages. Elle est devenue de facto un standard de communication pour des nœuds exécutant des programmes parallèles sur des systèmes à mémoire distribuée. MPI a été écrite pour obtenir de bonnes performances aussi bien sur des machines massivement parallèles à mémoire partagée que sur des clusters d'ordinateurs hétérogènes à mémoire distribuée. Elle est disponible sur de très nombreux matériels et systèmes d'exploitation. Ainsi, MPI possède l'avantage par rapport aux plus vieilles bibliothèques de passage de messages d'être grandement portable (car MPI a été implantée sur presque toutes les architectures de
mémoires) et rapide (car chaque implantation a été optimisée pour le matériel sur lequel il s'exécute).» 2 Travail réalisé Mise en place de l environnement de développement Au début de mon stage, j ai dû mettre en place un environnement de développement Giraph afin d effectuer avec quelques tests, de mesurer les performances et la facilité de programmation du framework. C est avec l aide de l administrateur système que j ai à plusieurs reprises testé des versions de Giraph sans succès. Nous avons finalement décidé d utiliser la dernière version de Giraph qui est actuellement en développement (version 1.0.0-dev) car c est la seule qui fonctionnait bien et qui restait récente. Après cette étape, j ai pris en main la plateforme de contrôle Cloud OpenNebula afin de créer ensuite ma première application Giraph. Applications Giraph La première application Giraph que j ai développé est le test de connexité que j ai présenté précédemment en tant qu exemple d application. J ai développé une seconde application qui consiste à appliquer un algorithme parallèle de détection cliques nommée PECO 3 (Parallel Enumeration of Cliques using Ordering) avec Giraph. L algorithme PECO se décompose en deux parties. Une première phase va réunir les arcs pointant vers un nœud sur le nœud en question en question. Une seconde phase va créer le graphe environnant (union des arcs entrant avec les arcs sortant) de chaque nœud puis va faire appel à une adaptation de l algorithme séquentiel Tomita 4 (modifié pour pouvoir mieux être utilisé dans un contexte parallèle). C est l algorithme Tomita qui va se charger d énumérer l ensemble des cliques maximales dans le sous graphe environnant. En faisant l union de toutes les cliques trouvées pour chaque sous graphes générés, on obtient l ensemble des cliques maximales du graphe d entrée. Lorsque j ai mis au point l application Giraph appliquant l algorithme PECO, j ai dû mettre au point un système de log afin de déboguer mon application plus simplement en voyant ce qui s y 2 http://fr.wikipedia.org/wiki/message_passing_interface 3 http://www.public.iastate.edu/~svendsen/research/thesis.pdf 4 E. Tomita, A. Tanaka, and H. Takahashi. The worst-case time complexity for generating all maximal cliques and computational experiments. Theor. Comput. Sci., 363:28 42, October 2006.
passe et j ai utilisé des fonctionnalités plus avancées de Giraph en programmant mon propre type de donnée associé au nœud afin de stocker des données entre les superstep et mon propre type de message afin de communiquer des données plus complexes entre les nœuds. Il est important de faire attention à ce que tous les types données de l application présents dans les nœuds du graphe ou qui sont échangés entre les nœuds soient bien sérialisables, car dans le cas contraire cela causerait des bogues dans l application. En effet, Giraph peut avoir besoin de sérialiser ce type de données lorsque la mémoire disponible sur les machines de calculs n est plus suffisante pour effectuer la suite du traitement ou pour d autres raisons liées aux performances (déplacement d un nœud sur une machine qui le sollicite fortement afin de réduire le coût des communications réseau). L algorithme PECO est intéressant par sa simplicité de mise en place mais présente un défaut majeur : les graphes environnant des nœuds se chevauchent fortement. Cela signifie que les cliques maximales qui en résultent peuvent être dupliquées. Cela signifie aussi que certains calculs effectués en parallèle vont être dupliqués. Cela met en défaut l efficacité de l algorithme. De nombreux graphes issus de réseaux réels présentent des propriétés intéressantes. L une d elle est l hétérogénéité de leur densité. Pour étudier une telle propriété, il peut être intéressant de pouvoir reconnaitre des régions fortement connexes d un graphe. Ces régions contiennent souvent des cliques mais n en sont pas forcément une et bien que l énumération de cliques maximale puisse nous aider à localiser de telles régions, cela est difficile à faire. Il existe des algorithmes plus adaptés à ce problème permettant de faire ce type d analyse. La recherche de P-SCC (P strongly connected components) permet de résoudre relativement simplement le problème. Une P-SCC est un ensemble de nœuds de telle sorte que pour tout couple de nœuds dans le graphe la distance allerretour entre ces deux nœuds est inférieure ou égale à P. Sur de grands graphes, la recherche de composantes fortement connexes peut devenir difficile. Dans ce cas, les algorithmes se basant sur un simple parcours en profondeur limitée semblent être l une des solutions les plus pertinentes. J ai donc finalement mis au point une autre application Giraph permettant de réaliser une P- SCC sur chaque nœud. La recherche en profondeur étant difficilement adaptée au modèle Pregel, j ai finalement réalisé un algorithme effectuant, sur chaque nœud, deux recherches en largeur en profondeur limitée (de longueur deux fois plus petite) puis une recherche en profondeur limitée est appliquée localement sur le sous-graphe généré par l union les deux recherches en largeur afin d obtenir finalement la P-SCC. La première recherche en largeur a pour but de déterminer l ensemble des nœuds atteignables en effectuant la moitié de la distance aller-retour dans le sens de l aller. La seconde effectue le même calcul, mais dans le sens du retour. La recherche en profondeur locale sert à supprimer les nœuds résiduels qui ne sont pas à une distance aller-retour inférieur ou égal à P. La recherche en largeur étant appliqué localement, elle peut être appliquée rapidement. Les
deux recherches en largeurs successives ont pour but premier de limiter l explosion combinatoire engendré par une recherche en profondeur limitée sur des gros graphes. Programmes complémentaires Afin de mener mon stage à bien, j ai dû programmer de petits scripts utiles. L un des premiers scripts que j ai dû réaliser est un générateur de graphes. Sachant que j allais travailler sur des algorithmes de détection de cliques au début du stage, j ai mis au point un script python générant un graphe contenant un nombre paramétré de cliques de tailles paramétré elle aussi. Ces cliques sont reliées entre elle avec un certain nombre d arcs et le nombre d arc dépend d un paramètre définissant la densité du graphe. Un script comme celui-ci est très pratique pour vérifier simplement si un algorithme de détection de cliques fonctionne, car en pratique, les graphes aléatoires ou issus de réseau sociaux par exemple peuvent contenir des cliques qui se chevauchent et cela devient donc difficile de tester la validité d un algorithme de détection de clique sur des graphe de taille conséquente (plusieurs centaines de nœud à plusieurs dizaines de milliers). Durant la suite de mon stage, j ai été confronté à des graphes qui n étaient pas toujours dans le format requit par mes applications, c est-à-dire sous forme de listes d adjacences. Il est fréquent de trouver des graphes sous forme de listes d arcs. J ai donc programmé un script de conversion d un graphe sous forme de liste d arcs en un graphe sous forme de liste d adjacence. Le script suppose que le graphe d origine et de destination peuvent être stockés en mémoire. Afin de bien comprendre et de tester le fonctionnement d un algorithme, j ai parfois programmé un script l appliquant sur de petits graphes comme l algorithme des P-SCC que j ai ensuite réutilisé pour créer une application Giraph. Cela permettait de vérifier l application Giraph de manière plus simple. En effet, dans ce cas, il suffit juste de comparer le résultat du script avec celui de l application Giraph écrit dans le même format plutôt que de chercher à le comparer avec une solution existante qui est parfois difficile à trouver et qui peut être dans un format différent. Pour finir, j ai tenté de réaliser un algorithme de clustering de graphe qui avait pour but d accélérer le programme MPI que j ai réalisé à la fin de mon stage. L algorithme utilisé pour découper le graphe est plutôt simple. Voici le pseudo-code de l algorithme :
Fonction clustering (graphe, nombregroupes) tasnoeuds = tableau de taille nombregroupes contenant des listes vides numerotas = 1 taillemaxgroupe = taille(graphe) / nombregroupes + 1 Tant que graphe non vide noeud = nœud quelconque dans graphe limite = taillemaxgroupe - taille(tasnoeuds[numerotas]) listenoeuds = BFS_Limité(noeud, limite) tasnoeuds[numerotas] = tasnoeuds[numerotas] + listenoeuds Pour chaque nœud n dans listenoeuds Supprimer n de graphe Fin pour Si taille(tasnoeuds[numerotas]) >= taillemaxgroupe numerotas = numerotas + 1 Fin si Fin tant que Fin fonction Notez que la fonction BFS_Limité(N, L) réalise un parcours en largeur du nœud N en s arrêtant dès que L nœuds ont été parcourus et en renvoyant cette liste de nœuds parcourus. Programme MPI Fonctionnement Afin d évaluer la facilité de programmation, la souplesse et l efficacité de Giraph, j ai décidé de comparer ce framework avec MPI C++ en mettant au point une recherche de P-SCC. Afin d éviter de trop complexifier le programme MPI, j ai supposé qu il disposerait d assez d espace libre en mémoire sur chacune des machines où il sera exécuté. Le format de fichier lu est le même que pour l application Giraph : une liste d adjacence sous forme de fichier texte dans laquelle une ligne représente une entrée de la liste. Mais à l exception du programme Giraph, le programme MPI suppose que les entrées de la liste d adjacence sont triées en fonction du nœud source. On notera par N le nombre de processus MPI exécuté par l application (défini lors du lancement). La première chose que fait le programme MPI est de lire le fichier d entrée dans le processus maître pour lire le nombre de lignes et ainsi connaitre le nombre de nœuds présents dans le graphe. Une fois fait, le programme va découper le graphe en autant de partie qu il y a de processus MPI. Pour réaliser ça, le fichier d entrée est lu une seconde fois par le processus maître. Lors de la lecture les lignes sont envoyées au processus MPI distant de manière bufférisées. Une fois la lecture
terminée, le graphe d entrée est réparti sur tous les processus MPI de manière équitable. À ce moment chaque processus envoie à tous les autres l intervalle d identifiants de nœuds de graphe qu il détient afin que les autres processus puissent savoir sur quelle machine chaque nœud peut être. Une fois cette opération réalisée le traitement peut commencer. Lors du traitement des données, chaque processus MPI va initier un DFS (parcourt en profondeur) sur les nœuds qu il détient. Durant le parcourt du DFS, certains nœuds du graphe ne sont pas détenus par le processus et celui-ci commande donc au processus distant qui détient le nœud de continuer le DFS. Lorsque le DFS passe par le nœud source, c est qu il existe un chemin aller-retour vers le nœud et donc que l ensemble des nœuds du chemin font partie de la P-SCC du nœud source. Les nœuds du chemin sont donc stockés dans un ensemble formant une P-SCC partielle. Lorsque le DFS retourne à la source et ne peut plus traiter d autres nœuds, la P-SCC partielle devient une P-SCC totale et on peut initier le DFS d un autre nœud qui n aurait pas été déjà traité. Afin d accroitre les performances du programme, chaque processus démarre l exécution de plusieurs DFS en parallèle et bufférise les envois de données. Les processus MPI du programme effectue en somme, un DFS distribué sur chaque nœud du graphe. Les P-SCC partielles et totales des nœuds du graphe, pouvant être relativement grosses, sont stockées dans un fichier de solution sur le disque évitant ainsi une explosion de la quantité de mémoire nécessaire par les processus MPI. Lorsque tous les nœuds locaux à un processus sont traités, celui-ci envoie au processus maître qu il a terminé son travail, mais reste disponible afin d effectuer une partie des DFS des nœuds des autres processus. Lorsque tous les nœuds ont terminé leur travail, le processus maître le sait (car il compte le nombre de processus qui ont terminé leur travail et le nombre de processus total lancés) et demande à tous les processus de s arrêter. Le programme distribué prend alors fin lorsque tous les processus ont reçu et traité ce message. Résultat des tests et comparatif Après avoir programmé plusieurs applications Giraph, j ai testé la montée en charge du framework et j ai rencontré quelques problèmes sur l application la plus simple : le teste de connexité. Bien que l application se comporte bien sûr des graphes de tailles relativement grandes (environ 3 millions de nœuds et 117 millions d arcs) mais ne fonctionne plus sur des graphes de très grandes tailles (environ 52 millions de nœuds et 2 milliard d arcs). Selon les concepteurs de Giraph cela pourrait être un problème de charge mémoire générique à Java et ils encouragent donc l utilisation de bibliothèques permettant de réduire l occupation de mémoire dans les applications Java. Cependant, ce problème ne peut pas être résolu simplement par les utilisateurs sur des graphes
aussi gros, car il n est pas possible d utiliser ces bibliothèques sur des exemples aussi simples. Néanmoins, les concepteurs de Giraph travaillent activement sur le problème et tentent de limiter l impact du framework sur la mémoire et de gérer toujours de plus gros volume de données. Les prochaines versions de Giraph devraient donc permettre de lancer des applications comme un test de connexité sur des graphes de plusieurs milliards d arcs sans problème. Le programme MPI est présent en deux versions. La première version semble bien fonctionner mais est relativement lente sur des graphes de taille moyenne. La seconde version est bien plus rapide mais contient quelques bogues qui n apparaissent que lorsque plusieurs processus sont utilisés pour réaliser le traitement. Dû aux problèmes rencontrés avec Giraph et MPI, il est difficile de comparer les performances des deux frameworks. Néanmoins, on peut quand même voir que la programmation d application Giraph est bien plus simple, car si l on veut réaliser une application MPI traitant des graphes, il faut obligatoirement programmer la lecture du fichier en faisant attention à la mémoire et répartir ce fichier sur plusieurs processus. Ces opérations sont relativement lourdes et peuvent vite devenir complexes sur des gros graphes. De plus, si l on veut aussi faire attention à ne pas dépasser la limite de mémoire, il faut manipuler une grande partie des données temporaires sur le disque dur ce qui rend la programmation MPI bien plus complexe et donc moins accessible. Dans les deux cas, le débogage d applications distribuées est complexe, mais les applications MPI sont plus assujetties à des bogues (inter-blocage, problèmes de synchronisations, etc.) dû à la nature même du contrôle des communications dans un environnement hétérogène distribué. Détail du contenu du SVN L ensemble des fichiers du stage se trouve dans le dossier nommé «Giraph» présent à la racine du SVN. Dedans on y trouve l architecture de fichier suivante : Dossier «application» : contient la liste des applications Giraph réalisées faisant référence à la partie «Applications Giraph» du rapport (contenant un script d upload configurable permettant de compiler et d envoyer rapidement une application Giraph sur le serveur). Dossier «doc» : contient la liste des documents trouvés ou utilisés durant le stage (thèse, rapports, etc.). Dossier «script» : contient la liste des scripts réalisés faisant référence à la partie «Programmes complémentaires» du rapport.
Dossier «server-scripts» : contient des scripts pouvant être utile pour le lancement de tâches Giraph ou l administration des machines virtuelles Cloud. o «broadcast.sh» : Exécute une commande passée en paramètre sur toutes les machines virtuelles Cloud en parallèle. o «init.sh» : Script de lancement et d initialisation d Hadoop (nettoie les logs des applications Giraph et du HDFS puis lance Hadoop en formatant le HDFS ensuite) o «end.sh» : Script d arrêt d Hadoop (arrête Hadoop de manière sûr). o «testmin.sh», «testpeco.sh» et «testpscc.sh» : Scripts permettant de tester simplement des tâche Giraph (envoi les données sur le HDFS, lance Giraph en mesurant le temps d exécution, affiche le résultat et nettoie le HDFS). o «clean_dfs.sh» : Sous script permettant de nettoyer le DFS Hadoop. o «clean_logs.sh» : Sous-script nettoyant les logs généré par les applications Giraph. Fichier «notes» : notes décrivent quelques règles à suivre ou observations faites lorsqu on programme avec Giraph. Afin d éviter de répéter plusieurs fois la structure d une application Giraph je ne vais présenter que la structure de l application la plus complète : celle qui applique les P-SCC : Dossier «sample_data» : Contient des jeux de données d exemple pour l application. Dossier «src» : Contient les sources de l application. o Dossier «main» : Contient les sources du programme de l application. Dossier «assembly» Fichier «compile.xml» : Précise comment l archive JAR va être construite. Suite de dossiers «java/lifo/giraph/test» Fichier «Arc» : Classe définissant un arc dans un graphe. Fichier «Logger» : Classe permettant de logger du texte durant l exécution de l application pour savoir ce qu elle fait. Fichier «Main» : Classe principale permettant de démarrer la tâche Giraph. Fichier «Message» : Classe définissant les messages qui transite entre les nœuds du graphe traité. Fichier «VertexComputation» : Classe contenant l algorithme qui traite le graphe. Fichier «VertexData» : Classe définissant les données associé à un nœud du graphe traité.
Fichier «VertexInputFormat» : Classe spécifiant comment lire le graphe d entrée. Fichier «VertexOutputFormat» : Classe spécifiant comment générer le fichier de sortie. Fichier «makefile» : Fichier permettant de compiler simplement l application en lançant uniquement la commande «make» sous Linux (lance Maven en interne). Fichier «pom.xml» : Fichier de configuration de projet pour Maven précisant comment compiler le projet. Bilan Dans un premier temps nous avons étudié le fonctionnement du modèle de calcul Prégel, dédié à la programmation de graphe. Puis, nous avons détaillé un exemple d application Giraph, une implémentation open-source de Pregel où nous avons vu qu il est possible de réaliser simplement des programmes appliquant des algorithmes distribués de traitement de graphe. Finalement, nous avons entrevu la technologie MPI, un standard de communication pour des machines exécutant des algorithmes distribués. Dans un second temps, j ai décrit le fonctionnement des applications Giraph et MPI que j ai réalisées en tentant finalement de comparer les deux technologies, où malgré quelques problèmes techniques, on remarque que la programmation MPI n est pas adaptée à l implémentation d algorithmes s appliquant graphe (en particulier lorsque le format est une liste d adjacence), surtout en raison de sa complexité. Il aurait été intéressant de tester des alternatives au framework Giraph se basant sur MPI en C++ par exemple, comme GraphLab, ou encore se basant sur des systèmes relationnels comme Neo4j. Ces technologies semblent être des alternatives pertinentes à Giraph et comme lui, elles semblent disposer d un avenir prometteur.