Rapport
Transcription
Rapport
Département informatique Diplôme préparé : DUT informatique Numéro de jury : 4 Implémentation d’algorithmes pair à pair et amélioration des interfaces utilisateur sur le simulateur SimGrid Samuel LEPETIT Tuteur enseignant Maître d’apprentissage Chantal ESCUDIÉ Bât.407 Université Paris-Sud 91400 Orsay France Martin QUINSON Equipe AlGorille, Batiment B LORIA – Campus Scientifique BP 239 – 54506 Vandoeuvre-lès-Nancy Cedex Année universitaire 2011-2012 16 Avril 2012 au 6 juillet 2012 Soutenance du 18 juin 2012 1 TABLE DES MATIÈRES Table des matières 1 Institution d’accueil 1.1 Le LORIA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 L’équipe AlGorille . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 6 2 Contexte et objectifs du stage 2.1 Applications distribuées . . . . . . . . . . . . . . 2.2 SimGrid . . . . . . . . . . . . . . . . . . . . . . . 2.3 l’interface MSG . . . . . . . . . . . . . . . . . . . 2.3.1 Fichiers de plate-forme et de déploiement 2.3.2 Les processus . . . . . . . . . . . . . . . . 2.3.3 Communication entre les processus . . . . 2.4 Objectifs du stage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 8 8 8 9 9 3 Implémentation d’algorithmes pair à pair 3.1 Le pair à pair . . . . . . . . . . . . . . . . . . . . . . 3.2 Chord Java . . . . . . . . . . . . . . . . . . . . . . . 3.3 Kademlia . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Messages . . . . . . . . . . . . . . . . . . . . 3.3.2 Tables de routage . . . . . . . . . . . . . . . . 3.3.3 Protocole pour rejoindre le réseau . . . . . . 3.3.4 Détails sur l’implémentation . . . . . . . . . . 3.4 Bittorrent . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Le protocole entre pairs . . . . . . . . . . . . 3.4.2 Algorithme de choix de la pièce à télécharger 3.4.3 Protocole de choix des pairs actifs . . . . . . 3.4.4 Détails sur l’implémentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 10 11 13 13 13 14 14 14 16 16 16 17 . . . . . . . 18 18 18 19 19 20 21 21 . . . . . . 23 23 24 25 25 26 27 . . . . . . . 4 Amélioration des interfaces utilisateur 4.1 Amélioration de l’interface Java pure . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Refactorisation du code de SimGrid Java . . . . . . . . . . . . . . . . 4.1.2 Implémentation des communications asynchrones . . . . . . . . . . . . 4.1.3 Changement de la manière dont les processus sont lancés . . . . . . . 4.1.4 Mise en cache des éléments Java . . . . . . . . . . . . . . . . . . . . . 4.1.5 Ajout d’une interface pour la génération de nombres pseudo-aléatoires 4.2 Création d’une interface java à très hautes performances . . . . . . . . . . . . 5 Évaluation expérimentale 5.1 Cache des éléments Java . . . . . . . . . . . . . . . . 5.2 Lancement des processus coté C . . . . . . . . . . . 5.3 Interface haute performance utilisant les Coroutines 5.4 Expériences sur l’algorithme Chord . . . . . . . . . . 5.5 Amélioration globale des performances . . . . . . . . 5.6 Complexité en terme de quantité de code . . . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . RÉSUMÉ Résumé Ce stage de fin de DUT Informatique s’est déroulé au LORIA[4] à Nancy, dans l’équipe AlGorille, sous la direction de Martin QUINSON, du 15 avril au 6 juillet 2012. J’ai eu l’occasion durant ce stage de travailler sur le projet SimGrid, qui est une boite à outils permettant de concevoir des simulateurs d’applications distribuées dans des environnements hétérogènes, permettant notamment de faire des simulations sur des algorithmes pair à pair. SimGrid, bien que programmé en C, possède également des interfaces permettant de l’utiliser dans un autre langage, dont une interface Java. Le pair à pair est un système d’organisation décentralisé d’un réseau opposé au schéma clientserveur classique, où chaque ordinateur dans le réseau est à la fois un client et un serveur, qui a diverses applications. Différents algorithmes permettant d’organiser les réseaux de nœuds ont ainsi été proposés. Les deux objectifs principaux de ce stage étaient l’implémentation d’algorithmes pair à pair afin de les tester sur SimGrid et l’amélioration de l’interface Java existante. Les algorithmes Kademlia et Bittorrent ont ainsi pu être implémentés sur la plateforme SimGrid, en C et en Java, afin de proposer des exemples aux utilisateurs mais également pour comparer les performances entre les interfaces C et Java, et enfin pour dépister des bogues dans SimGrid lui même. Une implémentation de l’algorithme Chord en Java a également été réalisée. Également, des travaux ont été réalisés afin d’améliorer les performances de l’interface Java de SimGrid et de la pourvoir des mêmes fonctionnalités que l’interface C . Des évaluations expérimentales sur Grid’5000[2] ont pu être réalisées, permettant de prouver expérimentalement le gain de performances de chacune des modifications apportées à l’interface Java, et montrant un gain de performance d’environ 50%. Ces expériences ont également pu montrer que l’interface expérimentale développée permet de diviser le temps d’exécution des simulations par 5 environ. 3 RÉSUMÉ Remerciements Je tiens à remercier Martin QUINSON, mon maitre de stage pour m’avoir accueilli et encadré durant tout ce stage, mais aussi pour m’avoir permis d’acquérir de nouvelles connaissances et de découvrir la recherche informatique publique. Par ailleurs, je remercie toute l’équipe AlGorille pour leur accueil, et pour m’avoir intégré durant ce stage. Également, je tiens à remercier Christophe THIÉRY, Marion GUTHMULLER et Sebastien BADIA pour leurs réponses à toutes mes questions, nombreuses durant ce stage. D’une manière générale, je tiens à remercier le LORIA. Je remercie également Mme ESCUDIE pour son encadrement, ainsi qu’à toute l’équipe de l’IUT pour m’avoir permis de mener à bien ce stage. 4 INTRODUCTION Introduction Aujourd’hui, les environnements distribués prennent une place de plus en plus importante dans l’informatique : ceux-ci deviennent de plus en plus complexes, sont soumis à de plus en plus de contraintes. Les architectures pair à pair font parti des environnements distribués les plus étudiés aujourd’hui. Or, comme expliqué dans The state of peer to peer simulators and simulations[9], il n’existait pas jusqu’à présent de simulateur standard pour le pair à pair répondant à tous les critères et utilisé de façon globale : la plupart (62%) des articles scientifiques utilisent un simulateur spécifiquement développé pour l’occasion, et une partie n’en utilisaient pas du tout. L’objectif de ce stage était donc de répondre à cet article scientifique en proposant SimGrid comme réponse. SimGrid disposait déjà de quasiment tout les éléments pour permettre la simulation d’algorithmes pair à pair, seulement deux étant manquants : Tout d’abord, le manque d’exemples d’implémentation d’algorithmes pair à pair classiques de la littérature pour les utilisateurs. Ensuite, l’interface Java ne possédait pas toutes les fonctionnalités de l’interface C et avait des problèmes importants de performances. Or, les utilisateurs souhaitent souvent pouvoir écrire leurs simulations dans un langage plus haut niveau que le C, et Java est aujourd’hui un langage répandu, très demandé par les utilisateurs, et utilisé par la majorité des simulateurs pair à pair aujourd’hui. Dans le cadre de mon stage de fin de DUT, j’ai ainsi pu faire divers travaux sur le simulateur SimGrid, sur deux axes principaux : l’implémentation d’algorithmes pair à pair et l’amélioration des interfaces utilisateurs, notamment l’interface Java. Je vais donc tout d’abord évoquer le contexte dans lequel s’est effectué mon stage, en présentant le LORIA, l’équipe AlGorille ainsi que le projet SimGrid. Ensuite, je développerai en détail mes contributions dans le projet SimGrid sur les implémentations d’algorithmes pair à pair ainsi que sur l’amélioration des interfaces utilisateurs, avant de faire un bilan sur l’avancement actuel. 5 INSTITUTION D’ACCUEIL 1 Institution d’accueil 1.1 Le LORIA Le LORIA 1 [4] est une unité mixte de recherche, qui est commune à plusieurs établissements : – Le CNRS : Centre National de la Recherche Scientifique. – L’Université de Lorraine (anciennes Université Henri Poincarré 1 et Université Nancy 2). – l’INRIA, organisme de recherche public dédié aux sciences et technologies du numérique. Il comporte 27 équipes, qui sont structurées en 5 départements, pour un total d’environ 500 personnes, dont : – 105 enseignants chercheurs – 63 chercheurs – 14 personnels administratifs – 130 doctorants – 105 post-doctorants – 50 ingénieurs sur contrat – 50 stagiaires Le travail de recherche au LORIA est articulé autour de 5 thématiques qui forment chacune un département : – algorithmique, calcul, image et géométrie, – méthodes formelles, – réseaux, systèmes et services, – traitement automatique des langues et des connaissances, – systèmes complexes et intelligence artificielle 1.2 L’équipe AlGorille J’ai été accueilli lors de mon stage dans l’équipe AlGorille[1] (Algorithmes pour la Grille). Le champ d’activité de l’équipe se situe principalement au niveau du calcul distribué et de l’accès efficace à des ressources distribuées, un des enjeux majeurs aujourd’hui. L’équipe AlGorille appartient au département "Réseaux, systèmes et services". L’équipe AlGorille est composée de 4 membres permanents, 3 doctorants, 2 membres associés, 4 ingénieurs de recherche. Elle est actuellement dirigée par Jens GUSTEDT. Elle contient également une partie des chercheurs, développeurs et doctorants travaillant sur le projet Simgrid, qui est un outil pour la simulation d’applications distribuées, avec qui j’ai pu travailler durant ce stage. AlGorille est également responsable de l’administration de Grid’5000 à Nancy, qui est une plateforme expérimentale, distribuée sur 10 sites, donc l’objectif est d’étudier les systèmes distribués sur une grande échelle. 1. Laboratoire de recherche en informatique et ses applications 6 CONTEXTE ET OBJECTIFS DU STAGE 2 Contexte et objectifs du stage 2.1 Applications distribuées Aujourd’hui, les applications distribuées sont de plus en plus populaires. Celles-ci permettent de partager des informations distantes, en exécutant un programme sur plusieurs ordinateurs, qui communiquent entre eux à l’aide d’un réseau (généralement sur un protocole IP). Le pair à pair (P2P, "Peer to peer" en anglais) est par exemple un des modèles réussis des applications distribuées : chaque nœud est alors à la fois client (il récupère des données) et serveur (il transmet des données aux autres nœuds du système) : cela supprime la nécessité d’avoir un serveur central pour distribuer les données. 2.2 SimGrid SimGrid[5] est un projet mené conjointement par Martin QUINSON (LORIA, Université de Lorraine), Arnaud LEGRAND (CNRS Grenoble), Henri CASANOVA (Université d’Hawaii à Manoa) et Frederic SUTER, débuté en 2000 [3]. SimGrid a pour objectif de fournir une boîte à outils pour la simulation d’applications distribuées diverses, dans des environnements qui peuvent être hétérogènes. Il existe deux manières d’appréhender un problème dans le cas d’un système distribué : Tout d’abord, l’expérimentation sur plateforme réelle (par exemple sur Grid’5000) : ce qui permet de vérifier dans des conditions réelles la validité d’une solution. Cependant, elle impose de nombreuses contraintes : l’application doit être entièrement fonctionnelle, il est difficile de reproduire les résultats. Ensuite, la simulation, qui permet de construire des expérimentations, sans avoir à construire le vrai système (ni même l’application complète), mais en utilisant un modèle. Elle autorise une reproductibilité plus facile des expériences. Cependant, étant donné que l’on utilise un modèle de l’application plutôt que l’application réelle, on ne peut trouver des problèmes liés à la mise en production de l’application réelle. Actuellement, la plupart des simulateurs pour les systèmes distribués sont conçus à usage unique, ne sont généralement pas maintenus dans le temps, et ont de piètres performances. L’objectif du projet SimGrid est de fournir une boite à outils permettant la simulation d’applications distribuées, de façon à proposer des résultats réalistes tout en proposant de très hautes performances. C’est un logiciel libre sous licence LGPL, les sources sont donc librement disponibles sur le site Internet du projet. Le projet utilise le système de gestion de versions Git pour gérer son code source. SimGrid est actuellement financé par l’ANR (Agence nationale de la recherche) dans le cadre du projet SONGS 2 . SimGrid est modulaire : il est divisé en plusieurs modules et propose plusieurs API utilisables pour l’utilisateur. SimGrid propose plusieurs API 3 , selon les besoins de la simulation que l’utilisateur souhaite faire : – MSG, pour les utilisateurs souhaitant simuler des applications distribuées sans objectif de les mettre en œuvre dans la réalité. – SimDag, pour exprimer les simulations sous la forme de graphes de dépendances de taches parallèles. – SMPI, pour les utilisateurs souhaitant utiliser des applications MPI 4 dans SimGrid, simplement en recompilant l’application. SimGrid permet la reproductibilité des simulations : chaque exécution d’une simulation donnera exactement le même résultat sur une même version de SimGrid. Les utilisateurs peuvent reproduire les expériences présentées par exemple dans les articles scientifiques. Des interfaces vers d’autres langages de programmation sont également disponibles (Java, Lua, Ruby). 2. Simulation of next generation systems : Simulation de systèmes de nouvelle génération 3. Application programming interface : Interface utilisée par les utilisateurs de l’application 4. Message Passing Interface 7 CONTEXTE ET OBJECTIFS DU STAGE Figure 1 – Schéma des différents modules de SimGrid Durant mon stage, j’ai eu l’occasion d’utiliser l’interface MSG de SimGrid, mais aussi de travailler sur l’interface Java de SimGrid. 2.3 l’interface MSG L’objectif de MSG dans SimGrid est de faciliter l’écriture de simulations qui n’ont pas pour objectif d’être exécutables sur une vraie plate-forme (contrairement à GRAS ou SMPI). L’objectif de MSG est de permettre de tester de façon simple un algorithme pour un problème sans l’implémenter réellement. MSG utilise plusieurs abstractions : – Hôte : Endroit où les processus s’exécutent : il est défini par un nom et une puissance de calcul – Processus : Ensemble de données privées et de code qui s’exécutent sur un Hôte. – Tâche : Représente une quantité de calcul/de communications – Mailbox (Boite aux lettres) : Point de rendez-vous entre deux processus qui souhaitent communiquer : les processus font des requêtes d’envoi et de réception sur une mailbox, et le simulateur lance la communication quand un envoi (send) correspond à une réception (receive). 2.3.1 Fichiers de plate-forme et de déploiement SimGrid utilise des fichiers de plate-forme et des fichiers de déploiement écris en XML pour décrire la simulation que l’on souhaite exécuter. Ceux-ci permettent de décrire deux choses : tout d’abord, la plate-forme sur laquelle la simulation s’exécute, c’est à dire l’ensemble des machines hôtes (leur nom, leur puissance) et les liens réseau entre elles . Ensuite, le fichier de déploiement permet de décrire quels processus s’exécutent sur quels hôtes, avec quels arguments. 2.3.2 Les processus Les API de SimGrid utilisant SIMIX (pour Simulated POSIX ) se basent sur un système de processus pour décrire la simulation. Ces processus sont définis par une fonction principale prenant à la manière d’un programme C des arguments. Ces processus sont exécutés pas à pas par le simulateur, et font des appels systèmes SIMIX pour leurs actions (exécuter une tâche, envoyer une tâche, commencer à recevoir une tâche, s’endormir pendant X secondes), à la manière d’un processus dans un système d’exploitation. 8 CONTEXTE ET OBJECTIFS DU STAGE SimGrid, dans Maestro, décide de réveiller les processus quand ceux-ci terminent leurs actions (on parle de "schedule"), et de les endormir quand ceux-ci sont en attente de quelque chose dans la simulation. Cela peut également être utilisé en parallèle , selon un modèle N threads pour M processus à exécuter. 2.3.3 Communication entre les processus Dans MSG, les processus communiquent en envoyant des tâches, qui représentent des messages envoyés dans le cadre du pair à pair, mais qui peuvent également représenter des données à calculer. Ces tâches sont définies par plusieurs éléments : un nom (qui est optionnel), une quantité de calcul, une quantité de données dans le cadre d’une communication, et des données arbitraires qui sont définies par l’utilisateur. Les processus font ensuite une demande d’envoi sur une mailbox. Si un autre processus a fait une demande de réception sur cette même mailbox, la tache est transférée du processus émetteur vers le processus récepteur. 2.4 Objectifs du stage SimGrid dispose déjà de tous les éléments permettant de l’utiliser comme simulateur pair à pair de façon très performante. Cependant, certains éléments n’étaient pas complètement finis : des implémentations d’algorithmes classiques de la littérature pair à pair manquaient. Seule une implémentation de l’algorithme Chord[6] était disponible. Enfin, l’interface Java de SimGrid n’était pas complète, certaines fonctionnalités étaient manquantes et l’interface posait des problèmes de performances. Mon travail s’est donc orienté sur ces deux points : proposer des exemples d’algorithmes classiques de la littérature pair à pair, et travailler sur l’interface Java de SimGrid. L’intérêt des exemples d’implémentation est multiple. Tout d’abord, ces exemples servent de servir d’introduction au fonctionnement de l’API MSG pour les utilisateurs débutant dans SimGrid, afin de voir comment réaliser une implémentation de protocole pair à pair. Ensuite, cela permet aux scientifiques utilisant le projet de tester leurs propositions. Enfin, cela a aussi pour objectif de tester l’infrastructure : des bugs dans SimGrid ont pu être détectés et corrigés durant l’écriture des implémentations des algorithmes. 9 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR 3 Implémentation d’algorithmes pair à pair SimGrid dispose aujourd’hui de tous les éléments pour permettre la simulation d’algorithmes pair à pair sur un nombre très important de nœuds (jusqu’à 1000000 nœuds ont pu été simulés sur l’algorithme Chord[7]). Un des objectifs principaux de mon stage a donc été d’implémenter des algorithmes pair à pair sur l’environnement SimGrid, en utilisant MSG en C et en Java, cela pour plusieurs choses. Tout d’abord, comme exemples pour les utilisateurs de SimGrid. Ensuite, pour permettre d’évaluer les performances de SimGrid-Java en comparaison avec SimGrid C (en implémentant les mêmes algorithmes). Un des principaux problèmes s’est posé dans l’implémentation en Java de ces algorithmes : certains éléments étaient manquants pour pouvoir correctement implémenter ces algorithmes, notamment l’implémentation des communications asynchrones entre les processus. 3.1 Le pair à pair Le pair à pair est connu du grand public notamment, depuis Napster (un des premiers précurseurs des réseaux pair à pair, fermé en 2001), et pour son utilisation à des fins de téléchargement illégal. Cependant, le pair à pair est également un sujet d’étude pour les chercheurs, car ceux-ci possèdent des caractéristiques intéressantes. Il existe deux schémas d’organisation dans un réseau : – Le schéma client-serveur, ou un serveur envoie les données qui sont récupérées par un client – Le schéma pair à pair, décentralisé, où chaque "nœud" du réseau transmet des données aux autres nœuds pour récupérer les données, sans hiérarchie entre les nœuds. Ces nœuds sont également appelés des "pairs". Figure 2 – Schéma des deux types d’organisation d’un réseau. A gauche, le schéma client/serveur, et à droite, le schéma pair à pair. La première génération de réseaux pair à pair (type Napster, EDonkey) utilisait un serveur central pour faire passer les informations, ce qui posait des problèmes assez évidents : à partir du moment où le serveur central était indisponible, tout le réseau était indisponible. La seconde génération de réseaux pair à pair (type Gnutella) n’utilisait pas de serveur centralisé. Elle souffrait de nombreux problèmes : ces réseaux étaient très peu efficaces, utilisant notamment de nombreux broadcast 5 pour localiser les informations cherchées. La complexité en terme de nombres de nœuds contactés pour obtenir une information dépassait O(n), ce qui est problématique : ces réseaux ne pouvaient pas passer à l’échelle, ils devenaient lents dès que le nombre de connectés augmente. Les algorithmes pair à pair proposés par la suite ont donc eu pour objectif de permettre d’organiser les réseaux de pairs pour atteindre des objectifs de performance (localiser rapidement d’autres nœuds 5. Envoi à tout les nœuds que le nœud connait d’une requête, qui vont eux-même envoyer une requête, ... 10 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR et les données associées), de redondance (empêcher les pertes de données quand un nœud quitte le réseau). Cela en proposant des structures innovantes (un anneau pour Chord par exemple). Une partie des algorithmes pair à pair ont pour objectif d’implémenter une DHT 6 , table de hashage distribuée où l’on attribue des données à une clé, et où l’on cherche les éléments dans la DHT en cherchant les valeurs associées à une clé. L’idée d’une DHT réside dans le fait que chaque noeud du réseau est responsable d’une partie de la table (et donc d’une partie des données). Une partie des DHT implémente également l’idée de redondance des données, de façon à éviter la perte de données quand un nœud quitte le réseau de façon brutale. L’objectif d’une DHT est d’atteindre des performances en O(log n) pour la localisation des nœuds et des informations, mais aussi de permettre la réplication des informations (pour éviter de perdre des informations quand des nœuds quittent le réseau). Les algorithmes pairs à pairs ont plusieurs applications diverses : – Le partage de fichiers entre utilisateurs sans serveur central, dont les applications Bittorrent et Emule, – Le calcul distribué, – Les systèmes de fichiers répartis. J’ai donc pu implémenter deux algorithmes pair à pair sur SimGrid (en C et en Java) : Kademlia, Bittorrent et faire l’implémentation en Java de Chord qui existait déjà en C. 3.2 Chord Java Une implémentation de l’algorithme Chord en Java a également pu être réalisée, notamment pour pouvoir comparer les performances de l’interface Java face à l’interface C et l’interface Lua de SimGrid. Chord est le premier algorithme proposant une DHT, proposé en 2001[6], permettant de localiser des nœuds en O(log n) dans la plupart des cas, et O(n) dans le pire des cas. Un des objectifs de Chord est de permettre une mise à l’échelle de celui-ci sur un nombre important de nœuds. Le réseau Chord ne possède qu’une seule opération : lookup, qui permet d’associer à une clé un nœud qui est sensé avoir la responsabilité de cette clé. L’ensemble des nœuds participant au réseau Chord forment un anneau, et possèdent un identifiant sur un nombre fixé n de bits (par exemple, pour un nombre fixé à 4, on pourra avoir 24 = 16 nœuds dans le réseau, et donc le même nombre de clés associées à des données). Chaque nœud d’un réseau n Chord de taille n où a nœuds sont présents est donc responsable de 2a clés associées à des données. Chord ne possède cependant pas de système de redondance : un nœud est supposé informer son successeur et lui transférer les paires clé/données dont il a la charge quand il quitte le réseau. Il s’agit d’un des premiers protocoles implémentant une DHT, il est donc simpliste comparé par exemple à Kademlia. Figure 3 – Schéma du système prédécesseur/successeur sur un réseau Chord comportant 16 nœuds Chaque nœud dans un réseau Chord possède un successeur et un prédécesseur, qui correspond au 6. Distributed Hash Table : Table de hachage distribuée, permettant l’identification et la récupération d’informations dans un réseau distribué 11 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR noeud avant et après lui dans l’anneau, comme on peut l’observer sur la figure 4. Chaque nœud stocke Figure 4 – Schéma des "fingers" stockés sur un réseau Chord comportant 16 nœuds également les informations dans une finger table, de taille "nombre de bits dans le réseau", contenant des informations sur les nœuds plus loin que son successeur : chaque élément i de la finger table stocke un nœud successeur situé à une distance comprise entre 2i et 2i+1 . Cela signifie que chaque nœud du réseau Chord possède des informations détaillées sur les nœuds proches de lui dans l’anneau, et peu d’informations sur les nœuds lointains. Chord utilise 5 messages différents pour la communication entre les nœuds : – FIND_SUCCESSOR, – GET_PREDECESSOR, – NOTIFY, – SUCCESSOR_LEAVING, – PREDECESSOR_LEAVING Quand un nœud reçoit une requête FIND_SUCCESSOR pour un identifiant précis, deux cas sont possibles : – Si l’identifiant en question est contenu entre lui même et son successeur, il lui renvoie ce successeur. – Sinon, il transfère la requête au nœud le plus proche qu’il connait de l’identifiant en question (en regardant dans sa finger table. Figure 5 – Schéma d’une demande d’un "lookup" sur la clé d’identifiant 15 par le nœud d’identifiant 8. On peut observer sur la figure 5 que quand un nœud cherche le responsable d’une clé, il fait une requête à son successeur le plus proche, qui la transfère au plus proche qu’il connaît, jusqu’à arriver sur le nœud possédant la clé (ici, le nœud d’identifiant 15). 12 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR Cela permet de réduire dans le meilleur des cas la distance à parcourir de moitié (complexité au final de O(log n). Dans le pire des cas (le cas où chaque nœud ne connaît que son successeur), la complexité est de l’ordre O(n). Pour joindre un anneau déjà existant, un nœud doit simplement connaître un autre nœud appartenant déjà à l’anneau. Il fait alors une requête au nœud qu’il connait pour connaitre son successeur (si cette requête échoue, c’est qu’il n’a pas pu joindre l’anneau). Régulièrement, afin de mettre à jour les informations qu’il possède, le nœud fait une requête afin de trouver le nœud suivant dans sa finger table, en commençant par le plus proche (il va donc du plus proche au plus lointain, en recommençant au début une fois la finger table complètement traversée). L’implémentation Java a été conçue de façon à se rapprocher le plus possible de la version C, afin de permettre de faire des tests de performance comparatifs entre les deux implémentations. 3.3 Kademlia L’algorithme Kademlia a été proposé en 2001 par Petar Maymounkov et David Mazières[8]. Il est notamment utilisé par le réseau Emule (implémenté sous le nom KAD). Kademlia en C a été la première implémentation que j’ai réalisée sur SimGrid. Il s’agit d’un algorithme implémentant une table de hachage distribuée. Il spécifie la structure d’un réseau pair à pair, ainsi que la communications entre les nœuds du réseau. Ces nœuds utilisent le protocole UDP/IP pour communiquer entre eux. Il forme en cela un réseau overlay, c’est à dire un réseau bâti sur un autre réseau (Internet). Les nœuds dans Kademlia utilisent des identifiants sur 160 bits, que nous avons simplifié en utilisant des identifiants sur 32 bits dans SimGrid (cela ne changeant pas le comportement de l’implémentation). Il est utilisé pour localiser des données (généralement des hash ou des mots clés du fichier). Pour localiser un nœud ou des données, Kademlia explore le réseau de façon à trouver les nœuds les plus proches de ce que l’on cherche. Cela permet de localiser un nœud en O(log n). Pour trouver les nœuds les plus proches, Kademlia utilise l’opération binaire XOR 7 pour calculer la distance entre deux nœuds. 3.3.1 Messages Kademlia utilise 4 messages dans son protocole pour la communication entre les nœuds : – PING, qui sert à vérifier qu’un noeud est encore en vie, – FIND_NODE : Envoie une requête de recherche d’un nœud et des nœuds les plus proches de ce nœud. Retourne les nœuds les plus proches du nœud visé que le nœud à qui on a fait la requête connait, – FIND_VALUE : Similaire à FIND_NODE, mais si le noeud possède la donnée demandée par la clé, il la renvoie, – STORE, qui permet de stocker une paire (clé, valeur) Les requêtes sont envoyées de façon asynchrones : plusieurs nœuds sont interrogés en même temps. 3.3.2 Tables de routage Kademlia utilise également des tables de routage pour permettre la localisation des nœuds les plus proche d’un nœud. Les tables de routages sont organisées en "k-bucket", qui contiennent au maximum "k" éléments (k est ici une constante représentant le nombre de nœuds susceptibles de se déconnecter simultanément). Chaque "bucket" d’indice contient les nœuds qui ont les premiers i-1 bits en commun avec le noeud courant, et le ième bit différent. Cela signifie que la moitié des nœuds du réseau vont donc dans le premier bucket, 14 dans le second, etc. Dans un réseau où les indices sont stockés sur 32 bits, chaque nœud aura donc 32 buckets. 7. Ou exclusif 13 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR Kademlia utilise le fait que les nœuds qui sont en ligne depuis longtemps ont une probabilité plus importante de le rester : les nouveaux éléments ne sont donc ajoutés à la table de routage seulement si les nœuds déjà dans la table de routage ne répondent plus. Une des particularités de Kademlia est dans le fait qu’il se sert des requêtes entrantes pour mettre à jour ses tables de routage : la table de routage du nœud est donc mise à jour à chaque message reçu d’un nœud. 3.3.3 Protocole pour rejoindre le réseau Pour pouvoir rejoindre un réseau, un nœud doit simplement connaître un autre nœud du réseau (et comment le contacter) : on parle de bootstrap node. Le nœud fait alors une requête FIND_NODE sur son propre identifiant auprès du nœud qu’il connait, ce qui lui permet de récupérer les nœuds les plus proches de lui même connus par le nœud. Il fait ensuite une requête aléatoire dans un rayon de buckets autour de celui où le nœud de départ est, afin de remplir ceux-ci. 3.3.4 Détails sur l’implémentation Une des erreurs fut au départ d’utiliser dans une première version des identifiants sur 160 bits : cela a entrainé une complexité supplémentaire inutile dans le code, et l’on s’est rendu compte en utilisant des outils de profilage de code 8 que le temps passé dans les allocations de mémoire était trop important. Il a donc été choisi de remplacer ces identifiants par des identifiants 32 bits (qui sont plus facilement utilisables dans les langages C/Java en utilisant des affectations simples et non des copies de mémoire), ce qui a amélioré la vitesse de l’implémentation, tout en simplifiant le code de celle-ci. L’implémentation C représente au final environ 1300 lignes de code, et environ 900 lignes de code pour l’implémentation Java. 3.4 Bittorrent Bittorrent est un protocole pair à pair utilisé pour l’échange de fichiers entre utilisateurs. Il a été conçu en 2001 par Bram Cohen, comme moyen pour distribuer le système d’exploitation GNU/Linux. C’est aujourd’hui l’un des protocoles les plus utilisés pour les échanges pair à pair. Un des objectifs de bittorrent est d’être égalitaire : le protocole favorise les pairs qui partagent comparativement aux pairs qui ne partagent pas les données (les pairs qui partagent recevront donc les informations plus rapidement) [11]. C’est un protocole mixte pair à pair et client/serveur : il existe un serveur centralisé, le tracker, mais celui-ci n’est pas responsable de l’échange des données : il fait simplement le lien entre les pairs. Contrairement à des protocoles comme Kademlia, celui-ci se concentre uniquement sur l’échange des données des fichiers entre les pairs, et non sur la localisation des données, afin l’objectif d’échanger le contenu aussi vite que possible. Les pairs ne proposent pas directement des fichiers en partage, les informations sur un fichier à télécharger sont mis à disposition sur un tracker. Bittorrent divise les fichiers en pièces, qui sont elles-même divisées en blocs, comme on peut le voir sur la figure 6. Le fichier torrent initialement téléchargé contient les checksum 9 SHA-1 des différentes pièces du fichier : les blocs invalides reçus seront donc systématiquement rejetés lors du contrôle de la validité de la pièce téléchargée. Les pairs gardent une liste des pairs auquel ils sont connectés, mais aussi une liste de pairs actifs (ceux qui téléchargent/envoient des données depuis le pair). Chaque pair télécharge des données depuis plusieurs pairs à la fois. L’ensemble des pairs forment un "essaim" (swarm en anglais). Le protocole est par ailleurs très léger : le coût du protocole ne dépasse pas les 2% lors de l’envoi de données et la réception de données. Bittorrent comporte quatre phases distinctes : 8. Outil permettant d’évaluer les fonctions appelées et le temps que l’on passe dans celles-ci 9. somme de contrôle d’un fichier, permettant d’évaluer si un fichier a été modifié 14 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR Figure 6 – Schéma de la division en pièces et en blocs d’un fichier dans Bittorrent – Le téléchargement d’un fichier "torrent", contenant les informations sur le fichier que l’on souhaite télécharger (notamment les checksum de tous les blocs, le nom du fichier. – La phase de connexion à un tracker : le pair se connecte au tracker indiqué dans le torrent, qui lui renvoie de façon aléatoire 50 pairs qui sont dans la liste des pairs qu’il connait. – La phase de téléchargement du fichier, où le pair envoie des messages. – La phase de partage du fichier, une fois celui-ci complètement téléchargé. Le pair peut donc être dans deux états : – L’état leecher, quand il n’a pas encore fini de télécharger le fichier : le pair télécharge des pièces du fichier, tout en partageant celles qu’il à terminé de télécharger. – L’état seed quand il a terminé de télécharger le fichier : Le pair ne fait plus que partager les pièces du fichier. Bien sur, afin de pouvoir télécharger complètement le fichier, un seed doit toujours être dans l’essaim : il faut toujours que toutes les pièces soient présentes au moins en un exemplaire dans le réseau. L’efficacité de bittorrent vient de son système de blocage des pairs (choke) : par défaut, les pairs sont bloqués, c’est à dire qu’ils ne peuvent pas demander des données au pair : le pair utilise un algorithme de sélection des pairs actifs (détaillé plus bas) pour déterminer qui a le droit de télécharger, afin de rendre les partages plus efficace. Par ailleurs, le système de "intéressé/non-intéressé" permet au pair de savoir quand un pair est intéressé ou non par les pièces qu’il a déjà téléchargé, permettant d’améliorer les performances (on ne débloque jamais un pair qui n’est pas intéressé). Figure 7 – Schéma du système de blocage entre les pairs. Il est à noter que comme on l’observe sur la figure 7, l’état "débloqué" n’est pas forcément réciproque : souvent, les pairs qu’un pair a débloqué ne sont pas les pairs qui l’ont débloqué. L’implémentation de Bittorrent dans Simgrid utilise là aussi l’interface MSG, en C et en Java. 15 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR 3.4.1 Le protocole entre pairs Il existe 10 types de messages échangés entre les pairs : 1. HANDSHAKE : envoyé à l’initialisation de la connexion entre deux pairs. 2. BITFIELD : envoyé lors de la reception d’un message "HANDSHAKE" : ce message contient les pièces que le pair possède déjà. Cela permet au pair de repérer quels sont les autres pairs qui possèdent des pièces du fichier qui l’intéressent. 3. INTERESTED : envoyé à un pair quand celui-ci possède des pièces dont on est intéresse(c’est à dire des pièces qu’on ne possède pas) : cela permet au pair de l’ajouter à la liste des pairs qu’il peut débloquer. 4. NOTINTERESTED : envoyé à un pair quand celui-ci ne possède plus de pièces que l’on ne possède pas : cela permet de l’enlever de la liste des pairs. 5. CHOKE : envoyé quand le pair nous a bloqué : il ne nous autorise plus à envoyer des demande de blocs. 6. UNCHOKE : envoyé quand le pair nous a débloqué, c’est à dire qu’il nous a autorisé à télécharger, c’est à dire à demander des blocs du fichier. 7. HAVE : envoyé quand un pair à fini de télécharger une pièce à tout les pairs auquel il est connecté. Cela permet de maintenir à jour la liste des pairs possédant chaque pièce. 8. REQUEST : envoyé quand un pair nous a débloqué, contient une demande de blocs de données à un pair. Le pair n’y répondra que si le pair a été débloqué, les requêtes envoyées par des pairs bloqués ne sont pas traitées. 9. PIECE : réponse à un message de type REQUEST : contient les données demandées par le pair. 10. CANCEL : Envoyé aux pairs à qui le pair a fait des demandes de blocs quand le pair a reçu d’un autre pair les blocs demandés. Cela interrompt le transfert du bloc en question par le pair à qui il est envoyé, pour économiser de la bande passante. 3.4.2 Algorithme de choix de la pièce à télécharger Bittorrent utilise un algorithme pour choisir la pièce à télécharger différent selon la situation, ce afin d’améliorer les performances : – Si le pair possède moins de 3 pièces du fichier, le pair choisit une pièce au hasard parmi celles qu’il ne possède pas. – Si le pair possède au moins 3 pièces, le pair utilise une stratégie de "plus rare en premier" : le pair demande à télécharger la pièce qui est la moins présente parmi les pairs auquel il est connecté : cela est rendu possible par le fait que les pairs possèdent toujours la liste des pièces possédées par les autres pairs. Cela permet d’empêcher que certaines pièces soient rares et donc difficiles à trouver pour les pairs. 3.4.3 Protocole de choix des pairs actifs Bittorrent met à jour la liste des pairs actifs toutes les 10 secondes en moyenne. Le temps entre chaque phase est important, car si le nœud bloque/débloque trop rapidement les nœuds, ceux-ci n’ont pas le temps d’atteindre leur vitesse maximale sur leur connexion. Bittorrent utilise deux algorithmes distincts pour choisir les pair actif : – L’algorithme principal, qui fonctionne de cette façon : 1. Tout d’abord, les pairs sont classés par leur vitesse de téléchargement. 2. Les pairs n’ayant pas envoyé de blocs dans les dernières secondes sont éliminés. 3. Les trois pairs les plus rapides sont débloqués. – L’algorithme dit du "choix optimiste" : le pair choisit au hasard un des pairs dans sa liste et le débloque. Cela a notamment pour objectif de permettre aux nœuds qui viennent de joindre le réseau (et qui n’ont donc pas de pièces à envoyer) de pouvoir recevoir des données. L’algorithme du choix optimiste est utilisé une fois sur 3. 16 IMPLÉMENTATION D’ALGORITHMES PAIR À PAIR 3.4.4 Détails sur l’implémentation Dans l’implémentation réalisée dans SimGrid, le tracker utilise un processus, qui se charge d’envoyer les pairs aux autres réseaux (en en tirant au maximum 50 de façon pseudo-aléatoire (et reproductible bien sûr) dans sa liste. Celui-ci reçoit simplement sur une mailbox "TRACKER", et traite les requêtes des pairs. Chaque pair stocke plusieurs informations : – son identifiant (qui est simplifié dans l’implémentation en utilisant un entier 32 bits, – la liste des pièces du fichier qu’il possède, – une liste du nombre de pairs qui possèdent chaque pièce du fichier – une liste des pairs auquel il est connecté, avec les informations suivantes : – son identifiant, – la liste des pièces qu’il possède, – si il est intéresse par une de nos pièces, – si il nous a débloqué, – si nous l’avons débloqué Le nœud passe donc par une première phase, où il envoie une tâche de requête sur la mailbox du tracker. Les mailbox de communication entre nœuds et entre nœuds et tracker sont dissociés, afin d’éviter que des messages soient mal interprétés. Les pairs s’envoient donc ensuite des messages HANDSHAKE sur leur mailbox de communication, par lesquels ils répondent en envoyant un message BITFIELD correspondant aux pièces qu’ils possèdent. Les pairs envoient ensuite des messages INTERESTED aux pairs qui possèdent les pièces qu’ils ne possèdent pas. Ceux-ci marquent le pair comme étant intéressé, et en appliquant l’algorithme de choix actifs, débloque certains des pairs auquel il est connecté, en leur envoyant des messages UNCHOKE. Ceux-ci vont alors leur envoyer des messages REQUEST, demandant une partie du fichier (des blocs appartenant à une pièce) auquel le pair répond en envoyant des messages PIECE contenant les données. L’une des principales difficultés dans l’implémentation de Bittorrent a été dans la liaison avec le tracker et la mise à l’échelle : étant donné que chaque pair arrive au début de la simulation, le tracker se trouve très vite surchargé pour répondre aux nœuds. L’implémentation représente au final environ 1100 lignes de code pour l’implémentation C, et environ 900 lignes de code pour l’implémentation Java. 17 AMÉLIORATION DES INTERFACES UTILISATEUR 4 Amélioration des interfaces utilisateur 4.1 Amélioration de l’interface Java pure Comme dit précédemment, SimGrid possède plusieurs interfaces permettant d’utiliser celui-ci dans un autre langage de programmation que le C, dont une interface permettant d’utiliser l’interface MSG en Java. Cependant, celui-ci manquait de certains éléments pour pouvoir implémenter des algorithmes pair à pair en Java : – Les performances de l’interface Java étaient très faibles comparées à l’usage direct de l’interface MSG en C, de nombreux points pouvaient faire l’objet d’optimisations – Très peu d’exemples étaient disponibles comparé à l’API 10 C – Seules les communications synchrones étaient disponibles : les communications asynchrones, qui sont nécessaires pour pouvoir implémenter des algorithmes pair à pair (étant donné que de nombreux messages doivent être envoyés en même temps, on ne peut pas attendre que chaque message soie envoyé pour passer au suivant). – L’implémentation était trop dépendante de "Thread" en Java : il n’était pas possible de changer ce qui était utilisé dans le simulateur sans changer l’interface utilisateur. SimGrid-Java a également pris de l’importance depuis le projet SONGS, ou de nombreux nouveaux utilisateurs de SimGrid souhaitent utiliser SimGrid avec Java. L’interface Java utilise uniquement des contextes basés sur des threads 11 , qui sont donc beaucoup moins performants que des contextes basés sur des changement de piles comme en C. SimGrid Java utilise JNI 12 pour faire la liaison entre le C et le Java. JNI est basé sur l’appel de fonctions natives : on peut en Java déclarer une fonction avec le mot clé native, en ne lui déclarant pas de corps, et Java cherchera dans les bibliothèques natives actuellement chargées les fonctions portant un nom précis (Java_[PAQUET]_[CLASSE]_[NOM_FONCTION]). Le passage d’une fonction Java à une fonction C a bien sûr un coût d’exécution assez élevé, et ne doit donc pas être fait inutilement. Par ailleurs, aucune vérification sur le type des données n’est faite (on peut briser l’encapsulation des données, mettre des données invalides dans les champs, des fuites de mémoire sont possibles), rendant plus difficile le debogage de l’application. 4.1.1 Refactorisation du code de SimGrid Java Un des problèmes de SimGrid Java se trouvait dans la façon dont la liaison entre le C et le Java était faite : chaque méthode des classes Java (pour exemple, la classe "Process", la classe "Task" pour les taches/messages échangés entre les processus) passait par des méthodes statiques d’une unique classe, MsgNative. Figure 8 – Schéma de l’ancienne méthode d’appel des méthodes natives dans SimGrid Java Ceci était problématique, étant donné que tout le code correspondant à la liaison entre les méthodes Java et les fonctions C étaient dans un seul fichier C, jmsg.c, de 1128 lignes. Il a donc été choisi de refactoriser le code afin d’éliminer tous les appels à MsgNative, ce qui permet deux choses. 1. Tout d’abord, cela élimine un appel de fonction ; la méthode "send" de Task par exemple appelle désormais directement la méthode native correspondante, au lieu de faire un appel a MsgNative.taskSend() qui fera un appel à la méthode native correspondante : cela permet de gagner en performances. 10. Application programming interface : Interface de programmation utilisée par le développeur 11. Processus léger 12. Java Native Interface : Interface standard en Java permettant de faire appel à des fonctions C depuis un code Java 18 AMÉLIORATION DES INTERFACES UTILISATEUR 2. Cela permet de mieux découper le code : les méthodes natives sont désormais découpées dans plusieurs fichiers (jmsg_process, jmsg_task, etc). Il devient plus facile d’ajouter de nouvelles fonctionnalités, le code étant plus découpé et donc plus facilement lisible. Figure 9 – Schéma de la nouvelle méthode d’appel des méthodes natives dans SimGrid-Java Comme visible sur la figure ci dessus, la classe "MsgNative" a été complètement éliminée. Cela a permis de préparer le terrain pour l’implémentation des communications asynchrones, que je vais développer dans le point suivant. 4.1.2 Implémentation des communications asynchrones Il existe deux manières d’envoyer des messages d’un processus à un autre : – La méthode synchrone, aussi appelée "bloquante", où on envoie un message d’un processus à un autre, et où le processus est bloqué tant que le message n’a pas été complètement envoyé/reçu. – La méthode asynchrone, ou non bloquante, où on initie simplement la communication et où l’on attend pas la fin de celle-ci pour rendre la main au programme : celui-ci pourra alors vérifier plus tard si la communication est finie ("test") où attendre que celle-ci se termine ("wait"). Comme montré sur la figure ci-dessus, les communications asynchrones sont importantes quand on ne souhaite pas attendre que le message aie été envoyé pour faire autre chose. Cela est très important pour les performances des protocoles de pair à pair : on ne peut pas se permettre pour des raisons de performances d’attendre que le message soie fini d’envoyer ou reçu (et donc bloquer le processus) pour faire d’autres actions : les protocoles pairs à pairs impliquent l’envoi de nombreux messages, on ne peut pas se permettre d’attendre la fin de l’envoi de chaque message pour passer au suivant. SimGrid propose depuis la version 3.4 les deux méthodes de communications entre les processus. Cependant, seules les communications synchrones étaient disponibles dans le binding Java. Afin de pouvoir implémenter correctement les algorithmes pair à pair en Java, j’ai donc pu implémenter les communications asynchrones de l’interface MSG en Java, appelées "Comm" dans SimGrid. Les opérations sur les communications sont donc les suivantes : – Initier une communication à recevoir (irecv MSG_task_irecv en C) – Initier un envoi de communication (isend, MSG_task_isend en C) – Tester si la communication est terminée (test, MSG_comm_test en C) – Attendre la fin de la communication (wait). Il a donc choisi pour cette implémentation d’implémenter une nouvelle classe "Comm", représentant une communication en cours. Les objets "Comm" sont obtenus en appelant l’une des méthodes de "Task" : Task.irecv (méthode statique), Task.isend, qui lancent une nouvelle communication et renvoient un objet de communication. Une fois cette communication lancée, on peut appeler les méthodes test ou wait pour vérifier l’état de la communication. Une fois cette communication terminée, la tache reçue (dans le cas d’une reception),si la communication s’est bien passé) est accessible en utilisant "getTask". Cela permet de proposer une API proche de la version C, tout en étant plus haut niveau pour l’utilisateur : celui-ci bénéficie complètement du garbage collector 13 Java et n’a pas à gérer la mémoire à la main. 4.1.3 Changement de la manière dont les processus sont lancés Comme expliqué précédemment, les processus simulés dans SimGrid utilisent un contexte d’execution dans lequel se trouve entre autres leur pile et leur état actuel. 13. Ramasse miettes, permettant de désallouer automatiquement les éléments qui ne sont plus utilisés dans un langage de programmation 19 AMÉLIORATION DES INTERFACES UTILISATEUR L’interface Java de SimGrid propose actuellement uniquement des contextes d’exécution basés sur des threads, ceci étant du à des limitations de la machine virtuelle Java. Le problème était dans le fait que les objets Processus en Java étaient des classes filles, héritant directement de la classe Thread. Cela empêchait donc de changer l’interface sans casser une partie de l’interface utilisateur. Par ailleurs, les objets Thread Java sont également plus lents que des Threads système. Il a donc été choisi de ne plus faire hériter l’objet Processus de la classe Thread, et de lancer les threads depuis le C, avant de les attacher ensuite à la machine virtuelle Java. Cela améliore ainsi les performances au lancement des processus simulés. Il a également été choisi de remplacer les sémaphores Java utilisées pour bloquer les Processus par des sémaphores systèmes, améliorant nettement les performances des simulations. Un des principaux problèmes rencontrés lors de ce changement a été de gérer correctement la mort des Processus : la machine virtuelle Java refuse de "détacher" un thread si celui-ci a une pile d’exécution non vide. Un contournement utilisant une exception C et un exception Java a donc du être mis en place pour vider les deux piles d’exécution du programme. SimGrid utilise pour le parsage 14 des fichiers de déploiement un parseur XML SAX développé en interne, surfxml, qui définit des "fonctions de callback 15 " à des évènements rencontrés quand une balise est ouverte/fermée. SimGrid-Java détournait auparavant ces événements afin d’utiliser une classe Java, appelée "ApplicationHandler", qui s’occupait de créer les objets "Process" et de les lancer en Java. Cette méthode était peu efficace, car elle impliquait un passage du monde C au monde Java à chaque callback (ce qui bien évidemment à un coût). Par ailleurs, cela faisait une duplication inutile de code avec le code C servant à créer les processus. Elle était utilisée car l’ancienne Grâce aux travaux effectués sur le lancement des processus coté C, j’ai pu adapter la context factory Java pour que les fonctions C de SimGrid puisse l’utiliser de façon transparente (sans toucher au code de SimGrid C) et créer correctement les processus. Cela a permis de simplifier le code et d’éviter un doublon, rendant SimGrid-Java plus facilement maintenable, réduisant sa taille : Module SimGrid-Java - Code C SimGrid-Java - Code Java 4.1.4 Quantité de code avant 1,882 465 Quantité de code après 1,808 389 Mise en cache des éléments Java Afin d’améliorer les performances de SimGrid-Java, il a tout d’abord été nécessaire de repérer là où les performances étaient perdues de façon inutiles. La mise en cache des fieldID Java est recommandée par la plupart des concepteurs de JVM afin d’améliorer les performances. 16 Ces "field id" sont des éléments (de type jfieldID, jmethodID, jclass), permettant quand on est en C en utilisant JNI de récupérer ou de définir des attributs d’objets Java. Ils sont récupérés en utilisant les méthodes GetFieldID, GetMethodID de JNI. La récupération de ces champs a cependant un coût non négligeable, et ces champs étaient auparavant recalculés à chaque fois qu’ils étaient nécessaires (c’est à dire à chaque fois qu’on accédait depuis le C à un champ Java, c’est à dire quasiment à chaque appel de fonction native) : le programme doit regarder dans une table de correspondance entre le nom du champ et sa position dans la mémoire. Il a donc été décidé pour améliorer les performances de "cacher" ces champs, c’est à dire de les calculer une seule fois et de les stocker dans la mémoire ensuite : on utilise pour cela une méthode appelée "nativeInit" par classe (rendu possible grâce à la factorisation réalisée auparavant), appelée à l’initialisation de la classe (donc une seule fois dans la durée de vie de l’application). Cela a permis d’améliorer nettement les performances, tout en diminuant la quantité de code dupliquée. 14. Opération de parcourir un fichier 15. Fonction appelée à chaque occurrence d’un événement 16. http://developer.android.com/guide/practices/design/jni.html 20 AMÉLIORATION DES INTERFACES UTILISATEUR 4.1.5 Ajout d’une interface pour la génération de nombres pseudo-aléatoires Certains algorithmes pair à pair nécessitent la génération de nombres pseudo-aléatoires dans certaines situations, par exemple pour tirer un nœud parmi la liste des nœuds auquel le nœud actuel est connecté. Or, comme dit précédemment, l’intérêt de SimGrid est dans le fait que les simulations sont reproductibles : les nombres tirés doivent toujours être les mêmes. SimGrid utilise en son sein la bibliothèque RngStream, qui permet de générer des nombres pseudo aléatoire avec une période de 2191 . Il a donc été choisi de rendre public l’interface RngStream dans SimGrid et de programmer l’interface permettant de l’utiliser en Java. Cela a permis deux choses : tout d’abord, la possibilité pour les utilisateurs d’utiliser un générateur de nombres pseudo-aléatoires puissant. Ensuite, la reproductibilité d’une expérience écrite en C et en Java : il est possible de tirer des nombres aléatoires qui sont les mêmes en C et en Java. 4.2 Création d’une interface java à très hautes performances Comme indiqué plus tôt, l’interface Java utilise uniquement des contextes basés sur des pthreads. Or ceux-ci ont des performances très mauvaises. Une des fonctionnalités manquantes dans les versions actuelles de Java est la gestion de Coroutines, qui permettent un multitache coopératif : une Coroutine dispose de sa propre pile d’exécution, son état et peut "donner la main" à une autre Coroutine. C’est donc exactement ce dont nous avons besoin pour les processus simulés dans SimGrid. Cependant, une implémentation sous la forme de patch (prévu actuellement pour l’intégration dans la version 9 de OpenJDK) réalisé par Lukas Stadler pour la Da Vinci JVM 17 propose une implémentation efficace des Coroutines en Java, en utilisant une machine virtuelle Java spécifiquement patchée[10]. Rendue possible grâce au travail sur le changement du lancement des processus coté C, une implémentation utilisant les Coroutines en Java a pu être implémentée. Cette implémentation remplace la création d’un thread par la création d’un objet Coroutine, qui gère le Processus. Cela permet notamment de gagner un temps important au lancement des Processus, car le coût d’un lancement d’un Thread est très élevé, alors que le coût de lancement d’une Coroutine est assez bas (étant donné qu’il n’y’a qu’une pile à allouer). Figure 10 – Schéma du changement de contexte entre les Processus. En utilisant les Coroutine, Maestro peut donner la main à un processus simplement en appelant la méthode yieldTo de la classe Coroutine : il n’y’a plus de coût de synchronisation, simplement un changement de pile, comme on l’observe sur la figure 10. De la même manière, quand un processus est suspendu par le simulateur, il n’y’a plus utilité d’utiliser des sémaphores pour bloquer celui-ci : un simple appel à la méthode yield de Coroutine rend la main au simulateur, ce qui permet d’éviter des couts de synchronisation (qui sont très importants), et donc des appels systèmes. 17. Machine virtuelle Java expérimentale proposant des fonctionnalités nouvelles, notamment pour l’implémentation de langages dynamiques autres que le Java 21 AMÉLIORATION DES INTERFACES UTILISATEUR Cette implémentation est très prometteuse, dans le sens où elle permet actuellement des gains de vitesse permettant de diminuer par un facteur de 5 le temps d’exécution de la simulation sur les tests effectués. En plus d’améliorer les performances brutes à l’exécution, utiliser les Coroutines permet également d’améliorer le passage à l’échelle : le nombre de Threads sur un système d’exploitation est limité (32000 sur les GNU/Linux récents par exemple) et cette limite est difficile à outrepasser. Or, le nombre de Coroutines sur un système n’est pas limité, permettant de dépasser cette limite de 32000 processus. Cependant, cette implémentation n’est pas encore utilisable en production : des problèmes subsistent la rendant inutilisable au bout d’un certain nombre de nœuds sans une configuration spécifique, du a un bogue dans l’implémentation du patch qui à pu être identifié, et qui est en cours de correction. Des travaux sur les performances peuvent également encore être réalisés : l’objectif est d’atteindre un changement de contexte pour les processus de la même manière qu’en C, où les processus se passent la main sans repasser par Maestro afin de diviser par deux le nombre de changement de contextes, ce qu’on peut voir sur la figure 11. Figure 11 – Schéma du changement de contexte "idéal" entre les Processus en utilisant les Coroutines Les travaux menés sur la parallélisation de SimGrid pourraient également être appliqués aux Coroutines, afin d’exécuter un nombre de Processus en parallèle égal au nombre de cœurs du processeur de la machine lançant la simulation. 22 ÉVALUATION EXPÉRIMENTALE 5 Évaluation expérimentale Diverses évaluations expérimentales ont pu être réalisées afin de pouvoir tester les gains de performances des différents changements opérés sur les interfaces de SimGrid. Ces expérimentations ont pour la plupart été réalisées en utilisant l’exemple masterslave de SimGrid, qui consiste en un processus maître envoyant des tâches à des processus esclaves qui les exécutent. Les exemples d’implémentations pair à pair n’ont pas pu être utilisées pour les comparaisons entre les changements sur l’interface Java, étant donné qu’une partie de l’interface (les communications asynchrones) était manquante . En ordonnée sur les courbes se trouve le temps d’exécution : un temps d’exécution inférieur indique de meilleures performances. Ces expérimentations ont été réalisées sur la plateforme Grid’5000[2], qui est une plateforme expérimentale permettant d’étudier des systèmes distribuées/parallèles. Elles ont toutes été réalisées sur le cluster parapide situé à Rennes (afin d’obtenir des résultats dans des conditions identiques). 5.1 Cache des éléments Java L’objectif principal de la mise en place du cache sur les éléments Java était d’améliorer les performances, en économisant des appels de fonctions inutiles. Par ailleurs, ces appels étant situés sur les appels de fonction sur la section critique (c’est à dire qu’ils étaient appelés dans des méthodes appelées lors de l’exécution de chaque processus). Nombre de processus 10 100 500 1000 2000 5000 10000 Temps d’exécution Avant Après 111,1525 96.4325 101,7725 88.935 101,7025 90.67 102,26 94.3275 108,435 97.5225 122,0575 109.5175 136,81 127.23 Figure 12 – Tableau de temps d’exécution d’un exemple masterslave en Java, avec un nombre de tâches fixé à 500 000. Moyenne sur 4 expériences. Figure 13 – Courbe de temps d’exécution d’un exemple "masterslave" en Java, avec un nombre de tâches fixé à 500000, avant et après le travail sur le cache des éléments Java 23 ÉVALUATION EXPÉRIMENTALE On peut observer sur les figures 12 et 13 qu’en moyenne, l’ajout d’un cache à permis ici d’améliorer les performances, permettant un gain de temps pour la simulation d’environ 13%. 5.2 Lancement des processus coté C Un des objectifs du changement de la méthode de lancement des Processus, en plus de permettre le changement d’interface sans casser la compatibilité avec les applications existantes, était d’améliorer les performances du Java au lancement, en faisant la supposition que le lancement d’un thread système (pthread) était plus rapide que le lancement d’un thread dans le Java. On peut remarquer que les Figure 14 – Courbe de temps d’exécution d’un exemple "masterslave" en Java, avec un nombre de taches fixé à 500000, avant et après le travail sur le changement de méthode de lancement des Processus performances se sont nettement améliorées après le changement de méthode pour lancer les processus. Cela est notamment du au fait que le lancement d’un thread est très couteux en terme de temps en Java, là où un thread système est plus léger, diminuant le temps de démarrage des processus. Par ailleurs, l’utilisation des sémaphores système à la place des sémaphores Java (ce qui a donc supprimé un aller-retour du C vers le Java à chaque exécution du processus) a permis de nettement améliorer le temps de changement de processus. On remarque ainsi un gain de 40% a 50% sur le temps d’exécution. 24 ÉVALUATION EXPÉRIMENTALE 5.3 Interface haute performance utilisant les Coroutines Comme expliqué précédemment, l’interface utilisant les coroutines ne peut être que plus rapide comparée à la version actuelle utilisant les threads : les coûts de synchronisation sont quasiment inexistants en utilisant cette méthode. Figure 15 – Courbe de comparaison entre l’interface actuelle de SimGrid Java (en rouge) et l’interface expérimentale utilisant les Coroutines (en rouges), sur l’exemple masterslave avec un nombre de tâches fixé à 500 000. Ainsi, sur la courbe 15, on observe que les performances sont clairement meilleures entre l’interface utilisant des threads et l’interface utilisant des coroutines : la vitesse d’exécution est divisée par plus de 5. Ces bonnes performances restent à relativiser : elles restent toujours en dessous des performances de l’interface C d’un facteur d’environ 3, et l’interface souffre toujours de problèmes de stabilité. 5.4 Expériences sur l’algorithme Chord Les expériences réalisées sur l’algorithme Chord ont permis de tester la charge du simulateur dans des situations réelles de pair à pair. Comme on peut le voir sur la figure 16, les performances de Chord Figure 16 – Courbe des résultats de simulation sur l’algorithme Chord. Temps de simulation fixé à 1000 secondes simulées. en Java restent toujours très en déca des performances de Chord en C en utilisant les raw context. 25 ÉVALUATION EXPÉRIMENTALE Cependant, elles se rapprochent des performancses de Chord C en utilisant des threads (ce qui est utilisé par l’interface actuelle en Java). 5.5 Amélioration globale des performances Nombre de noeuds 10 100 500 1000 2000 5000 10000 Nombre 1000 0.39 0,375 0,5725 0.80 1,7025 6,1775 19,325 de tâches 10000 1,5275 1,675 1,5675 1,9325 2,815 7,85 21,6225 100000 11,0825 10,6425 9,29 11,8125 14,1775 20,01 37,1025 500000 46,755 45,84 49,1975 50,105 54,1175 61,645 96,7 1000000 87,33 91,8225 92,585 98,595 105,1975 121,1525 140,875 Figure 17 – Tableau de résultat d’expériences sur l’exemple "masterslave" en Java, utilisant le modèle réseau "LV08" (moyenne sur 4 expériences). Expériences menées sur le cluster Grid’5000, sur les machines "parapide" à Rennes. Figure 18 – Courbe des résultats d’expérience sur l’exemple "masterslave" en java, avant (en rouge) et après (en bleu) les travaux menés sur l’interface, ainsi qu’une comparaison avec la version C utilisant les raw context et les threads. Le nombre de tâches a été fixé à 500 000. Moyenne sur 4 expériences. Les performances de l’interface expérimentale sont en violet. On peut remarquer sur les figures 17 et 18 que les performances globales de l’interface Java se sont nettement améliorées, d’un facteur d’environ 2, se rapprochant des performances du C en utilisant les pthreads. Celles-ci restent cependant très en déca des performances offertes par l’interface C utilisant les "raw context". Cependant, celles-ci restent bien supérieures à ce que propose les simulateurs concurrents, comme on peut l’observer sur la figure 18. Les mauvaises performances des interfaces utilisant les threads s’expliquent par le fait que les coûts de synchronisation et de passage entre threads sont très importants, là où le passage d’un processus à l’autre est fait en assembleur (simplement en changeant la pile) dans les contextes "raw". Par ailleurs, l’interface expérimentale utilisant les Coroutine est très 26 ÉVALUATION EXPÉRIMENTALE prometteuse. Cependant, si on compare ces résultats à ceux des simulateurs concurrents (notamment le simulateur GridSim), ces résultats restent très bons, comme on peut l’observer sur la figure 19 Figure 19 – Courbe de comparaison des performances de SimGrid et de GridSim sur un exemple "masterslave", avec un nombre de nœuds fixé à 1000. 5.6 Complexité en terme de quantité de code Les performances ne sont pas le seul apport apporté à l’interface Java. Comme expliqué plus haut, de nombreux travaux ont été faits dans l’objectif de diminuer la duplication de code entre l’interface C et l’interface Java. Module SimGrid Java - Code C SimGrid Java - Code Java SimGrid Java - Exemples Quantité de code avant (c71a4e60) 1,690 588 653 Quantité de code après (1ef4a44f) 1932 419 1,892 Figure 20 – Tableau des quantités de code dans l’interface Java de SimGrid avant et après (en nombre de lignes de code source, sans compter les commentaires, calculées avec l’outil sloccount) On remarque ainsi que malgré l’ajout de nouvelles fonctionnalités (les classes Comm et RngStream), la quantité de code Java a diminué ; montrant deux choses : d’une part, que l’objectif de contrôler le code Java depuis le C est atteint. Ensuite, cela permet une maintenabilité plus facile du code . Par ailleurs, la quantité d’exemples sur l’interface Java a augmenté, ce qui permet, en plus de fournir aux utilisateurs des exemples couvrant plus de parties de l’interface, de tester plus en profondeur le code. 27 CONCLUSION Conclusion et perspectives L’implémentation des algorithmes pair à pair Kademlia, Bittorrent et Chord sur l’interface Java permet ainsi de fournir aux utilisateurs de SimGrid des exemples pairs à pairs complets, tout en permettant aux développeurs de SimGrid de tester sur plusieurs interfaces dans des cas réels les performances en fonction du temps. L’amélioration de l’interface utilisateur Java, très demandée par les utilisateurs, a permis d’augmenter nettement ses performances et de la mettre à niveau en termes de fonctionnalités avec l’interface C, et permettra dans un futur proche d’avoir de plus hautes performances avec l’interface hautes performances qui a été développée, qui permet de diviser par 5 le temps d’exécution des simulations. Cependant, plusieurs éléments restent encore à réaliser dans le cadre de ce stage : l’interface java expérimentale à hautes performances utilisant les Coroutines peut encore être améliorée, dans le sens où elle n’est pas encore utilisable en production. Par ailleurs, des ajustements peuvent encore être faits sur l’implémentation des algorithmes pair à pair réalisés (de façon à mieux gérer le support du churn, c’est à dire l’arrivée/départ des nœuds dans la simulation, qui sera bientôt géré dans SimGrid). Également, d’autres algorithmes pair à pair peuvent également être implémentés : l’algorithme Pastry par exemple est à implémenter, celui-ci étant une des cibles pour le model checker en cours de développement sur SimGrid. Une implémentation des algorithmes Kademlia et Bittorrent en Lua pourra également être faite, afin de pouvoir évaluer les performances de l’interface Lua comparée à l’interface C et l’interface Java. D’un point de vue personnel, les travaux que j’ai pu réaliser sur le simulateur Simgrid durant ce stage ont été particulièrement enrichissants pour moi : j’ai eu l’occasion de découvrir les applications distribuées, et d’approfondir mes connaissances en programmation système, notamment en C, où j’ai pu découvrir notamment l’utilisation des outils Valgrind et GDB comme outils de débogage. Par ailleurs, j’ai pu également découvrir le monde de la recherche. J’ai ainsi pu assister à des conférences en rapport avec le monde des applications distribuées. Mes travaux sur les algorithmes pair à pair m’ont également permis de lire des articles scientifiques en anglais afin de pouvoir comprendre la logique de ces algorithmes et leurs spécificités. J’ai pu découvrir le framework JNI dans le cadre de mes travaux sur l’interface Java, me permettant de découvrir comment interfacer ces deux langages de programmation. J’ai pu renforcer mes connaissances dans le logiciel de gestion de version Git dans le cadre d’un vrai projet en production, me permettant de découvrir ses fonctions avancées. Cela m’a permis de découvrir le fonctionnement d’un projet en production, au delà des projets que nous avons pu mener à l’IUT. 28 BIBLIOGRAPHIE Bibliographie Références [1] Algorille. http://www.loria.fr/equipes/algorille/members.html. [2] Grid’5000. https://www.grid5000.fr. [3] Historique du projet simgrid. http://simgrid.gforge.inria.fr/history.html. [4] Loria, laboratoire lorrain de recherche en informatique et ses applications. http://www.loria.fr. [5] Simgrid. http://simgrid.gforge.inria.fr/. [6] David Karger M. Frans Kaashoek Hari Balakrishnan Ion Stoica, Robert Morris : Chord : A scalable peer-to-peer lookup service for internet applications. 2001. [7] Christophe Thiéry Martin Quinson, Cristian Rosa : Parallel simulation of peer-to-peer. 2012. [8] Petar Maymounkov et David Mazieres : Kademlia : A peer-to-peer information system based on the xor metric. 2002. [9] S. Naicken, B. Livingston, A. Basu, S. Rodhetbhai, I. Wakeman et D. Chalmers : The state of peer-to-peer simulators and simulations. [10] Lukas Stadler : Serializable coroutines for the hotspotTM java virtual machine. 2011. [11] Ryan Toole : Bittorrent architecture and protocol. 2006. 29