Implémenter RabbitMQ dans une API Symfony 2

Depuis Matrix, vous avez toujours rêvé de « suivre le lapin blanc ». Mais voilà, le seul lapin que vous ayiez trouvé ressemble plutôt au chapelier fou ou à Roger Rabbit !

Je vous présente un lapin plus efficace : RabbitMQ.

Rabbit aime quoi ?

Basé sur le protocole AMQP (Advanced Message Queuing Protocol), RabbitMQ est un gestionnaire avancé de files de messages. Il permet d'envoyer et recevoir des messages aux plateformes connectées à son service.

OK… et sinon ça veut dire quoi ?

Votre application va pouvoir envoyer des informations de façon asynchrone à d'autres applications utilisant le même service.

Ah, « asynchrone », j'ai déjà entendu ce mot là quelque part, il paraît que c'est bien non ?

Ça s'avère très utile dans certaines situations. Imaginons par exemple que vous ayiez plusieurs applications devant communiquer entre elles :

  • Une API publique
  • Une application privée gérant les statistiques
  • Etc…

Dans votre API, en cas d'ajout d'un enregistrement, vous souhaitez mettre à jour les statistiques.

Ah, ça je sais déjà faire : j'appelle un WebService (par exemple en REST) vers l'application gérant les statistiques pour lui transmettre l'enregistrement !

En effet, mais si un grand nombre de personnes ajoutent un enregistrement en même temps, vous allez requêter autant de fois votre application. Cela peut être très risqué !

L'alternative consiste à mettre en attente ces informations dans un service, et laisser l'application des statistiques récupérer ces informations quand elle le souhaite.

Ainsi, lors de l'ajout d'un enregistrement, l'API va envoyer un message à RabbitMQ contenant toutes les informations que vous souhaitez partager. On parle ici d'un Producer. RabbitMQ va le stocker jusqu'à ce qu'une application le récupère. Cette application est appelée Consumer.

RabbitMQ

Installation

Le service RabbitMQ existe en .exe, .tar.gz, .deb, .rpm, .zip, brew, etc… Mieux encore, de nombreux plugins proposent une compatibilité de ce service dans les principaux langages Web utilisés de nos jours : PHP, Node.JS, Java, Ruby, Python, etc…

Vous n'avez qu'à installer ce service…et l'allumer ! Simple non ? ☺

Bonus : vous pouvez faire tourner RabbitMQ grâce à Docker :

docker run -d -P rabbitmq:3-management  

Lancement/arrêt du service

Le service RabbitMQ est disponible à travers la commande suivante :

rabbitmq-server  

Il est également possible de lui passer l'option -detached afin de le lancer comme daemon. Pour l'arrêter, il vous suffit de lancer la commande :

rabbitmqctl stop  

Interface de gestion

Un grand avantage de RabbitMQ face à ses concurrents est qu'il propose une interface complète de vue, gestion et d'administration de son service.

Parmi ses options, il propose en particulier une vue sur le trafic, l'administration des listes (exchanges/queues), l'administration des utilisateurs, et même une visualisation des relations entre les listes.

Une fois le service lancé, l'interface est disponible en vous rendant sur http://localhost:15672.

Exchange vs Queue

Il est important sous RabbitMQ de comprendre ce qu'est un exchange et une queue.

Un exchange est une liste de publication de messages, tandis qu'une queue est une liste de lecture de messages publiés depuis un exchange.

Hum… quelqu'un a compris ce qu'il a dit le zouave ?

Je vais tâcher d'être plus clair : un message est soumis sur une liste que l'on appelle un exchange. Cette liste va transmettre le message à d'autres listes qui lui sont connectées, que l'on appelle des queues. Le message sera alors disponible en lecture depuis ces dernières listes.

Il existe plusieurs types d'exchange, les principaux sont :

  • Fanout : le message est transmit dans chacune de ses queues liées
  • Direct : le message est transmit à 1 seule queue identifiée
  • Topic : le message est transmit dans N queues filtrées

Mais alors Fanout = Topic, non ?

Et bien non, justement. Dans le cas d'utilisation d'un type Topic, un filtre (routing_key) est utilisé afin d'identifier à quelle(s) queue(s) transmettre le message.

Options

Et c'est tout ce qu'il propose ton lapin ?

Et…non ! Il existe encore plein d'options intéressantes à mettre en place sur vos projets. En voici quelques-unes que j'utilise souvent :

  • Vhost : cette option permet d'encapsuler vos listes dans une domaine (comprendre url), dans le cas où différents projets utilisent le même service RabbitMQ
  • Lazy : les services instanciés à la demande vous permettent d'alléger l'instanciation de vos services, mais Symfony vous en parlera bien mieux
  • RPC : Remote Procedure Call, cette option permet une communication synchrone entre vos applications, l'émetteur du message attendra donc la réponse

Attention : l'utilisation du mode RPC implique un mode synchrone ! À utiliser avec parcimonie.

Cas concrêt : une API Symfony 2

Reprenons l'exemple précédent : une API et une application de statistiques, toutes deux développées en Symfony 2. Il faudra mettre en place le service RabbitMQ sur chacune, l'une pour l'émission du message, et l'autre pour la récupération.

Commençons par installer les dépendances dans chaque projet :

composer require videlalvaro/rabbitmqbundle  

Puis, déclarez ce bundle dans le fichier app/AppKernel.php :

public function registerBundles()  
{
    $bundles = array(
        ...
        new OldSound\RabbitMqBundle\OldSoundRabbitMqBundle(),
    );
}

Voilà, ce n'était pas trop long j'espère ? ☺

Le paquet RabbitMQBundle requiert la librairie PHP-AMQLIB. Cependant, celle-ci est déjà incluse dans les dépendances, vous n'avez donc rien de plus à faire.

Ça c'est cool, merci Alvaro Videla !

Configurons maintenant ce bundle, à commencer par la connexion au serveur RabbitMQ. Dans votre fichier app/config/config.yml, déclarez la configuration suivante :

old_sound_rabbit_mq:  
    connections:
        default:
            host:     localhost
            port:     5672
            user:     guest
            password: guest
            vhost:    /
            lazy:     false

Il est nécessaire de déclarer au moins l'hôte (localhost, ou l'adresse du serveur distant), le port utilisé (par défaut : 5672), et l'authentification (par défaut : guest:guest). Il est bien entendu recommandé de modifier ces paramètres d'authentification !

Maintenant, publions notre premier lettre d'amour. Pour cela, il faut déclarer un…un…un Producer (on voit ceux qui suivent !).

Dans l'interface d'administration de RabbitMQ, créez un exchange (que nous nommerons par exemple foo_exchange). Puis, éditez le fichier app/config/config.yml pour y ajouter la configuration suivante, déclarant notre Producer :

old_sound_rabbit_mq:  
    producers:
        foo:
            connection: default
            exchange_options:
                name: 'foo_exchange'
                type: topic

La section exchange_option définit la correspondance avec l'exchange dans le service RabbitMQ. Il faut donc que le name et le type concordent !

Une fois cette configuration mise à jour, vous disposerez d'un service déclaré selon la structure old_sound_rabbit_mq.<name>_producer. Dans notre exemple, cela donnera old_sound_rabbit_mq.foo_producer.

Récupérez ce service où vous le souhaitez, puis publiez le message comme suit :

$producer->publish(serialize($object));

La serialization par défaut est serialize.

Votre API est maintenant capable d'envoyer des mots d'amour à sa dulcinée.

Parlons-en de sa dulcinée justement, celle-ci doit récupérer ce message et le lire. Dans l'interface d'administration de RabbitMQ, créez une queue et lier-la à l'exchange précédemment créé :

Puis, éditez le fichier app/config/config.yml pour y déclarer votre Consumer comme suit :

old_sound_rabbit_mq:  
    consumers:
        bar:
            connection: default
            exchange_options:
                name: 'foo_exchange'
                type: topic
            queue_options:
                name: 'foo_queue'
            callback: les_tilleuls_demo.consumer.foo

L'option callback fait référence à un service Symfony qu'il nous faut maintenant déclarer. Créez une classe FooConsumer implémentant l'interface OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface :

<?php  
namespace LesTilleuls\DemoBundle\Component\AMPQ;

use LesTilleuls\DemoBundle\Entity\Foo;  
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;  
use PhpAmqpLib\Message\AMQPMessage;

class FooConsumer implements ConsumerInterface  
{
    /**
     * {@inheritdoc}
     */
    public function execute(AMQPMessage $msg)
    {
        /** @var Foo $foo */
        $foo = unserialize($msg->body);
        echo 'foo '.$foo->getName()." successfully downloaded!\n";
    }
}

N'oubliez pas de déclarer votre service dans Symfony !

Enfin, le bundle RabbitMQBundle propose une commande vous permettant de récupérer les messages selon un Consumer :

php app/console rabbitmq:consumer <name>  

Dans notre précédent exemple, la commande sera donc :

php app/console rabbitmq:consumer foo  

Bonus : il existe même une option permettant de limiter le nombre de messages récupérés : -m 50. À vous de modifier ce nombre par votre propre valeur.

Vous l'aurez compris, il est tout à fait possible d'avoir autant de Consumer que de fonctionnalités.

Important : une fois un message récupéré, il est supprimé de la queue dans le service RabbitMQ.

Picoti !

Face à sa concurrence, RabbitMQ est donc :

  • Très simple à installer
  • Disponible dans tellement de langages : Ruby, Python, PHP, Perl, JAVA
  • Disponible depuis Symfony 2.0 grâce au bundle RabbitMQBundle
  • Très souple à manipuler grâce à son interface de gestion
  • Optimal car permet une répartition de la charge : s'il y a un pic de publication, ils sont stockés dans la queue et traités petit à petit par le Consumer

Picota !

Bon, tout n'est pas rose non plus, RabbitMQ et le bundle RabbitMQBundle ont quand même leurs inconvénients et limites :

  • Orange (plutôt bizarre pour un lapin non ?)
  • Si vous souhaitez utiliser l'extension, celle-ci n'est pas installée par défaut dans PHP
  • L'utilisation de la librairie PHP-AMQLIB est plus lourde que l'extension : voir le benchmark

Et…c'est tout comme inconvénient ?

Et bien oui, c'est tout. Mais j'attends impatiemment vos retours et commentaires pour savoir ce qui ne vous plaît pas (et surtout ce qui vous plaît) dans RabbitMQ !

Outils similaires

Non pas qu'ils m'aient payé pour que je parle d'eux, mais je pense qu'il est néanmoins important de connaître les alternatives à ce mignon lapin orange :

Sources

Ils m'ont inspiré, tout a commencé grâce à eux, ils sont mes sources, mon inspiration, mes muses :

Vous trouverez les sources de ce tutoriel sur GitHub : https://github.com/vincentchalamon/poc-rabbitmq.

Hey, vous pouvez aussi consulter ma présentation au sfPot : http://slides.com/vincentchalamon/rabbitmq