Depuis plusieurs années, Symfony fournit tout ce qu'il faut pour créer des workers PHP asynchrones, notamment grâce à son composant Messenger. Nous verrons comment configurer ces workers pour les faire tourner en production avec systemd, ce "gestionnaire de système et de services" qui s'est imposé sur la plupart des distributions GNU/Linux. systemd permet, entre autres, de déclarer des services à exécuter et maintenir en vie. Disponible nativement sur bon nombre d'OS populaires, il sera donc un allié parfait pour simplifier la mise en place de nos workers.
_ Merci à tous d'être là. On va faire vite pour la dernière conférence de la matinée. Tout le monde a faim. On va parler de worker PHP avec SystemD et Symfony. Je travaille au quotidien avec ces écosystèmes. Je fais un peu de front aussi. Sur Twitter et Github, vous pourrez me trouver.
J'ai quelques passions comme prendre des photos avec mon drone et faire des reprises en vélo l'été pendant plusieurs semaines. Ou encore, ce qui touche au bricolage à l'électronique. Par exemple, un système pour appeler l'ascenseur quand on n'a pas son badge ou passer le bonjour à son patron. C'était la partie marrante de la conférence. Mais aujourd'hui, on va parler de worker PHP. On va se demander ce que c'est, et globalement, c'est un processus PHP qui va tomber en dehors de votre logique. L'idée est d'avoir du code qui tombe en asynchrone.
Le but de ce que l'on cherche à faire, c'est retarder les traitements et qui ne sont pas nécessaires pour répondre à vos clients. Vous avez par exemple le besoin d'envoyer un mail de création de comptes pour un utilisateur qui s'est inscrit. Vous n'avez pas besoin de lui envoyer un mail tout de suite avant que sa demande ait été prise en compte. Dans les cas précis, on va voir les envois de mails. Si vous avez de la logique, des calculs un peu lourds à faire ou si vous avez de la logique qui requiert de faire plein de requêtes à votre base, c'est intéressant à faire dans un worker. On a souvent des documents ElasticSearch à réindexer quand les données changent en base. On va avoir une requête un peu autonome. S'il y a un souci pour l'exécution du worker, on va avoir un système qui retravaille automatiquement.
Si vous interagissez avec une API tierce et que cette API n'est plus fonctionnelle, avoir un système automatique, cela permettra de réessayer plusieurs fois jusqu'à ce que l'API répondre. Pour cette conférence, je me suis inspiré d'une application que j'ai développée pour moi et que l'on utilise quasiment au quotidien à Jolicode. C'est une application de monitoring comme il en existe pléthore sur Internet. Ça m'a amusé d'en recréer une et devoir comment ça marchait et ce que l'on pouvait voir ce que l'on pouvait mettre dedans. L'idée était d'avoir des notifications Slack en temps réel.
Aujourd'hui, le fonctionnement est ultra simplifié. Il y a un compte qui exécute une commande Symfony. Elle va demander le check d'une URL de manière asynchrone. On va pour cela utiliser Messenger, en composant Symfony qui est sorti en 2018. Il est sorti avec la version 4.2. De Symfony. Globalement, ce composant va envoyer les messages qui vont représenter une logique métier. De l'autre côté, il y aura un handler qui s'occupera d'exécuter le code qui va bien pour faire le traitement du messager la logique de correspondance.
Messenger, c'est simple. C'est une classe PHP. Dans notre cas, on va faire des checks URL. Le message aura une propriété URL. Une URL monitorée. Une fois que l'on a ça, il faut faire le handler en Symfony classique. On va juste mettre un attribut. Les nouvelles annotations natives de PHP 8. On ajoute ça et on ajoute une méthode avec un paramètre qui sera personnalisé avec le type de notre message. Quand il y a un message de type check URL envoyé, c'est automatiquement ce handler qui sera appelé. Quand on regarde le code, c'est simpliste. Il faudra juste faire une requête http à notre URL pour logger le status code de la réponse. Ensuite, le message, on le passe. Ensuite, on va le dispatcher.
Si on exécute cette commande, on voit que le handler a été appelé. Le log apparaît. Ça marche, mais ce n'est pas ce que l'on veut. On va voir d'un côté de la logique qui empile des messages dans une file d'attente, et de l'autre côté, on veut que le worker défile les messages au fur et à mesure. L'idée est d'avoir un traitement asynchrone.
Il est possible d'utiliser plein de choses comme AMQP. Pour avoir le comportement que l'on veut, ce que fait le Transport Doctrine va créer une table dans votre base de données. C'est là qu'il va empiler les messages. Un message sera une ligne de la table. Nous, on va configurer le transport pour configurer Doctrine. Le default veut dire que l'on prend la connexion Doctrine par défaut de l'application. Ensuite, il reste deux choses à faire. La première, créer un transport. C'est fait avec la conflagration Messenger transport. On va lui passer la barre d'environnement que l'on a créé un instant.
Ensuite, on va faire comprendre par le routing à symphonique quand il y a un message de ce type, il faut l'envoyer dans ce transport. On veut qu'il soit envoyé dans le transport async pour le cas d'un URL.
La commande va attendre que les messages soient envoyés dans Transport. Dès qu'il y aura un nouveau message, elle va le prendre, le traiter et elle va appeler le handler correspondant et elle va se remettre en attente de nouveaux messages. Ça, on ne va pas s'y arrêter. Qu'est-ce qu'on passe si on ne lance que ça en prod et qu'une erreur intervient dans un worker ? Si le worker crashe, comment on fait pour le relancer ?
Pour ça, on va utiliser SystemD. Si on regarde le site de SystemD, c'est un ensemble de composants pour Linux. En fait, globalement, c'est un gestionnaire de services. C'est le premier processus qui sera lancé quand on démarre la machine. SystemD démarrera le reste de l'OS. Pourquoi on utilise SystemD ? Il y a plusieurs alternatives. Il y a des gens qui ont peut-être déjà utilisé Supervisor.
L'intérêt de SystemD, c'est que c'est un standard dans la plupart les distributions Linux. Vous avez déjà SystemD sur votre machine si vous utilisez Redhat, par exemple. Il y a beaucoup de critiques auprès de SystemD. Ça fait trop de choses, par écrit la logique est déjà dans Linux, il y a plein de gens qui reprochent la toxicité de certaines choses de SystemD. Ce n'est pas tout rose. Il n'empêche que c'est actuellement probablement installé déjà sur votre machine, autant l'utiliser.
Un des avantages de SystemD aussi, c'est qu'on peut l'utiliser sans être administrateur sur la machine. Il y a un système de user unit qui permet de déclarer ses propres services sans avoir à être hôte. Ce n'est pas ce que l'on va faire dans la suite, mais c'est bon de savoir que c'est possible.
Notre but, c'est que SystemD gère un autre worker. Pour ça, dans SystemD, il y a une notion d'unit, c'est un objet SystemD qui contrôle un matériel, qui démarre des éléments. SystemD gère plein de types d'units différents, mais nous, on va faire une unit de services, c'est des plus classiques.
Un autre exemple intéressant, c'est qu'il y a une notion de dépendance en SystemD. Si vous avez besoin que d'autres units, soit démarrées avant, c'est possible de le faire dans la configuration. Il y a plusieurs emplacements standards pour placer cette configuration. On ne pourra pas tout détailler. On va la mettre dans ce chemin. Cette configuration ressemble à ça. Il y a plusieurs choses. La première chose, c'est de taper le nom de fichier de configuration. Ici en rouge. Ce nom est important. Ça va être le nom de l'unit et c'est par ce nom que l'on va référencer le service dans les commandes que l'on va référencer après. Ça permet de dire à SystemD que c'est une unit de type service. On a d'autres parties intéressantes.
Il y a la configuration ExecStart, la commande qu'il faut lancer pour démarrer le worker. C'est la commande que l'on a lancée tout à l'heure avec Messenger.
On a aussi une partie importante, la partie qu'il y a SystemD ce qu'il doit faire quand votre service est stoppé ou qu'il crashe.
On va lui dire de toujours démarrer le service avec cette commande dès lors qu'il y a un crash ou dès lors qu'il stoppe. On va lui dire d'attendre une seconde entre chaque redémarrage. On ne va pas surcharger le système. Et on va aussi configurer un timeout important. Il est configuré à deux moments. D'abord, au démarrage. Quand on demande à SystemD de démarrer le service, si au bout du timeout, le service n'a toujours pas démarré, SystemD doit considérer que notre service a un souci. Il va donc le redémarrer. Et deuxièmement où le timeout est important, c'est quand on demande à stopper un service. Quand on demande ça, SystemD informe le service qu'on veut le stopper, il va envoyer un signal d'interruption. Cela permettra aux workers du service de terminer son traitement en cours, de le terminer proprement et de s'arrêter. Si au bout de ce timeout le worker, le service n'a toujours pas terminé, SystemD va tout simplement killer le processus. Là, on met une valeur importante. Si jamais vous avez un cas où vous avez un message qui prend un peu de temps à être processé, en laissant 300 secondes, en théorie, vous avez largement le temps de terminer le traitement. On a une autre partie importante, c'est la configuration StartLimit. Si notre service démarre plus de cinq fois en moins de 20 secondes, alors, SystemD doit considérer qu'il y a un souci. Le service n'arrive pas à démarrer. Du coup, passée cette limite, ce n'est pas la peine de continuer indéfiniment.
Pareil, c'est toujours dans l'optique de ne pas spammer alors qu'il y a clairement un souci. Il ne va pas se réparer tout seul. Une fois qu'on a notre unit configurée, il ne reste plus grand-chose pour que le worker tournant prod. La première chose, démarrer le service. tout simplement. Il y a le nom de notre unité. C'est ce que l'on disait tout à l'heure. Une fois que l'on fait ça, on a le worker qui tourne en prod et qui sera démarré tout seul s'il crashe.
Il faut aussi penser à activer le service. Par défaut, si la machine redémarre, tant que le service n'est pas activé, il ne sera pas démarré automatiquement au prochain reboot de la machine. Ça se fait avec la commande systemctl enable est le nom du service. Une fois que l'on fait ça, ça démarrera correctement.
Si jamais vous faites des modifications dans votre unit, il faut bien informer SystemD qu'il y a des modifications qui ont été faites dans l'unit.
Et ensuite, on voit bien avec cette commande l'état actuel du service. On peut voir depuis combien de temps il tourne. On peut voir la quantité de mémoire qui est consommée, on peut voir plusieurs choses. Notamment, tout en bas, on voit les derniers logs qui ont été produits par votre worker. Si on regarde plus en détail, on voit que ce sont des logs de Messenger que l'on avait tout à l'heure. A priori, tout se passe bien. On a vraiment un worker qui peut tourner en prod de manière safe.
Quelques protips pour terminer, en général, on évite de laisser le worker tourner en continu en prod et indéfiniment. Si jamais vous avez une fuite mémoire, globalement, ça peut faire exploser la mémoire. Ce n'est pas ce que l'on désire. Pour ça, Symfony Messenger fournit plusieurs options que l'on pourra passer à la commande Messenger consume.
La première, on va dire qu'une fois que 10 messages ont été traités, en stop. On peut aussi créer une limite de mémoire. Par exemple, si le Workers consomme plus que la limite, il va automatiquement finir par se stopper. On a aussi une configuration de limite de temps. On configure la durée maximale au bout de laquelle le worker se stoppe. Quand le worker sera stoppé, il va se redémarrer de zéro. Un autre point, il ne faut pas oublier de couper les workers. Sinon, ils vont continuer de tourner avec l'ancienne version du code. Vous avez deux n'a rien de faire ça. Il y a une commande qui est fournie par Messenger directement. Ça va demander à tous les workers de terminer le traitement en cours. Une fois que c'est bon, ils vont s'arrêter.
Il y a un inconvénient à cette méthode, c'est que vu qu'on a SystemD derrière, le worker va être redémarré automatiquement. Quand vous faites le déploiement, en général, il faut préférer couper les workers pour les laisser inactifs et faire toute la logique de déploiement. Mettre à jour les codes, jouer les migrations, etc. Je vous conseille d'utiliser le stock de SystemD. Ça va arrêter proprement le service. Ils vont finir de traiter le message qui est en cours, et en plus, on a la logique du timeout qu'on a vu juste avant. Cela permet que s'il n'a pas terminé au bout de 300 secondes, SystemD va kill le worker. L'intérêt de cette commande est qu'elle est bloquante. Tant que le worker n'est pas coupé, SystemD ne va pas vous donner la main. Vous allez avoir un déploiement propre. On est sûr qu'on a d'abord coupé le worker et après, on peut faire la logique de déploiement.
Et après, évidemment, il faut bien penser à redémarrer le worker. Un truc important, quand vous avez un code qui tourne en prod, c'est les logs.
Avec SystemD, les logs sont gérés par journald. Pour consulter les codes de votre unit, il faut utiliser la commande journalctl.
Cette commande permettra d'afficher seulement les logs de notre unit, ça va afficher des infos en plus sur l'unit est ça va afficher les derniers logs que cela mettra à jour.
Un autre truc intéressant dans SystemD, sans entrer dans les détails, il y a un système de templates. Cela permettra de faire tourner plusieurs instances du même worker. Si vous avez une file d'attente avec plein de messages qui arrivent, ce pourrait être intéressant d'avoir plusieurs workers en parallèle plutôt qu'un seul.
Enfin, pour terminer, dans Symfony Messenger, il y a par défaut un système de retry authentique. Il va par défaut réessayer au bout de trois fois. Ça peut être configuré ou on peut configurer un délai entre chaque essai. Par contre, pour la partie ou le message est ignoré, ce n'est pas cool.
Dans Messenger, on peut configurer de manière spéciale avec la configuration failure transport. Le transport va empiler les messages qui sont en attente. Cela vous permettra de voir dans la file d'attente qu'il y a des messages qui n'ont pas été traités, et surtout, ça vous permettra de les rejouer et pour débugger, ce sera plus simple.
Il y a plusieurs commandes Symfony qui sont disponibles nativement pour travailler avec ça. On a fait le tour.
Je ne sais pas s'il y a des questions, mais en tout cas, merci pour votre attention.
_ On a le temps pour une ou deux questions rapides.
_ Bonjour. Merci pour cette conférence. J'aurais une question. Tout à l'heure, sur la fin, tu as brièvement parlé de SystemD avec docker*. Je voulais savoir si tu avais expérimenté des services et cela implique une question, pour lancer le service, ça nécessite que le service soit préalablement lancé. Est-il possible de prioriser ses services dans la configuration de SystemD ?
_ Je n'ai pas vraiment parlé de docker. Si c'est le cas, c'est une erreur. Je n'ai pas la réponse. À quel moment ? OK. Vu que l'on peut configurer les dépendances, on peut peut-être jouer là-dessus. Une priorité, en tant que telle, je ne sais pas. Il y a une question par là-bas.
_ Ce n'est pas vraiment une question, c'est plutôt une réponse à notre question. Quand tu lances ton consume, tu peux mettre l'ordre du plus important au moins important, ou l'inverse. Tu peux gérer des dépendances si tu envoies une commande qui contient un client et que tu as besoin que le client soit envoyé avant d'envoyer la commande. Tu vas envoyer le client avant. Tant que le client n'est pas envoyé, tu ne vas pas envoyer la commande. C'est dans l'ordre des queues que tu mets dans ta commande que tu peux gérer les priorités.
_ Une dernière question ?
_ Bonjour. Est-ce que Symfony propose un composant de supervision des workers ?
_ Non. Une interface pour voir les workers qui tournent et tout ça ?
_ Par exemple.
_ Je ne crois pas, je ne crois pas que ça affiche les workers qui tournent, ça peut être une feature intéressante à apporter.
_ Merci beaucoup.
_ Cool !
_ Ça va être la pause déjeuner, maintenant.
Tweets