Nous présentons aujourd'hui un petit retour d'expérience sur le composent Symfony Messenger. L'objectif derrière ce billet est de comprendre comment:
- Mettre en place une architecture de conteneur Docker qui répond à notre besoin.
- Fonctionnement de l'ordonnancement avec RabbitMq.
- Fonctionnement de Symfony Messenger.
- Le couplage de Symfony Messenger et RabbitMQ, SQS, Redis...afin d'envoyer des emails d'une façon asynchrone.
De nombreux de traitement web pourrait être assez lourd qui prennent parfois beaucoup de temps soit à cause d'un trafic intense,soit à cause d'une mauvaise architecture logicielle, soit à cause de la mauvaise gestion de mémoire, y a des gens qui préfèrent augmenter la mémoire ou bien mettre un système de balance loader
...Mais ca résout pas le problème principal.
Bref, pour résoudre ce problème, on fait retour souvent à un mécanisme de file d'attente (ex: Standard Queue, FIFO Queue) pour effectuer des tâches chronophages et d’une manière ordonnancée et plus performante.
RabbitMQ est un système permettant de gérer des files de messages afin de permettre à différents clients de communiquer très simplement et plus efficacement. Pour que chaque client puisse communiquer avec Rabbit Mq, celui-ci s’appuie sur le protocole AMQP. Ce protocole définit précisément la façon d’ordonnancement asynchrone des messages. C’est un gestionnaire de queues, permettant d’à synchroniser différents traitements. C’est pour assurer la haute disponibilité du serveur, dédié aux d exécutions de traitements lourds comme l’envoi des e-mails, l’exportation, l'importation des fichiers et d'autres traitements durable.
Le composant messenger permet à l'application d'envoyer et de recevoir des messages vers / depuis d'autres applications ou via un système de queue basée sur un message bus. Le bus a pour rôle de dispatcher le messages et d’exécuter l'handler approprié au message . Durant ce processus le message va passer dans une pile ordonnée de notre middleware.
Le composant nous fournit aussi des fonctionnalités de routage pour intercepter les messages et les router vers le bon Transporter déjà défini au niveau de notre configuration au-dessous deconfig.yml
.
Message : Objet PHP qui doit être sérialisable qui reprèsente un DTO (Data transfer object).
Transporter: Par défaut, les messages sont traités d'une manière synchrone dès leur distribution.
Au cas où nous préferons traiter les messages de manière asynchrone, vous devez configurer un transport. Ces transports ne communiquent avec notre application via des systèmes de file d'attente ce qu'on appelle brocker(RabbitMQ, Kafka, AWS, SQS, memcached et meme un base de données ou bien un système de fichier etc...)
Message Handler : C’est ici qu’il pourra y avoir et exécuter le logique métier applicable aux messages. La classe NotificationHandler
qui recevra les messages va représenter notre Handler.
Sender (Expéditeur) : Responsable de la sérialisation et de l'envoi de messages. Cela peut être un courtier de messages ou une API par exemple.
Receiver (Récepteur) : Responsable de la désérialisation et de la transmission des messages aux gestionnaires. Cela peut être un extracteur de file d'attente de messages ou un point de terminaison d'API, par exemple.
Message Bus : C'est le composant qui a pour role de dispatcher notre message, définir et exécuter le handler coresspondant associer à notre message.
D'ailleurs le message Bus représente une approche de développementbasé sur une architecture d'event sourcing ce qu'on appelle CQRS. L'une des architectures les plus recommandées qui résoudre énormément de problèmes de performance et de complexité algorithmique.
Worker: Consomme les messages depuis les transports.
Pourquoi utiliser un transport?
Tronspoter : Il permet de faire transiter les messages via différents brocker (ex: RabbitMQ, Kafka, Amazon SQS, etc.)
À la place d'envoyer notre message d'une manière synchrone directement vers notre handler, on dirige le message vers un transporté
pour le fournir d'une façon asynchrone à notre message Handler. C'est cool !
Le composant bénéficie d’un panel sur la toolbar de debug, mais aussi d’une commande afin de pouvoir consommer les messages à travers la commande suivante: bin/console messenger:consume-messages 'nom de Tronspoter(amqp, default)'
Ok on y va !! … Nous allons présenter maintenant comment utiliser le compossant Messenger
de Symfony pour pouvoir mettre en place un système d'envoi de mail d'un facon asynchrone.
Imganions si nous devons enovyer à 60000 utilisateurs un mail de notification, évidament la page correspendante à notre route va continuer à charger tantque le boucle for ($i = 1; $i <= 6000; $i++)
n'a pas terminé son parcours, et du coup ca va consommer énormement de mémoire et de plus ce n'est pas pratique ni supportable en terme de User Experience
.
Pour se faire, nous avons deux choix :
La 1ere Méthode: installer notre environnement directement sur notre machine local
1. Installer rabbitmq et lancer la commande rabbitmq-server
2. rabbitmqctl status
3. rabbitmq-plugins enable rabbitmq_management
4. Activer l'extension amqp. : Il faudrait lancer 'apt install php-amqp' et ajouter 'extension = amqp.so' sous le fichier php.ini
5.composer create-project symfony/skeleton my-project
# c'est mieux de travailler avec le projet symfony/skeleton
plus légère avec le minimum de dépenses possibles et installer à fur et aux mesures les composants nécessaires.
- La mise en place des dépendences necessaires:
composer req mailer
composer messenger
composer req annotation
composer req serializer
composer req twig
composer req web-server --dev
La 2eme Méthode: utiliser un système de conteneurs
On prépare notre docker-compose.yml
pour regroupper les différents configurations:
Voici mon docker-compose.yml:
version: '2'
services:
php:
build: docker/php
tty: true
restart: on-failure
volumes:
- '.:/symfony'
command: service supervisor start
nginx:
build: docker/nginx
tty: true
restart: on-failure
volumes:
- './public:/symfony'
links:
- php
ports:
- '80:80'
rabbitmq:
image: rabbitmq:3.4-management
tty: true
ports:
- "15672:15672"
maildev:
image: djfarrelly/maildev
tty: true
ports:
- "1080:80"
redis:
image: redis
tty: true
ports:
- "6379:6379"
La création de notre Message : SendNotification
namespace App\Message;
class SendNotification
{
private $message;
private $users;
public function __construct(string $message, array $users = [])
{
$this->message = $message;
$this->users = $users;
}
...
Voici notre controlleur:
Nous allons utlisé notre command Bus
pour dispatcher notre command, autrement dit notre message de type SendNotification
pour que l'handler puisse sera notifié de cette action et répondre à travers le traitement nécessaire.
namespace App\Controller;
class HomeController extends AbstractController
{
/**
* @Route("/", name="home")
*/
public function index(MessageBusInterface $bus)
{
$bus->dispatch(new SendNotification('notification'));
return $this->render('home/index.html.twig');
}
...
La création de notre Handler: (par convention faudrait que le nom de la callse soit 'nom de la classe de notre message'+ Handler.
Pour traiter un message il faudra créer un handler avec une méthode __invoke
namespace App\MessageHandler;
class SendNotificationHandler
{
/**
* @var Swift_Mailer
*/
private $mailer;
public function __construct(Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
public function __invoke(SendNotification $notification)
{
for ($i =1; $i<5000000; $i++) {
echo 'The mail has been sent with success !';
$message = (new \Swift_Message('Mail object'))
->setFrom('[email protected]')
->setTo('test@gmailcom')
->setBody(
$notification->getMessage()
)
;
$this->mailer->send($message);
}
}
}
...
Ensuite, il faudrait enregistrer notre Handler
en tant que service avec le tag messenger.message_handler
:
# config/services.yaml
services:
App\MessageHandler\:
resource: '../src/MessageHandler'
tags: [messenger.message_handler]
On pourrait aussi définir notre service avec la syntaxe complète afin de spécifier la méthode à appeler et le modèle supporté.
# config/services.yaml
services:
App\MessageHandler\NotificationHandler:
tags:
- {name: 'messenger.message_handler', method: 'process', handles: 'App\Message\SendNotification'} # telque 'process' reprèsente est une méthode deja définit au niveau de la classe SendNotificationHandler
Transporteur/sender RabbitMQ:
Au lieu d’appeler directement notre Handler
, nous voulons bien acheminer les messages vers un ou plusieurs expéditeurs/transports:
framework:
messenger:
transports:
# Uncomment the following line to enable a transport named "amqp"
amqp: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
# Route your messages to the transports
'App\Message\SendNotification': amqp
.nev
: Ici c'est l'endroit ou définit notre variable denvironnement MESSENGER_TRANSPORT_DSN
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
Une fois que les messages ont été acheminés, nous pouvons les consommer avec la commande
$ bin/console messenger:consume-messages messenger.default_receiver
et s'intervient Supervisor
pour exécuter la commande d'une façon automatique, faudrait prendre en considération que le lancement de service supervisor arrête le service php-fpm
, c'est pour cela que nous devons au niveau de la configuration le relancement de php-fpm en cas d'arrêt.
[unix_http_server]
file=/tmp/supervisor.sock
[supervisord]
logfile=/tmp/supervisord.log
pidfile=/var/run/supervisord.pid
nodaemon=true
[program:php-fpm]
command = /usr/local/sbin/php-fpm
autostart=true
autorestart=true
[program:amqp]
command=php /symfony/bin/console messenger:consume-messages amqp
startsecs = 0
stdout_logfile=/tmp/supervisord-amqp.log
stdout_logfile_maxbytes=10MB
On pourait aussi lancer la commande bin\console debug:messenger
c'est une commande CLI pour exposer les classes de message pouvant être dispatché à travers notre Messenger.
#config.yml
swiftmailer:
url: '%env(MAILER_URL)%'
# spool: ~
#.env
MAILER_URL=smtp://maildev:25
(Nous avons commenter le mail spool parceque nous utilisons déja un transporteur amqp pour enchainer l'envoi d'une facon asynchrone donc ya pas besoin de traiter le processus avec un système de spooling.)
Tout d'abord commencons par la création d'un service SQS à partir de SQS Managment Console et récupérer le nom la région et les clés d'authentification qui se trouvent sous l'interface IAM.
Après il suffit d'installer les dépedences necessaires:
composer require messenger enqueue/messenger-adapter enqueue/sqs
pour pouvoir utiliser d'autres type de transports nous avons fait recours à ce package https://github.com/adtechpotok/enqueue-messenger-adapter qui va nous fournir différents adapteurs de plus.
Voici les différentes configurations nécessaires:
.env
ENQUEUE_DSN=sqs:?secret=[AWSSecretKey]&key=[AWSAccessKey]region=[region]
messenger.yml
framework:
messenger:
transports:
# Uncomment the following line to enable a transport named "amqp"
sqs: enqueue://default?&topic[name]=test&queue[name]=test
routing:
# Route your messages to the transports
'App\Message\SendNotification': sqs
Afin de traiter les messages, il suffit de lancer la commande:
php bin/console messenger:consume sqs
Nous pouvons visualiser, lire le continue des messages et purger la queue à travers l'interface offerte par AWS SQS:
Pour les gens qui prèfère memcached
, on pourait suivre le meme démarche.
$ composer require enqueue/redis predis/predis:^1
#.env
ENQUEUE_DSN=redis://redis:6379
On pourrait connecter à notre CLI redit à travers une simple docker compose exec redit redis-cli
Après nous allons remarquer la création automatique d'une CLE de type list messages
.
127.0.0.1:6379> KEYS *
1) "messages"
127.0.0.1:6379> lrange messages 0 -1
pour récupérer tout les messages saugardés.
Et finallement il suffit de lancer la commande: php bin/console messenger:consume redis pour pouvoir connsomer les différents messages.
Je vous mettre le lien suivant pour le repo github de notre exemple: https://github.com/ahmed-bhs/messenger-demo