Oui, je sais ce que vous allez me dire. En ce moment, je parle pas mal de ZeroMQ. Bah oui, c’est comme ça.
Bref. Suite à mon dernier article sur le sujet, l’un de mes développeurs m’a fait une remarque très pertinente, à laquelle j’ai répondu en expliquant au passage quelques notions concernant le travail distribué.
La situation
Je replace rapidement le contexte : Nous développons un petit programme de sauvegarde, constitué d’un serveur et de « workers ». Le serveur envoie aux workers des ordres, pour leur indiquer les machines à sauvegarder. Grâce aux fonctionnalités de load-balancing intégrées à ZeroMQ, le serveur n’a pas à se soucier de «qui fait quoi» ; il se contente d’envoyer les ordres, en se disant que les workers les recevront et les traiteront.
Le seul petit soucis est qu’il fallait prendre en compte le temps de création des workers, et le temps qu’ils mettent à se connecter au serveur. La solution que j’ai présenté est d’ajouter un canal de communication supplémentaire, qui permet aux workers d’indiquer au serveur qu’ils sont prêts.
Voici le schéma de l’infrastructure finale :
Les workers font du PUSH vers le serveur, pour lui confirmer leur disponibilité. Le serveur fait du PULL pour recevoir ces confirmations, puis fait du PUSH pour envoyer ses ordres, qui sont répartis entre les workers. Les workers font du PULL pour recevoir les ordres et les traiter.
La solution alternative
Mon développeur m’a alors demandé : « Mais pourquoi les workers ne feraient pas du REQ pour demander au serveur le prochain ordre à exécuter, qu’il enverrait par REP ? »
Pour rappel, le REQ/REP (request/response) est l’un des 3 types de communications supportées par ZeroMQ. À chaque requête effectuée (du côté de la socket REQ), une réponse doit obligatoirement être envoyée (côté REP).
Effectivement, le schéma de l’architecture devient plus simple :
En faisant ainsi, on se soustrait la partie « initialisation », pendant laquelle les workers indiquent au serveur qu’ils sont connectés et disponibles. Par contre, tout le reste du serveur se complexifie.
Dans un tel fonctionnement, les workers vont se connecter au serveur, pour lui demander «Eh, qu’est-ce que je dois faire ?». Le serveur devra garder en mémoire une liste des machines à sauvegarder ; à chaque fois qu’un worker fera une demande, il devra dépiler un nom de machine, pour l’envoyer dans sa réponse au worker.
Cela n’a rien de bien méchant, nous sommes d’accord. Juste une liste supplémentaire à gérer. Mais si on veut gérer une remontée d’information (que les workers préviennent le serveur quand ils ont terminé une tâche), il faudra là encore ajouter un peu de code supplémentaire.
Si on compare avec le fonctionnement précédent, on s’aperçoit d’une chose : En simplifiant l’architecture réseau, on complexifie le code applicatif ; en complexifiant légèrement l’architecture réseau, on simplifie le code applicatif.
Toute la magie de ZeroMQ est là. Ajouter une socket ZMQ supplémentaire ? C’est seulement deux lignes de code en plus !
Il est donc plus simple de faire reposer la complexité sur ZeroMQ, le code n’en est que plus simple à comprendre, plus rapide à écrire, plus facile à maintenir.
Une file de message
Imaginons maintenant que nous voulions utiliser ZeroMQ pour mettre en place une file de message.
Je vais prendre un exemple concret. Dans mon entreprise, nous devons indexer des contenus, pour permettre leur recherche textuelle. Cette étape est relativement longue et coûteuse en CPU, elle ne peut donc pas être faite par les processus qui communiquent avec les navigateurs web au moment où les contenus sont créés.
Les contenus sont donc enregistrés sans être indexés, et des messages sont ajoutés à une file de messages pour indiquer les contenus qui doivent être indexés.
Sur une machine dédiée, un programme est exécuté à intervalles réguliers. Il consulte la file de messages pour savoir si de nouveaux contenus doivent être indexés.
La file de messages que nous utilisons est très frustre, basée sur une table en base de données. L’extension FineMessageQueue disponible sur le site de notre framework Temma en reprend le principe.
Si nous voulions la remplacer par une file de messages réalisée avec ZeroMQ, comment nous y prendrions-nous ?
Pour commencer, nous devrions écrire un programme qui fera office de « serveur » (mettons de côté toute notion de redondance ; pour nous simplifier la vie, on va faire une file centralisée).
D’un côté, le serveur recevra des messages, qu’il ajoutera à sa file.
La file de message pourrait être stockée uniquement en mémoire, mais c’est un peu risqué. Partons du principe que les messages sont stockés sur disque ou dans une base de données.
De l’autre côté, le serveur va envoyer ses messages à des workers qui sont là pour ça et qui attendent qu’on leur demande de travailler.
Avec ce type d’architecture, il paraît évident que la communication entre le serveur et les workers ne va pas se faire par du REQ/REP. Si on imagine que les workers fassent un REQ pour savoir ce qu’ils ont à faire, et que la file de messages est vide, que se passe-t-il ? Le serveur répond qu’il n’y a rien à faire ; donc le worker ne « fait rien ». Et après ? Il redemande au serveur ce qu’il a à faire. On tombe vite dans la boucle qui consomme énormément de CPU pour rien.
Il est donc bien plus logique que ce soit le serveur qui informe les workers du travail qu’ils ont à faire. C’est un peu différent de la vision classique du client-serveur, mais c’est beaucoup plus efficace, et ZeroMQ permet d’y arriver très simplement.
Une architecture complète
Dans l’idée d’une telle file de messages, on considérera que les workers « sont là ». Ce n’est donc pas le serveur de file de messages qui va les démarrer. Cela implique potentiellement d’avoir des mécanismes dédiés à la gestion des workers : un démon se charge de lancer de nouveaux workers quand il n’y en a pas assez (dans une limite maximale définie), et les tue quand il y en a trop d’inactifs (dans une limite minimale définie).
Idéalement, le serveur de file de messages doit être capable de relancer une tâche qui n’a pas été déclarée comme terminée, après un certain délai. Ce qui veut dire que les workers doivent informer le serveur qu’ils ont terminé telle ou telle tâche.
On ne parle jamais trop des bons outils 😉