Retour au blog

REX : RabbitMQ et Supervisor avec Docker et le Composant Messenger de Symfony

... C’est bon, j’ai fait mon quota de buzzwords, je peux vous laisser...

Plus sérieusement, je travaille actuellement sur un projet qui me demande de gérer un grand nombre de traitements, sur un grand nombre de données. De fait, le composant Messenger de Symfony s’est rapidement avéré être une solution adéquate. En voici les raisons :

  • une abstraction permettant de choisir l’implémentation adaptée et d’en changer si besoin 
  • une possibilité de paralléliser les traitements indépendants et de le faire en background
  • (et je dois bien l’avouer) un peu la hype du moment

En plus, depuis la version 4.3, ils ont ajouté un transport Doctrine ! Du coup - avec toute l’assurance du mec qui ne va pas s'embêter à configurer un RabbitMQ qu’il ne connaît pas - je me suis lancé dans l’aventure.

Comme le titre l’indique, j'étais un peu à côté de la plaque. Cela me donne néanmoins la possibilité de vous raconter mon histoire et de vous fournir des informations utiles pour configurer vos machines.

Premier essai avec le Transport Doctrine

La configuration du transport Doctrine est assez simple. Il suffit de suivre la documentation et on arrive très rapidement à envoyer et consommer des messages. Hourrah ! 

Cependant, on se rend rapidement compte que le traitement de chaque message est long. Sur ma machine, les traitements sont de l’ordre d’une seconde par message, pour des messages assez simples.
J’avais une volumétrie de 30 000 messages par matinée (minimum absolu). Cela m'amenait donc déjà à 8h de traitement minimum par matinée (sans compter la logique de traitement). Alors certes, je pouvais paralléliser. Mais cette solution ne me semblait pas optimale. J'ai donc décidé de changer mon fusil d’épaule.   

Benchmark : pour 500 messages, à 4 process concurrents, 150 secondes.

Installation de RabbitMQ

Comme vous le savez probablement, RabbitMQ est l'un des message broker les plus utilisés. C’est le transport historique compatible avec Messenger. J'avais plutôt eu de bons échos le concernant, et j'étais convaincu que c'était plus rapide qu'un message/seconde. Okay, vendu !

Par contre, il fallait que je l’intègre à ma configuration Docker. C’est donc ce que je vous propose aujourd’hui :

docker-compose.yml

version: '2'
services:
    ....
    rabbitmq:
        image: rabbitmq:3-management
        hostname: rabbit
       ports:
           - 5672:5672
           - 15672:15672
        networks:
            default:
                aliases:
                    - service.rabbitmq

.env

MESSENGER_TRANSPORT_DSN=amqp://guest:guest@service.rabbitmq:5672/%2f/messages
MESSENGER_TRANSPORT_FAILED_DSN=amqp://guest:guest@service.rabbitmq:5672/%2f/failed

config/packages/messenger.yaml 

framework:
   messenger:
       failure_transport: failed
       transports:
           # https://symfony.com/doc/current/messenger.html#transport-configuration
           async:
               dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
           failed:
               dsn: '%env(MESSENGER_TRANSPORT_FAILED_DSN)%'
       routing:
           Core\Domain\Common\Message\AsyncMessage: async
  

/etc/hosts

127.0.0.1       service.rabbitmq

Voilà, c’est tout. Et ça marche tout seul ! Même pas besoin d’utiliser la commande `bin/console messenger:setup-transports`. Toute la configuration est créée directement au premier envoi de message sur le transport.

Cela donne aussi accès au RabbitMQ manager sur http://service.rabbitmq:15672/#/ qui permet de consulter l’activité de RabbitMQ.

Je rencontrerai probablement des problèmes un peu plus complexes que ça. J'avoue néanmoins être agréablement surpris par la facilité de mise en place du système.

Benchmark : pour 500 messages, à 4 process concurrents, 20 secondes ; soit 7.5 fois plus rapide que le transport Doctrine.

Et Supervisor dans tout ça ?

Supervisor est un outil permettant de s’assurer que certains process tournent en continue. Il les redémarre s’ils viennent à s'arrêter. En bref, Supervisor est très adapté pour les consumers (et même recommandé dans la documentation). Mais théoriquement, il est conçu pour la production.

Comme mentionné plus haut, j’ai besoin d’un grand volume de données. Cela nécessite donc de faire des fixtures à assez grande échelle (ce sera l’objet d’un autre article).

Pour cela, j’ai eu besoin de faire tourner 8 consumers en parallèle pendant une heure (je ne le savais pas encore au moment de la configuration, mais je savais que j’aurai besoin de plusieurs process en parallèle pendant un moment). On est donc bien dans le cas d’utilisation de Supervisor. Il m'a alors fallu le configurer dans Docker.

J'ai donc décidé de le rajouter à mon image PHP en ajoutant ceci :

config/docker/images/php-fpm/Dockerfile

FROM php:7.3-fpm
ENV DEBIAN_FRONTEND noninteractive
...
RUN apt-get update && apt-get install -y supervisor
RUN chmod a+rwx /var/log/supervisor/ 
# ajouté car sinon l’image docker ne voulait plus se lancer 

docker-compose.yml

php:
   build: './config/docker/images/php-fpm/'
   volumes:
   ...
       - './config/docker/supervisord.conf:/etc/supervisor/supervisord.conf'
working_dir: /var/www/site
 

config/docker/supervisord.conf

[program:messenger-consume
command=php /var/www/site/bin/console messenger:consume async --memory-limit=128M -vv
user=novaway
numprocs=4
autostart=true
autorestart=true
process_name=%(program_name)s_%(process_num)02d
 
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock
[supervisord]
 
[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file
chmod=0700                       ; sockef file mode (default 0700)
 
; The [include] section can just contain the "files" setting.  This
; setting can list multiple files (separated by whitespace or
; newlines).  It can also contain wildcards.  The filenames are
; interpreted as relative to this file.  Included files *cannot*
; include files themselves.
 
[include]
files = /etc/supervisor/conf.d/*.conf

C’est sur cette configuration que j’ai rencontré le plus de problèmes :

  • [supervisord] : même vide, cette clef semble indispensable. Si je la supprime l’image ne se lance plus.
  • [supervisorctl] et [unix_http_server] : les valeurs par défaut de la socket `/var/run/supervisor.sock` et `unix:///var/run/supervisor.sock` ne fonctionnaient pas et provoquaient différentes erreurs comme :
    • error : <class 'socket.error'>, [Errno 2] No such file or directory: file: /usr/lib/python2.7/socket.py line: 228
    • error : cannot open an HTTP server: socket.error reported errno.EACCES (13)
    • unix:///var/run/supervisor.sock no such file

=> remplacer par la valeur /tmp semble aider.

Il suffit ensuite de lancer la commande `docker-compose exec php supervisord` pour que les consumers se mettent en route automatiquement (grace au autostart=true et autorestart=true).

Conclusion

J’espère que ces indications de configuration vous auront servies. Dans un prochain article, je vous expliquerai comment j’ai tiré parti de Messenger dans le but de créer des fixtures de plus d’un million d’objets, tout en conservant l’insertion de Doctrine.