Le blog

Retour sur le sfPot du 30 avril

Publié le 12 juin 2015

Jeudi dernier, Les-Tilleuls.coop co-organisait un nouveau sfPot lillois en collaboration avec SensioLabs, CGI et Auchan E-commerce. Pour cette cinquième édition, le succès était au rendez-vous : ce sont plus de soixante-dix personnes qui ont investi le Meatballs, un restaurant du Vieux-Lille privatisé pour l'occasion. Cette soirée placée sous le signe de la convivialité a été rythmée par deux talks enrichissants sur le thème du framework SymfonyVincent Chalamon, développeur chez Les-Tilleuls.coop, nous a d'abord montré comment implémenter RabbitMQ dans une API Symfony 2.

C'est ensuite Anthony Blin, développeur chez CGI, qui a pris la parole pour nous parler de l'optimisation de Symfony avec HHVM. Après les conférences, les échanges se sont poursuivis autour d'un copieux buffet à base de boulettes et de quelques verres offerts.

Une fois de plus, ce sfPot a donc été une belle réussite !

Un grand merci à tous d'avoir répondu présent ainsi qu'aux co-organisateurs de cet événement et bravo aux conférenciers ! On se retrouve très bientôt pour un sixième sfPot... Stay tuned ! Pour vous faire patienter, voici la version écrite du tutoriel présenté par Vincent. Vous pouvez également la retrouver sur son blog. Bon codage !

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.

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ù plusieurs projets différents 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 communicationsynchrone 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 lecture.

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 OldSoundRabbitMqBundleOldSoundRabbitMqBundle(),
    );
}

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éer un exchange (que nous nommerons par exemple foo_exchange). Puis, éditez le fichierapp/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 exchangeoption 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._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 OldSoundRabbitMqBundleRabbitMqConsumerInterface :

<?php  
namespace LesTilleulsDemoBundleComponentAMPQ;use LesTilleulsDemoBundleEntityFoo;  
use OldSoundRabbitMqBundleRabbitMqConsumerInterface;  
use PhpAmqpLibMessageAMQPMessage;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 

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

Vincent Chalamon

Mots-clésBière, Fabien Potencier, framework, HHVM, Lille, Meatballs, PHP, RabbitMQ, sfPot, Symfony

Le blog

Pour aller plus loin