Techos Blog technique de Xotelia

Mise en place de Robocop

Cela fait maintenant quelque temps que c’est en production, mais il n’est jamais trop tard pour en parler. Xotelia est un gestionnaire de canaux de vente. Notre job est de faire en sorte que les calendriers de toutes les chambres, gîtes, hôtels, etc qui sont gérés par Xotelia soient à jour sur tous les sites de vente sur lesquels ils sont présents, ce peut être Airbnb, comme Booking.com ou encore Expedia. Donc il faut sans cesse aller mettre à jour les calendriers de ces sites dès que l’on reçoit une réservation, ou qu’un propriétaire modifie ses calendriers sur notre back office.

Contextualisation

Historiquement, cette synchronisation est faite de manière asynchrone via un système d’événements et de queues. Seulement le problème c’est que la queue en question c’est MySQL et MySQL est fait pour faire plein de choses, mais surtout pas un système de queue. Pour ça il y a RabbitMQ par exemple. Non seulement MySQL n’est pas fait pour, mais plus la base client augmente, plus le nombre de synchronisations demandées augmente, plus la taille de la queue augmente et donc plus les performances de MySQL se dégradent. Suite à cette constatation nous avons voulu aller plus loin en sortant le système de synchronisation du back office pour 1. ne pas gêner les performances du backoffice et 2. le réécrire avec des technos adaptées.

Ainsi est né Robocop.

Cette décision fait suite à notre volonté d’orienter Xotelia vers une architecture de type microservices pour pouvoir plus facilement le maintenir mais surtout encaisser la charge à venir.

Architecture

Nous sommes tout de suite partis sur une architecture complèmetent asynchrone, en essayant de découper au maximum les process. Désormais, quand une demande de synchronisation apparait, au lieu d’être stockée dans la queue MySQL, elle est envoyée à Robocop via une API REST.

process

Comme vous pouvez le voir ce n’est pas les données à synchroniser qui sont envoyées à Robocop, mais seulement un masque de synchronisation qui indique ce qui doit être synchronisé. Nous avons fait ce choix d’une part pour des raisons de performamce, mais surtout parce que entre le moment où la demande est envoyée et le moment où Robocop va effectivement mettre à jour le canal de vente, il peut se passer plusieurs secondes voire quelques minutes. Et donc la donnée peut avoir changé entre temps.

Nous avons donc plusieurs process, le premier, est un simple contrôleur, il se contente de prendre le masque de synchronisation et de l’envoyer dans un premier exchange. Le deuxième est un worker de calcul, il prend le masque, récupère les données à synchroniser, les re-découpe en fonction des besoins du canal de vente sur lequel il faut mettre à jour et envoie le tout dans un deuxième exchange pour la synchronisation.

Pour ce deuxième exchange, nous sommes partis sur le modèle topic de RabbitMQ, qui permet et rediriger vers telle ou telle queue en fonction d’une clée de routage. Ce modèle correspond très bien à nos besoins, car il permet d’avoir un ou plusieurs par queue spécifique à chaque canal de vente.

Ensuite la dernière étape consiste à prendre le message dans la queue relative au canal de vente en question, et de pousser les données.

Under the hood

Il s’agit d’un projet Symfony 2 avec RabbitMQ. Rien de bien complexe à mettre en place. Le problème a surtout été de trouver comment gérer les workers. Nous sommes d’abord partis sur une technique pas très propre, avec un script bash qui lance les workers, récupère les PID et les relance s’ils ont consommé le nombre de messages demandé. Cette technique n’est pas a recommander car se baser sur le PID via bash n’est pas ce qu’il y a de plus sûr.

Supervision des workers

Nous sommes donc partis sur Supervisor. Il s’agit d’un superviseur de processus écrit en python avec une configuration déclarative comme systemd, à l’inverse d’initscript ou la configuration se fait en bash (mourir). L’avantage de cette technique est que ce programme est conçu pour maintenir les services qu’il gère up. Si le processus est tué ou que le nombre de message à consommer est atteint, supervisor se charge de relancer le processus. Le problème est que du coup on ne peut plus vraiment spécifier un nombre de message pour chaque worker, car supervisor ne ferait que relancer des processus en permanence, et il n’est pas conçu pour ça. Désormais, chacun de nos workers consomme un nombre de messages infini jusqu’à être stoppé ou relancé.

Déclaration des queues

Pour RabbitMQ nous sommes partis sur le très bon RabbitMQBundle. La documentation est très bien pour faire des choses simples, par contre dès que l’on souhaite profiter au maximum des possibilités de RabbitMQ, il faut fouiller. C’est le cas pour les exchanges de type topic. Pour la partie consumer il faut définir un consumer pour chaque queue différente que l’on veut. Un seul producer suffit.

old_sound_rabbit_mq:
    producers:
        test:
            connection: default
            exchange_options: { name: 'test', type: topic }
    consumers:
        foo:
            connection: default
            exchange_options: { name: 'test', type: topic }
            queue_options: { name: 'test.foo', routing_keys: ['foo'] }
            callback: foo.consumer
        bar:
            connection: default
            exchange_options: { name: 'test', type: topic }
            queue_options: { name: 'test.bar', routing_keys: ['bar'] }
            callback: bar.consumer

Avec cette configuration nous disposons d’un producer et de deux consumers. Tous les messages envoyés par notre producer test avec la clé foo iront dans la queue test.too et ceux avec la clé bar iront dans la queue test.bar. Il est possible de ne pas spécifier le nom de la queue et de laisser RabbitMQ s’en charger. Mais ceci devient problématique quand les consumers sont redémarrés, RabbitMQ génère un nouveau nom de queue et donc tous les messages dans l’ancienne queue sont perdus. Aussi, si on spécifie le nom de la queue, mais qu’on lance les consumers avec des clés de routage différents, il se passe la même chose que si on avait configuré l’exchange en mode direct.

État des lieux

Cela fait maintenant plusieurs mois que Robocop est en production, nous avons maintenant une dizaine de canaux de ventes gérés par Robocop et les performances s’en ressentent sur le back office. De plus avec RabbitMQ, que l’on ait 3 messages dans une queue, ou 3 millions, les performances seront les même (à peu de chose près bien sûr). Nous avons une latence de sychonisation d’une dizaine de secondes en moyenne. Mais grâce à cette architecture nous allons pouvoir profiter du cloud et ainsi déployer Robocop sur autant de machines que nécessaire pour s’adapter au traffic.

La prochaine étape consiste à avoir un dashboard sur lequel on peut choisir les workers dont on a besoin, dire le nombre de process qu’on veut et laisser Robocop les placer sur tel ou tel serveur en fonction de la charge et des restrictions.