Préparer notre produit à évoluer avec les files d'attente Laravel

Une traduction de l'article a été préparée spécialement pour les étudiants du cours «Framework Laravel» .




Bonjour, je m'appelle Valerio, un ingénieur logiciel italien.

Ce guide est destiné à tous les développeurs PHP qui ont déjà des applications en ligne avec de vrais utilisateurs, mais qui n'ont pas une compréhension plus approfondie de la façon d'implémenter (ou d'améliorer considérablement) l'évolutivité de leur système à l'aide des files d'attente Laravel. J'ai découvert Laravel pour la première fois fin 2013 au début de la 5e version du framework. Ensuite, je n'étais pas encore un développeur impliqué dans des projets sérieux, et l'un des aspects des frameworks modernes, en particulier à Laravel, qui me semblait le plus incompréhensible, était Queues.

En lisant la documentation, j'ai deviné le potentiel, mais sans réelle expérience de développement, tout cela n'est resté qu'au niveau des théories dans ma tête.
Aujourd'hui, je suis le créateur Inspector.dev- tableau de bord en temps réel , qui effectue des milliers de tâches toutes les heures, et je comprends cette architecture bien mieux qu'auparavant.



Dans cet article, je vais vous expliquer comment j'ai découvert les files d'attente et les tâches et quelles configurations m'ont aidé à traiter de grandes quantités de données en temps réel, tout en économisant les ressources du serveur.

introduction


Lorsqu'une application PHP reçoit une requête http entrante, notre code est exécuté séquentiellement étape par étape jusqu'à ce que la requête soit terminée et qu'une réponse soit renvoyée au client (par exemple, le navigateur de l'utilisateur). Ce comportement synchrone est vraiment intuitif, prévisible et facile à comprendre. J'envoie une requête http au point de terminaison, l'application extrait les données de la base de données, les convertit au format approprié, effectue des tâches supplémentaires et les renvoie. Tout se passe linéairement.

Les tâches et les files d'attente introduisent un comportement asynchrone qui viole ce flux de ligne. C'est pourquoi ces fonctionnalités m'ont paru un peu étranges au début.
Mais parfois, une demande http entrante peut déclencher un cycle de tâches chronophages, par exemple, l'envoi de notifications par e-mail à tous les membres de l'équipe. Cela peut signifier l'envoi de six ou dix e-mails, ce qui peut prendre quatre ou cinq secondes. Par conséquent, chaque fois que l'utilisateur clique sur le bouton correspondant, il doit attendre cinq secondes avant de pouvoir continuer à utiliser l'application.

Plus l'application grandit, plus ce problème s'aggrave.

Qu'est-ce qu'une tâche?


Un travail est une classe qui implémente la méthode handle qui contient la logique que nous voulons exécuter de manière asynchrone.

<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Bus\Queueable;


class CallExternalAPI implements ShouldQueue
{
    use Dispatchable,
        InteractsWithQueue,
        Queueable;
        
    /**
     * @var string
     */
    protected $url;

    /**
     *    .
     *
     * @param array $Data
     */
    public function __construct($url)
    {
        $this->url = $url;
    }
    

    /**
     *  ,   .
     *
     * @return void
     * @throws \Throwable
     */
    public function handle()
    {
        file_get_contents($this->url);
    }
}

Comme mentionné ci-dessus, la principale raison de conclure un morceau de code dans Job est de terminer une tâche longue, sans forcer l'utilisateur à attendre sa fin.

Qu'entendons-nous par «tâches laborieuses»?


C'est une question naturelle. L'envoi d'e-mails est l'exemple le plus couramment utilisé dans les articles qui parlent de files d'attente, mais je veux vous parler de la véritable expérience de ce que je devais faire.

En tant que propriétaire de produit, il est très important pour moi de synchroniser les informations de voyage des utilisateurs avec nos outils de marketing et d'assistance client. Ainsi, en fonction des actions des utilisateurs, nous mettons à jour les informations des utilisateurs dans divers logiciels externes via une API (ou des appels http externes) à des fins de service et de marketing.
L'un des points de terminaison les plus couramment utilisés dans mon application peut envoyer 10 e-mails et effectuer 3 appels http vers des services externes avant la fin. Aucun utilisateur n'attendra autant de temps - très probablement, ils cessent tous d'utiliser mon application.

Grâce aux files d'attente, je peux encapsuler toutes ces tâches dans les classes allouées, transférer les informations nécessaires à l'exécution de leur travail au constructeur et planifier leur achèvement à une date ultérieure en arrière-plan afin que mon contrôleur puisse immédiatement renvoyer la réponse.

<?php

class ProjectController 
{
    public function store(Request $request)
    {
        $project = Project::create($request->all());
        
        //  NotifyMembers, TagUserActive, NotifyToProveSource 
        //   ,     
        Notification::queue(new NotifyMembers($project->owners));
        $this->dispatch(new TagUserAsActive($project->owners));
        $this->dispatch(new NotifyToProveSource($project->owners));
        
        return $project;
    }
}

Je n'ai pas besoin d'attendre la fin de tous ces processus avant de retourner la réponse; au contraire, l'attente sera égale au temps nécessaire pour les publier dans la file d'attente. Et cela signifie la différence entre 10 secondes et 10 millisecondes !!!

Qui effectue ces tâches après les avoir envoyées dans la file d'attente?


Il s'agit de l'architecture classique éditeur / consommateur . Nous avons déjà publié nos tâches dans la file d'attente à partir du contrôleur, nous allons maintenant comprendre comment la file d'attente est utilisée et, enfin, les tâches sont exécutées.



Pour utiliser la file d'attente, nous devons exécuter l'une des commandes artisanales les plus populaires:

php artisan queue:work

Comme le dit la documentation:
Laravel inclut un travailleur de file d'attente qui traite les nouvelles tâches lorsqu'elles sont en file d'attente.

Génial! Laravel fournit une interface prête à l'emploi pour les tâches de mise en file d'attente et une commande prête à l'emploi pour récupérer les tâches de la file d'attente et exécuter leur code en arrière-plan.

Rôle de superviseur


C'était une autre «chose étrange» au début. Je pense qu'il est normal de découvrir de nouvelles choses. Après cette étape de formation, j'écris ces articles pour m'aider à organiser mes compétences, et en même temps aider d'autres développeurs à étendre leurs connaissances.

Si la tâche échoue lors du lancement d'une exception, l'équipe queue:workarrêtera son travail.

Pour qu'un processus queue:works'exécute en continu (consommant vos files d'attente), vous devez utiliser un moniteur de processus tel que Supervisor pour vous assurer que la commande queue:workne s'arrête pas de fonctionner même si la tâche lève une exception. Supervisor redémarre la commande après qu'elle se bloque, en commençant à nouveau de la tâche suivante, en mettant de côté l'exception de lancement.

Les tâches s'exécuteront en arrière-plan sur votre serveur, ne dépendant plus de la requête HTTP. Cela introduit quelques modifications que j'ai dû prendre en compte lors de l'implémentation du code de tâche.

Voici les plus importants auxquels je ferai attention:

Comment savoir que le code de tâche ne fonctionne pas?


Lors de l'exécution en arrière-plan, vous ne pouvez pas immédiatement remarquer que votre tâche provoque des erreurs.
Vous n'aurez plus de commentaires immédiats, comme lors d'une demande http depuis votre navigateur. Si la tâche échoue, elle le fera en silence et personne ne le remarquera.

Envisagez d'intégrer un outil de surveillance en temps réel tel que l' inspecteur pour détecter chaque défaut.

Vous n'avez pas de demande http


Il n'y a plus de requête HTTP. Votre code sera exécuté à partir de cli.

Si vous avez besoin de paramètres de requête pour terminer vos tâches, vous devez les transmettre au constructeur de tâches pour une utilisation ultérieure au moment de l'exécution:

<?php

//   

class TagUserJob implements ShouldQueue
{
    public $data;
    
    public function __construct(array $data)
    {
        $this->data = $data;
    }
}

//       

$this->dispatch(new TagUserJob($request->all()));

Vous ne savez pas qui s'est connecté


Il n'y a plus de session . De la même manière, vous ne connaîtrez pas l'identité de l'utilisateur connecté, donc si vous avez besoin d'informations sur l'utilisateur pour terminer la tâche, vous devez passer l'objet utilisateur au constructeur de la tâche:

<?php

//   
class TagUserJob implements ShouldQueue
{
    public $user;
    
    public function __construct(User $user)
    {
        $this->user= $user;
    }
}

//       
$this->dispatch(new TagUserJob($request->user()));

Comprendre comment évoluer


Malheureusement, dans de nombreux cas, cela ne suffit pas. Utiliser une seule ligne et un consommateur peut vite devenir inutile.

Les files d'attente sont des tampons FIFO («premier entré, premier sorti»). Si vous avez planifié beaucoup de tâches, peut-être même de types différents, ils doivent attendre que d'autres terminent leurs tâches avant de terminer.

Il existe deux façons de faire évoluer:

plusieurs consommateurs par file d'attente.



Ainsi, cinq tâches seront supprimées de la file d'attente à la fois, ce qui accélérera le traitement de la file d'attente.

Files d'attente à usage spécial

Vous pouvez également créer des files d'attente spécifiques pour chaque type de tâche à lancer avec un consommateur dédié pour chaque file d'attente.



Ainsi, chaque file d'attente sera traitée indépendamment, sans attendre la fin des autres types de tâches.

Horizon


Laravel Horizon est un gestionnaire de files d'attente qui vous donne un contrôle total sur le nombre de files d'attente que vous souhaitez configurer et la capacité d'organiser les consommateurs, permettant aux développeurs de combiner ces deux stratégies et de mettre en œuvre celle qui convient à vos besoins d'évolutivité.

Le lancement se fait à travers l' horizon d'artisan php au lieu de la file d'attente d'artisan php: travail . Cette commande analyse votre fichier de configuration horizon.phpet démarre une série de travailleurs de file d'attente en fonction de la configuration:

<?php

'production' => [
    'supervisor-1' => [
        'connection' => "redis",
        'queue' => ['adveritisement', 'logs', 'phones'],
        'processes' => 9,
        'tries' => 3,
        'balance' => 'simple', //   simple, auto  null

    ]
]

Dans l'exemple ci-dessus, Horizon démarre trois files d'attente avec trois processus affectés pour traiter chaque file d'attente. Comme mentionné dans la documentation Laravel, l'approche basée sur le code d'Horizon permet à ma configuration de rester dans un système de contrôle de version où mon équipe peut collaborer. C'est la solution parfaite en utilisant un outil CI.

Pour découvrir la signification des paramètres de configuration en détail, lisez ce merveilleux article .

Ma propre configuration



<?php

'production' => [
    'supervisor-1' => [
        'connection' => 'redis',
        'queue' => ['default', 'ingest', 'notifications'],
        'balance' => 'auto',
        'processes' => 15,
        'tries' => 3,
    ],
]

Inspector utilise principalement trois files d'attente:

  • ingérer des processus d'analyse de données provenant d'applications externes;
  • les notifications sont utilisées pour planifier immédiatement les notifications si une erreur est détectée lors de la réception des données;
  • la valeur par défaut est utilisée pour d'autres tâches que je ne veux pas mélanger avec les processus d' acquisition et de notification .

Grâce à l'utilisation d' balance=autoHorizon, il comprend que le nombre maximum de processus activés est de 15, qui seront distribués dynamiquement en fonction du chargement des files d'attente.

Si les files d'attente sont vides, Horizon prend en charge un processus actif pour chaque file d'attente, ce qui permet au consommateur de traiter immédiatement la file d'attente si une tâche est planifiée.

Observations finales


L'exécution simultanée en arrière-plan peut provoquer de nombreuses autres erreurs imprévisibles, telles que MySQL "Lock timed out" et de nombreux autres problèmes de conception. En savoir plus ici .

J'espère que cet article vous a aidé à utiliser les files d'attente et les tâches avec plus de confiance. Si vous voulez en savoir plus sur nous, visitez notre site Web à www.inspector.dev

Précédemment publié ici www.inspector.dev/what-worked-for-me-using-laravel-queues-from-the-basics-to -horizon



Suivez le cours et bénéficiez d'une remise.



All Articles