Après ces quelques mois sans nouvelle, voici un article de mes dernières avancées sur mon projet Nodecast. En effet depuis mon dernier article Nodecast : architecture d’une application web il y a eu quelques changements d’implémentation.
Constat et évolution
Tout d’abord Gearman qui était idéal sur le papier s’est avéré très instable à l’usage, comme l’ont constaté d’autres développeurs sur la liste de diffusion. Ils ont l’air malgré tout d’avoir confiance en leur produit car ils vont ouvrir un service d’hébergement de files d’attente basé sur German : GearmanHQ.
Après quelques recherches je me suis finalement dirigé vers un serveur de files qui implémente le protocole AMQP ce qui permet de ne pas dépendre d’un serveur en particulier. Plusieurs serveurs libres l’implémentent, comme RabbitMQ (racheté par VMware), ActiveMQ ou Qpid.
L’autre problème est venu des workers qui traitent les données envoyées par le client desktop ( nodecast-gui ). Le code Ruby était tellement lent qu’il fallait parfois plus de 1 minute pour effectuer un traitement de parsing et insertion dans MongoDB. Je pense que le problème venait de la pile Ruby / Mongoid / Mongodb ruby driver et non de Ruby seul, mais il n’est tout simplement pas concevable que le worker qui intègre dans la base les processus utilisateur met plus ou moins 2 minutes pour effectuer 1 traitement avec une charge CPU maximale.
J’ai donc décidé de récrire les workers ainsi que le dispatcher, en C++ avec l’aide de Qt. Mes tests en développement sont passés à moins de 1 seconde sur le worker process, le plus lourd…. Evidemment l’effort de développement est certes plus conséquent mais les résultats sont largement payant pour que cet investissement technique paye.
Qpid quant à lui s’est pour l’instant imposé de lui même, car il est le seul à proposer une API C/C++ fonctionnelle. Des plugins permettent de lui ajouter une persistance sur disque, des fonctionnalités de cluster, du support SSL et XML en natif. Il fait d’ailleurs partie du coeur de la solution de Red Hat Enterprise MRG. Cette architecture représentée par le schéma plus bas reste tout de même à valider par l’épreuve du feu de la production.
Ingénierie
Néanmoins il est à mes yeux évident qu’un service web ayant pour objectif à moyen/long terme la prétention de monter en charge, se doit d’avoir une architecture scalable et cela dès sa conception. C’est toute la différence entre créer un site web et créer une architecture web, ou bien entre le développement logiciel et l’ingénierie logicielle. Cette dernière implique une réflexion sur les méthodes de travail, les outils à utiliser, qu’ils soient ceux utilisés par les développeurs que ceux à utiliser dans l’architecture; la veille techno, etc. En somme tout ce qui permet d’optimiser sa productivité, l’architecture mise en place n’en sera que le reflet, réussi ou pas, de ces choix…
Voici un exemple du résultat de l’ingénierie logiciel avec cette présentation de l’architecture logicielle de la nouvelle version de LinuxFR.org l’un des plus gros site technique francophone.
Implémentation
Pour revenir à Nodecast, les workers utilisent les drivers natifs de memcached, pour invalider le cache dont la page a été mise à jour, de MongoDB et de Qpid, ce qui permet, en ayant aucune couche d’abstraction intermédiaire d’obtenir les performances maximales. Le framework Qt permet d’obtenir une certaine simplification du développement, grâce entre autre aux signaux, même si en effet la bibliothèque Boost intègre cette fonctionnalité.
J’ai développé nodecast-worker de manière relativement générique afin qu’il instancie la bonne classe worker selon le paramètre –worker-type fourni en argument :
nodecast-worker --memcached-ip=127.0.0.1 --memcached-port=11211 --mongodb-ip 127.0.0.1 --mongodb-base=nodecast_prod --qpid-ip=127.0.0.1 --qpid-port=5672 --worker-type=process
L’usage du serveur de file Qpid, permet outre le fait de rendre les traitements asynchrones, de monter un cluster de worker d’un même type sans développement particulier. En effet chaque worker s’abonne à la même file d’attente Qpid de type pub/sub (amq.topic). Une routing key appliquée par le dispatcher sert à taguer chaque message envoyé dans la file d’attente. Ainsi les workers filtrent les messages qui leur sont destiné, par exemple le tag worker.cpu pour le worker qui va traiter les messages qui contiennent les données de CPU envoyées par le client desktop nodecast-gui.
Comme il est possible de lancer plusieurs instances worker du même type, chacun des worker dépile la même file taguée ce qui permet de répartir la charge sur plusieurs processus, voir aussi de la répartir sur des machines physiques différentes puisque les workers utilisent des connexions TCP … !
Pour comprendre les rouages et le potentiel d’une telle architecture, je conseille la lecture de ces 2 excellents articles sur l’utilisation d’un serveur AMQP : Introduction à RabbitMQ – AMQP Partie I et Introduction à RabbitMQ – AMQP Partie II
Pour finir sur cet épisode, ce développement me met de plus en plus la puce à l’oreille sur la nécessité de développer un framework / service qui permettrait le développement rapide de workers, de les enchaîner, les monitorer en temps réel et de les administrer. Ce framework / service proposerait en outre l’accès à une multitude de bibliothèques et d’API vers des services externes afin de pouvoir implémenter n’importe quelle idée, de manière rapide, stable et scalable. Cela éviterait de devoir redévelopper la roue et de devoir gérer toutes les exceptions inhérente à l’utilisation d’API bas niveau (timeout, déconnexion, reprise, exception, start/stop, …). En quelque sorte un tarpipe OpenSource mais qui permettrait d’utiliser n’importe quel langage script ou langage compilé. Comme on dit, je dis ça, je dis rien
Workflow
Le schéma suivant montre l’architecture en court de développement du backend de Nodecast. On y voit les 3 paliers asynchrones par lesquelles transite le traitement d’un message.
Palier 1
Le premier palier nécessite 7 étapes.
- Le client nodecast extrait les données système de la machine, génère un XML et l’envoie par un POST (ajout) ou un PUT (update) HTTP.
- le serveur web Nginx fait suivre la requête vers le cluster web Thin.
- Thin exécute via rack le DSL Sinatra qui sert à créer simplement l’API REST de nodecast.
- Ce dernier vérifie dans MongoDB les droits d’accès (couple email / token) grâce à un auth basic request HTTP transmis par le client,
- Si l’autorisation a réussie, le code Sinatra stocke le XML dans le GridFS mongoDB, génère une collection de hash et transmet la charge dans queue dédiée au dispatcher.
- Le code sinatra génère un XML de réponse au client.
- NGinx le fait suivre au client, ce qui termine du point de vue utilisateur le traitement.
L’objectif de ce palier est d’être le plus minimaliste possible afin qu’une autre requête puisse être traité avec le moins d’attente possible. L’auth, la conception de la charge, sa transmission et la réponse au client sont malgré tout chacune nécessaire. Dans cette architecture asynchrone il n’est pas possible de signaler dans la même passe, la bonne fin du traitement à moins de revenir à une architecture synchrone non scalable… A vrai dire il n’est de toute manière pas nécessaire de le signaler puisque le service tourne en arrière plan sur le poste utilisateur.
Palier 2
La transmission de la charge utilise 2 canaux AMQP direct : dispatcher.update ou dispatcher.add. Le dispatcher est lancé selon cette ligne de commande :
nodecast-dispatcher --mongodb-ip 127.0.0.1 --mongodb-base=nodecast_prod --qpid-ip=127.0.0.1 --qpid-port=5672
- Il écoute les 2 files d’attente puis lors de la réception d’une charge, il vérifie l’existence de l’hôte à mettre à jour si c’est un update ou bien créé l’hôte si c’est un ajout.
- Il injecte ensuite le XML extrait du GridFS dans un QHash Qt. Il sérialise ce QHash et envoie par AMQP la partie dédiée à chaque file de chaque worker concerné (hash["network"] pour le worker network par exemple). En clair, le XML est découpé et chaque morceau est envoyé dans la file d’attente de chacun des workers.
Palier 3
- Tous les workers sont lancés et écoutent la file d’attente amqp.topic sur leur tag respectif (worker.cpu, worker.load. worker.uptime, worker.network, worker.memory, worker.process).
- A réception d’un message, ils le sérialisent, mettent à jour la base de donnée puis invalident le cache de leur page web associée.
- Ils transmettent via syslog leurs statuts et leurs exceptions.
Avenir
A ce jour les workers process et cpu ont été réécrit et fonctionnent. Il reste l’implémentation des logs vers syslog ou AMQP afin de tracer les traitements des workers via des streams Graylog2.
Je souhaite remplacer Memcached par Redis pour profiter de ses très intéressantes fonctionnalités. Migrer le frontal web de Rails 2 vers la version 3. Stabiliser le client desktop et lui ajouter toutes les fonctionnalités de la lib SIGAR. Développer un client Android. Terminer le rework et Dominer le Monde
