Introduction à Gearman pour le multitâche en PHP

#api#framework#multitâche#performance#php

Gearman (anagramme de « Manager ») est un framework applicatif open source conçu pour distribuer des tâches vers plusieurs machines ou processus, en supportant le changement d’échelle. Il permet d’effectuer des traitements en parallèle, de faire de la répartition de charge, et d’appeler des fonctions entre différents langages. En utilisant la redondance et la persistance, il peut être utilisé sans présenter de point individuel de défaillance et en assurant la reprise après erreur.

Présentation de Gearman

Gearman utilise trois types d’entités qui communiquent : client, worker, et serveur de jobs :
• Le client envoie des tâches (principalement des demandes d’exécution de job) à un serveur de jobs. Les tâches peuvent être synchrones ou asynchrones, et avoir différents niveaux de priorité (haute, basse, ou normale).
• Le serveur de jobs gère une « file de priorité » de tâches, et transmet chaque job à un worker disponible capable de le traiter.
• Le worker exécute chaque job envoyé par un serveur et envoie une réponse au serveur qui la transmet au client initiateur de la demande.

Un job pourrait correspondre par exemple au redimensionnement d’une image, dans le contexte d’une application web recevant jusqu’à plusieurs milliers d’images uploadées par seconde.

Gearman fournit des interfaces de programmation (pour différents langages) destinées à être appelées par le code des applications (clients et workers) pour communiquer avec le serveur de jobs Gearman (figure 1, recréée d’après le site officiel gearman.org).

Figure 1 : pile applicative Gearman
Figure 1 : pile applicative Gearman

Mise en application initiale : sans Gearman

Nous allons créer une application web très simple avec le fonctionnement suivant : on peut soumettre des chaînes de caractères (cinq phrases), l’application effectue sur chacune un traitement relativement long (pour l’exemple, cela se résumera à transformer les lettres en capitales, mais en simulant un temps de calcul de trois secondes). L’application enregistre ensuite le résultat dans un fichier (un fichier distinct par phrase soumise), et une page affiche la liste des fichiers existants et leurs contenus.

Nous utiliserons deux machines (en réseau local), déjà installées et configurées :
• « www » (adresse IP 192.168.0.100) : serveur web (HTTP) Apache + PHP.
• « files » (adresse IP 192.168.0.150) : serveur de fichiers FTP.

Le code

Nous utilisons le langage PHP, la version du serveur est 5.5 (le code est compatible 5.4).

Nous écrivons les deux pages de notre application web : liste et formulaire. Pour garder l’exemple concis, nous ne faisons pas de séparation MVC, et nous plaçons nos deux fichiers PHP dans le dossier DocumentRoot d’Apache sur le serveur www. Nous factorisons juste quelques parties communes dans trois fichiers à inclure (dans un sous-dossier « inc »). Voici le code des cinq fichiers :

list.php :

form.php :

inc/config.php :

inc/html_begin.php :

inc/html_end.php :

Les trois fichiers à inclure sont très simples. Précisons juste que le fichier inc/config.php définit le chemin d’un dossier accessible en lecture-écriture sur le serveur files. Notons que le fichier inc/html_begin.php lie une feuille de styles non reproduite ici.

La page list.php affiche la liste des fichiers présents dans le dossier sur le serveur files, avec pour chacun le nom, la date de création (de modification) et le contenu (phrase originale, phrase transformée et nom de la machine ayant effectué le traitement).

La page form.php a deux modes. Si on y accède directement (requête GET), la page affiche un formulaire de type « post » dont la cible est cette page elle-même, avec cinq champs de texte. Si on a soumis le formulaire (requête POST), pour chaque phrase soumise, le code exécute la fonction « doUppercase » définie plus haut, en lui passant aussi un chemin de fichier unique à créer sur le serveur files. Cette fonction effectue le traitement long et écrit un fichier de résultat. Enfin, le code redirige vers la page list.php.

Test de l’application

On ouvre un navigateur web et on entre l’adresse « http://www/list.php » (ou « http://192.168.0.100/list.php ») pour afficher la page liste (figure 2).

Figure 2 : page liste de notre application web
Figure 2 : page liste de notre application web

On voit que la liste est vide. On clique sur le lien pour accéder à la page formulaire (figure 3).

Figure 3 : page formulaire de notre application web
Figure 3 : page formulaire de notre application web

On saisit des phrases dans les cinq champs de texte et on soumet le formulaire (figure 4).

Figure 4 : soumission de 5 phrases à l’instant T
Figure 4 : soumission de 5 phrases à l’instant T

On remarque alors que la page reste bloquée en attente d’une réponse pendant que le traitement tourne, et c’est seulement au bout de 15 secondes (5 fois 3 sec) que la requête reçoit enfin une réponse qui redirige vers la page liste. On voit alors que cinq fichiers ont été créés au rythme d’un toutes les 3 secondes (figure 5).
Figure 5 : liste affichée après redirection à T+15s
Figure 5 : liste affichée après redirection à T+15s

Problème

Afin d’éviter ce long blocage de 15 secondes durant lesquelles l’utilisateur ne peut rien faire (à part stopper la requête ou quitter la page), nous souhaiterions lancer le traitement en arrière-plan, rendre la main à l’utilisateur (au navigateur) et laisser les calculs se terminer de façon asynchrone. De plus, comme le traitement d’une phrase est indépendant des autres, nous aimerions que plusieurs calculs puissent s’effectuer en même temps en parallèle, éventuellement sur des machines distinctes.

Mise en application : ajout de Gearman

Nous utilisons des machines supplémentaires à installer et configurer :
• « gearman1 » et « gearman2 » (192.168.0.201 et 202) : serveurs de jobs Gearman (il en faut au moins un, le deuxième prend le relai si le premier ne répond plus).
• « workers1 », « workers2 » et « workers3 » (192.168.0.211, 212 et 213) : hôtes des workers Gearman.

Installation

Sur toutes les machines, le système d’exploitation est Ubuntu Server 14.04 LTS (« trusty ») 64 bits.

– Sur chacun des serveurs gearman1 et 2, on installe le paquet gearman, qui installe en fait les paquets gearman-job-server (qui fournit le serveur gearmand) et gearman-tools (qui fournit l’outil d’administration gearadmin et une interface en ligne de commande pour le client gearman) :

– Sur chacun des serveurs workers1, 2 et 3, on installe PHP (seule la version ligne de commande est nécessaire) et l’extension Gearman pour PHP (pour l’interface worker/serveur) :

– Et sur www, on installe aussi l’extension php5-gearman (pour l’interface client/serveur) :

Fonctionnement choisi

Schématisons comment fonctionne notre nouvelle architecture (figure 6).
Figure 6 : schéma de fonctionnement
Figure 6 : schéma de fonctionnement

Un worker, répliqué sur chacun des hôtes workers1 à 3, déclare une fonction sous le nom « uppercase » auprès des serveurs gearman1 à 2, et définit le traitement associé (correspondant à doUppercase).

L’application web, lors de la soumission de phrases, envoie pour chaque phrase une demande d’exécution asynchrone de la fonction « uppercase » à un des serveurs gearman1 à 2.

Chacun des serveurs gearman1 à 2 gère une file de tâches, et pour chaque tâche transmet un job à un des workers connus disponibles pouvant effectuer la fonction demandée (s’ils sont tous occupés, il attend qu’un se libère). Tout cela est fait de manière automatique par Gearman.

Chaque instance de worker effectue les jobs reçus des serveurs, cela étant fait de manière automatique par Gearman. Pour la fonction « uppercase », chaque exécution crée un fichier sur le serveur files.

L’application web, sur la page liste, continue de lister les fichiers créés sur le serveur files.

Le code

Nous écrivons le worker pour la fonction « uppercase » sous la forme d’un script PHP. Nous aurions pu choisir un autre langage supporté et installer les paquets correspondants, mais ici nous allons réutiliser notre fonction PHP doUppercase :

uppercase_worker.php :

Le fichier reprend d’abord la définition de la fonction doUppercase de form.php. En dessous, le code crée un objet worker Gearman qui peut se connecter aux serveurs gearman1 et gearman2, et enregistre auprès de ces serveurs une fonction « uppercase », en y associant un code de traitement, ici une fonction anonyme, qui reçoit un objet job Gearman, décode le workload (argument de la fonction « uppercase », ici un tableau associatif encodé en JSON) et exécute la fonction doUppercase avec les paramètres « input » et « result_path » du workload. Puis le code appelle la méthode « work » de l’objet, qui attend la réception d’un job d’un serveur, appelle alors le code de traitement associé à la fonction demandée, puis une fois le traitement terminé (ou en cas d’échec), retourne un booléen de succès. Alors, si le booléen indique un échec ou si le code de retour Gearman vaut autre chose que la valeur de succès, le script se termine (sortie de la boucle while), sinon le code rappelle la méthode work (boucle while), qui attend le prochain job, etc.

Nous copions ce script sur chacun des hôtes de workers, par exemple dans un sous-dossier « workers » de notre dossier home. Puis, sur chaque hôte, nous lançons un processus de worker (en arrière-plan avec redirection des sorties dans un fichier de log), avec la commande :

Puis sur chacun des serveurs Gearman, on vérifie qu’il y a bien eu enregistrement d’une fonction « uppercase » auprès du serveur par trois instances de worker, avec la commande :

qui affiche :

uppercase    0    0    3
.

et on peut voir le détail des workers avec la commande :

qui affiche quelque chose comme :

36 ::3530:3738:3400:0%1470921328 - :
33 192.168.0.211 - : uppercase
34 192.168.0.212 - : uppercase
35 192.168.0.213 - : uppercase
.

Nous modifions ensuite l’application web sur www. Dans form.php, nous supprimons la définition de la fonction doUppercase, qui est maintenant dans le code du worker, et modifions le code de gestion de la soumission du formulaire :

form.php :

Le nouveau code crée un objet client Gearman qui peut se connecter aux serveurs gearman1 et gearman2. Puis, pour chaque phrase soumise, l’application envoie automatiquement au premier serveur disponible une tâche en arrière-plan, sans attendre le résultat, demandant d’exécuter la fonction « uppercase » (enregistrée sous ce nom auprès du serveur) avec les arguments « input » et « result_path » (sous la forme d’un tableau associatif encodé en JSON car le workload doit être une string). Puis le code redirige vers la page liste.

Les autres fichiers restent inchangés.

Test de l’application

Avant tout, nous supprimons sur le serveur files les fichiers créés par la version initiale. On recharge la page liste dans le navigateur (on retrouve la liste vide comme sur la figure 2). Puis à nouveau, on accède au formulaire, et on soumet les mêmes phrases (comme sur la figure 4).

On constate que la requête reçoit maintenant une réponse quasi immédiate, qui redirige vers la page liste, sur laquelle la liste est pour l’instant toujours vide (figure 7).

On recharge la page dans le navigateur toutes les secondes, et au bout de 3 secondes on voit apparaître les trois premiers fichiers (figure 8), puis 3 secondes plus tard les deux autres (figure 9).

Figure 7 : liste rechargée entre T+0s et T+3s
Figure 7 : liste rechargée entre T+0s et T+3s

Figure 8 : liste rechargée entre T+3s et T+6s
Figure 8 : liste rechargée entre T+3s et T+6s

Figure 9 : liste rechargée après T+6s
Figure 9 : liste rechargée après T+6s

On peut remarquer au passage que le serveur Gearman n’a pas suivi d’ordre particulier pour les workers : bien qu’il traite les tâches dans l’ordre de leur réception, chaque job est soumis à un worker disponible quelconque.

Analyse des résultats

Nous avons bien obtenu le comportement souhaité : toutes les tâches sont lancées en arrière-plan, l’utilisateur est libéré presque immédiatement, et les N workers actifs permettent de générer N fichiers en même temps en parallèle.

Cet exemple était volontairement très simple. Pour une utilisation réelle on lancerait plusieurs instances de worker sur chaque hôte (sous forme de processus indépendants ou de threads par exemple). On aurait aussi probablement plusieurs workers (par exemple un autre script lowercase_worker.php pour une fonction « lowercase »). En outre, un même worker peut déclarer plusieurs fonctions (mais bien sûr chaque instance ne pourra en traiter qu’une à la fois).

Conclusion

Gearman nous a permis d’exploiter de manière automatique nos ressources de calcul en parallèle, via une API très simple à utiliser. L’architecture offerte est rapide à mettre en place et modulable : elle permet de facilement ajouter ou supprimer des machines, des workers et des fonctions, en laissant le serveur de jobs s’adapter à l’évolution.

Pour plus d’informations sur Gearman en général et sur les points non abordés, par exemple la communication entre des langages différents ou les files persistantes, on peut consulter :
• le site officiel (avec documentation) : http://gearman.org (en anglais)
• la documentation de l’extension PHP : http://php.net/gearman

Il est possible de retrouver l’intégralité de cet article dans les numéros 193 du mois de Février 2016 et 194 du mois de Mars 2016 du magazine « Programmez ».