Exécuter la tâche PHP de manière asynchrone

144

Je travaille sur une application web assez volumineuse, et le backend est principalement en PHP. Il y a plusieurs endroits dans le code où je dois effectuer une tâche, mais je ne veux pas que l'utilisateur attende le résultat. Par exemple, lors de la création d'un nouveau compte, je dois leur envoyer un e-mail de bienvenue. Mais quand ils cliquent sur le bouton «Terminer l'enregistrement», je ne veux pas les faire attendre jusqu'à ce que l'e-mail soit réellement envoyé, je veux juste démarrer le processus et renvoyer un message à l'utilisateur tout de suite.

Jusqu'à présent, dans certains endroits, j'ai utilisé ce qui ressemble à un hack avec exec (). En gros, faire des choses comme:

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");

Ce qui semble fonctionner, mais je me demande s'il existe un meilleur moyen. J'envisage d'écrire un système qui met en file d'attente les tâches dans une table MySQL, et un script PHP séparé de longue durée qui interroge cette table une fois par seconde et exécute toutes les nouvelles tâches qu'il trouve. Cela aurait également l'avantage de me permettre de répartir les tâches entre plusieurs machines de travail à l'avenir si j'en avais besoin.

Est-ce que je réinvente la roue? Existe-t-il une meilleure solution que le hack exec () ou la file d'attente MySQL?

Davr
la source

Réponses:

80

J'ai utilisé l'approche de mise en file d'attente, et cela fonctionne bien car vous pouvez différer ce traitement jusqu'à ce que la charge de votre serveur soit inactive, vous permettant de gérer votre charge assez efficacement si vous pouvez partitionner facilement les «tâches qui ne sont pas urgentes».

Rouler soi-même n'est pas trop difficile, voici quelques autres options à vérifier:

  • GearMan - cette réponse a été écrite en 2009, et depuis lors, GearMan semble une option populaire, voir les commentaires ci-dessous.
  • ActiveMQ si vous voulez une file d'attente de messages open source complète.
  • ZeroMQ - c'est une bibliothèque de sockets assez cool qui facilite l'écriture de code distribué sans avoir à trop se soucier de la programmation des sockets elle-même. Vous pouvez l'utiliser pour la mise en file d'attente des messages sur un seul hôte - votre application Web enverrait simplement quelque chose dans une file d'attente qu'une application console exécutée en continu consommerait à la prochaine opportunité appropriée.
  • beanstalkd - n'a trouvé celle-ci qu'en écrivant cette réponse, mais semble intéressante
  • dropr est un projet de file d'attente de messages basé sur PHP, mais n'a pas été activement maintenu depuis septembre 2010
  • php-enqueue est un wrapper récemment (2017) maintenu autour d'une variété de systèmes de files d'attente
  • Enfin, un article de blog sur l'utilisation de Memcached pour la mise en file d'attente des messages

Une autre approche, peut-être plus simple, consiste à utiliser ignore_user_abort - une fois que vous avez envoyé la page à l'utilisateur, vous pouvez effectuer votre traitement final sans craindre une interruption prématurée, bien que cela ait pour effet de prolonger le chargement de la page de l'utilisateur. la perspective.

Paul Dixon
la source
Merci pour tous les conseils. La spécificité de ignore_user_abort n'aide pas vraiment dans mon cas, tout mon objectif est d'éviter des retards inutiles pour l'utilisateur.
davr
2
Si vous définissez l'en-tête HTTP Content-Length dans votre réponse "Merci de votre inscription", le navigateur doit fermer la connexion une fois le nombre d'octets spécifié reçu. Cela laisse le processus côté serveur en cours d'exécution (en supposant que ignore_user_abort est défini) sans faire attendre l'utilisateur final. Bien sûr, vous devrez calculer la taille de votre contenu de réponse avant de rendre les en-têtes, mais c'est assez facile pour les réponses courtes.
Peter
1
Gearman ( gearman.org ) est une excellente file d'attente de messages open source multi-plateforme. Vous pouvez écrire des workers en C, PHP, Perl ou à peu près dans n'importe quel autre langage. Il existe des plugins Gearman UDF pour MySQL et vous pouvez également utiliser Net_Gearman à partir de PHP ou du client gearman pear.
Justin Swanhart
Gearman serait ce que je recommanderais aujourd'hui (en 2015) sur tout système de file d'attente de travail personnalisé.
Peter
Une autre option consiste à configurer un serveur node js pour traiter une requête et renvoyer une réponse rapide avec une tâche intermédiaire. Beaucoup de choses à l'intérieur d'un script node js sont exécutées de manière asynchrone, comme une requête http.
Zordon
22

Lorsque vous souhaitez simplement exécuter une ou plusieurs requêtes HTTP sans avoir à attendre la réponse, il existe également une solution PHP simple.

Dans le script d'appel:

$socketcon = fsockopen($host, 80, $errno, $errstr, 10);
if($socketcon) {   
   $socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n";      
   fwrite($socketcon, $socketdata); 
   fclose($socketcon);
}
// repeat this with different parameters as often as you like

Sur le script.php appelé, vous pouvez invoquer ces fonctions PHP dans les premières lignes:

ignore_user_abort(true);
set_time_limit(0);

Cela provoque l'exécution du script sans limite de temps lorsque la connexion HTTP est fermée.

Markus
la source
set_time_limit n'a aucun effet si php s'exécute en mode sans échec
Baptiste Pernet
17

Une autre façon de bifurquer les processus est via curl. Vous pouvez configurer vos tâches internes en tant que service Web. Par exemple:

Ensuite, dans vos scripts accédés par l'utilisateur, appelez le service:

$service->addTask('t1', $data); // post data to URL via curl

Votre service peut garder une trace de la file d'attente des tâches avec mysql ou tout ce que vous voulez: tout est enveloppé dans le service et votre script ne consomme que des URL. Cela vous permet de déplacer le service vers une autre machine / serveur si nécessaire (c'est-à-dire facilement évolutif).

L'ajout d'une autorisation http ou d'un schéma d'autorisation personnalisé (comme les services Web d'Amazon) vous permet d'ouvrir vos tâches à d'autres personnes / services (si vous le souhaitez) et vous pouvez aller plus loin et ajouter un service de surveillance en plus pour garder une trace de état de la file d'attente et de la tâche.

Cela prend un peu de travail de configuration, mais il y a beaucoup d'avantages.

Rojoca
la source
1
Je n'aime pas cette approche car elle surcharge le serveur web
Oved Yavine
7

J'ai utilisé Beanstalkd pour un projet et je prévoyais de recommencer. J'ai trouvé que c'était un excellent moyen d'exécuter des processus asynchrones.

J'ai fait quelques choses avec:

  • Redimensionnement d'image - et avec une file d'attente légèrement chargée passant à un script PHP basé sur CLI, le redimensionnement d'images de grande taille (2 Mo +) fonctionnait très bien, mais essayer de redimensionner les mêmes images dans une instance mod_php se heurtait régulièrement à des problèmes d'espace mémoire (je limité le processus PHP à 32 Mo, et le redimensionnement a pris plus que cela)
  • vérifications dans un futur proche - beanstalkd a des retards à sa disposition (rendez ce travail disponible pour s'exécuter seulement après X secondes) - afin que je puisse déclencher 5 ou 10 vérifications pour un événement, un peu plus tard dans le temps

J'ai écrit un système basé sur Zend-Framework pour décoder une URL «sympa», par exemple pour redimensionner une image qu'il appellerait QueueTask('/image/resize/filename/example.jpg'). L'URL a d'abord été décodée en un tableau (module, contrôleur, action, paramètres), puis convertie en JSON pour être injectée dans la file d'attente elle-même.

Un script cli de longue durée a ensuite récupéré le travail dans la file d'attente, l'a exécuté (via Zend_Router_Simple) et, si nécessaire, mis des informations dans memcached pour que le site Web PHP les récupère lorsque cela a été fait.

Une difficulté que j'ai également ajoutée était que le cli-script ne fonctionnait que pendant 50 boucles avant de redémarrer, mais s'il voulait redémarrer comme prévu, il le ferait immédiatement (exécuté via un script bash). S'il y avait un problème et que je l'ai fait exit(0)(la valeur par défaut pour exit;ou die();), cela ferait d'abord une pause pendant quelques secondes.

Alister Bulman
la source
J'aime le look de beanstalkd, une fois qu'ils ajoutent de la persistance, je pense que ce sera parfait.
davr
C'est déjà dans la base de code et en cours de stabilisation. J'ai aussi hâte d'avoir des «emplois nommés», donc je peux y ajouter des choses, mais sachez que cela ne sera pas ajouté s'il y en a déjà un. Bon pour les événements réguliers.
Alister Bulman
@AlisterBulman pourrait vous donner plus d'informations ou d'exemples pour "Un script cli long a ensuite récupéré le travail dans la file d'attente". J'essaye de créer un tel script cli pour mon application.
Sasi varna kumar
7

S'il s'agit simplement de fournir des tâches coûteuses, dans le cas où php-fpm est pris en charge, pourquoi ne pas utiliser la fastcgi_finish_request()fonction?

Cette fonction vide toutes les données de réponse vers le client et termine la demande. Cela permet d'effectuer des tâches chronophages sans laisser la connexion au client ouverte.

Vous n'utilisez pas vraiment l'asynchronicité de cette manière:

  1. Faites d'abord tout votre code principal.
  2. Exécutez fastcgi_finish_request().
  3. Faites tous les trucs lourds.

Une fois de plus, php-fpm est nécessaire.

Denys Gorobchenko
la source
5

Voici une classe simple que j'ai codée pour mon application Web. Il permet de forger des scripts PHP et d'autres scripts. Fonctionne sous UNIX et Windows.

class BackgroundProcess {
    static function open($exec, $cwd = null) {
        if (!is_string($cwd)) {
            $cwd = @getcwd();
        }

        @chdir($cwd);

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $WshShell = new COM("WScript.Shell");
            $WshShell->CurrentDirectory = str_replace('/', '\\', $cwd);
            $WshShell->Run($exec, 0, false);
        } else {
            exec($exec . " > /dev/null 2>&1 &");
        }
    }

    static function fork($phpScript, $phpExec = null) {
        $cwd = dirname($phpScript);

        @putenv("PHP_FORCECLI=true");

        if (!is_string($phpExec) || !file_exists($phpExec)) {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', dirname(ini_get('extension_dir'))) . '\php.exe';

                if (@file_exists($phpExec)) {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            } else {
                $phpExec = exec("which php-cli");

                if ($phpExec[0] != '/') {
                    $phpExec = exec("which php");
                }

                if ($phpExec[0] == '/') {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            }
        } else {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', $phpExec);
            }

            BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
        }
    }
}
Andrew Moore
la source
4

C'est la même méthode que j'utilise depuis quelques années maintenant et je n'ai rien vu ni trouvé de mieux. Comme les gens l'ont dit, PHP est monothread, donc vous ne pouvez pas faire grand-chose d'autre.

J'ai en fait ajouté un niveau supplémentaire à cela et c'est obtenir et stocker l'ID de processus. Cela me permet de rediriger vers une autre page et de faire asseoir l'utilisateur sur cette page, en utilisant AJAX pour vérifier si le processus est terminé (l'identifiant de processus n'existe plus). Ceci est utile dans les cas où la longueur du script entraînerait l'expiration du délai du navigateur, mais l'utilisateur doit attendre que ce script se termine avant l'étape suivante. (Dans mon cas, il traitait de gros fichiers ZIP avec des fichiers de type CSV qui ajoutent jusqu'à 30000 enregistrements à la base de données, après quoi l'utilisateur doit confirmer certaines informations.)

J'ai également utilisé un processus similaire pour la génération de rapports. Je ne suis pas sûr que j'utiliserais le "traitement en arrière-plan" pour quelque chose comme un e-mail, à moins qu'il y ait un réel problème avec un SMTP lent. Au lieu de cela, je pourrais utiliser une table comme file d'attente, puis avoir un processus qui s'exécute toutes les minutes pour envoyer les e-mails dans la file d'attente. Vous devez être prudent d’envoyer des courriels deux fois ou d’autres problèmes similaires. J'envisagerais également un processus de mise en file d'attente similaire pour d'autres tâches.

Darryl Hein
la source
1
De quelle méthode parlez-vous dans votre première phrase?
Simon East
3

PHP HAS multithreading, il n'est tout simplement pas activé par défaut, il existe une extension appelée pthreads qui fait exactement cela. Cependant, vous aurez besoin de php compilé avec ZTS. (Thread Safe) Liens:

Exemples

Un autre tutoriel

Extension de pthreads PECL

Omar S.
la source
2

C'est une bonne idée d'utiliser cURL comme suggéré par rojoca.

Voici un exemple. Vous pouvez surveiller text.txt pendant que le script s'exécute en arrière-plan:

<?php

function doCurl($begin)
{
    echo "Do curl<br />\n";
    $url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
    $url = preg_replace('/\?.*/', '', $url);
    $url .= '?begin='.$begin;
    echo 'URL: '.$url.'<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    echo 'Result: '.$result.'<br>';
    curl_close($ch);
}


if (empty($_GET['begin'])) {
    doCurl(1);
}
else {
    while (ob_get_level())
        ob_end_clean();
    header('Connection: close');
    ignore_user_abort();
    ob_start();
    echo 'Connection Closed';
    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();

    $begin = $_GET['begin'];
    $fp = fopen("text.txt", "w");
    fprintf($fp, "begin: %d\n", $begin);
    for ($i = 0; $i < 15; $i++) {
        sleep(1);
        fprintf($fp, "i: %d\n", $i);
    }
    fclose($fp);
    if ($begin < 10)
        doCurl($begin + 1);
}

?>
Kjeld
la source
2
Cela aiderait vraiment si le code source était commenté. Je n'ai aucune idée de ce qui se passe là-dedans et quelles parties sont des exemples et quelles parties sont réutilisables pour mon propre usage.
Thomas Tempelmann
1

Malheureusement, PHP ne dispose d'aucune sorte de capacités de threading natives. Je pense donc que dans ce cas, vous n'avez pas d'autre choix que d'utiliser une sorte de code personnalisé pour faire ce que vous voulez faire.

Si vous recherchez sur le net des trucs de threads PHP, certaines personnes ont trouvé des moyens de simuler des threads sur PHP.

Peter D
la source
1

Si vous définissez l'en-tête HTTP Content-Length dans votre réponse "Merci de votre inscription", le navigateur doit fermer la connexion une fois le nombre d'octets spécifié reçu. Cela laisse le processus côté serveur en cours d'exécution (en supposant que ignore_user_abort est défini) afin qu'il puisse finir de fonctionner sans faire attendre l'utilisateur final.

Bien sûr, vous devrez calculer la taille de votre contenu de réponse avant de rendre les en-têtes, mais c'est assez facile pour les réponses courtes (écrire la sortie dans une chaîne, appeler strlen (), appeler l'en-tête (), rendre la chaîne).

Cette approche a l’avantage de ne pas vous obliger à gérer une file d’attente «front-end», et bien que vous deviez peut-être faire un peu de travail sur le back-end pour éviter que les processus enfants HTTP ne se marchent les uns sur les autres, c'est quelque chose que vous deviez déjà faire , en tous cas.

Peter
la source
Cela ne semble pas fonctionner. Lorsque j'utilise header('Content-Length: 3'); echo '1234'; sleep(5);alors même si le navigateur ne prend que 3 caractères, il attend toujours 5 secondes avant d'afficher la réponse. Qu'est-ce que je rate?
Thomas Tempelmann
@ThomasTempelmann - Vous devez probablement appeler flush () pour forcer la sortie à être rendue immédiatement, sinon la sortie sera mise en mémoire tampon jusqu'à ce que votre script se termine ou que suffisamment de données soient envoyées à STDOUT pour vider le tampon.
Peter
J'ai déjà essayé de nombreuses façons de vider, trouvées ici sur SO. Aucune aide. Et les données semblent également être envoyées non gzippées, comme on peut le voir phpinfo(). La seule autre chose que je pourrais imaginer est que je dois d'abord atteindre une taille de tampon minimum, par exemple 256 octets environ.
Thomas Tempelmann
@ThomasTempelmann - Je ne vois rien dans votre question ou ma réponse à propos de gzip (il est généralement logique de faire fonctionner le scénario le plus simple avant d'ajouter des couches de complexité). Afin d'établir quand le serveur envoie réellement des données, vous pouvez utiliser un renifleur de paquets du plugin de navigateur (comme Fiddler, Tamperdata, etc.). Ensuite, si vous constatez que le serveur Web contient vraiment toute la sortie du script jusqu'à la sortie indépendamment du vidage, vous devez alors modifier la configuration de votre serveur Web (il n'y a rien que votre script PHP puisse faire dans ce cas).
Peter
J'utilise un service Web virtuel, j'ai donc peu de contrôle sur sa configuration. J'espérais trouver d'autres suggestions sur ce qui pourrait être le coupable, mais il semble que votre réponse n'est tout simplement pas aussi universellement applicable qu'il n'y paraît. Trop de choses peuvent mal tourner, évidemment. Votre solution est sûrement beaucoup plus facile à mettre en œuvre que toutes les autres réponses données ici. Dommage que ça ne marche pas pour moi.
Thomas Tempelmann
1

Si vous ne voulez pas l'ActiveMQ à part entière, je vous recommande d'envisager RabbitMQ . RabbitMQ est une messagerie légère qui utilise la norme AMQP .

Je recommande de consulter également php-amqplib - une bibliothèque cliente AMQP populaire pour accéder aux courtiers de messages basés sur AMQP.

phpPhil
la source
0

Je pense que vous devriez essayer cette technique, cela vous aidera à appeler autant de pages que vous aimez toutes les pages fonctionneront à la fois indépendamment sans attendre que chaque page de réponse soit asynchrone.

cornjobpage.php // page principale

    <?php

post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
            ?>
            <?php

            /*
             * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
             *  
             */
            function post_async($url,$params)
            {

                $post_string = $params;

                $parts=parse_url($url);

                $fp = fsockopen($parts['host'],
                    isset($parts['port'])?$parts['port']:80,
                    $errno, $errstr, 30);

                $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                $out.= "Host: ".$parts['host']."\r\n";
                $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                $out.= "Content-Length: ".strlen($post_string)."\r\n";
                $out.= "Connection: Close\r\n\r\n";
                fwrite($fp, $out);
                fclose($fp);
            }
            ?>

testpage.php

    <?
    echo $_REQUEST["Keywordname"];//case1 Output > testValue
    ?>

PS: si vous souhaitez envoyer des paramètres d'url en boucle, suivez cette réponse: https://stackoverflow.com/a/41225209/6295712

Hassan Saeed
la source
0

Création de nouveaux processus sur le serveur en utilisant exec() ou directement sur un autre serveur utilisant curl ne s'adapte pas très bien du tout, si nous optons pour exec, vous remplissez essentiellement votre serveur de processus de longue durée qui peuvent être gérés par d'autres serveurs non Web, et l'utilisation de curl lie un autre serveur à moins que vous ne construisiez une sorte d'équilibrage de charge.

J'ai utilisé Gearman dans quelques situations et je le trouve meilleur pour ce genre de cas d'utilisation. Je peux utiliser un serveur de file d'attente de travaux unique pour gérer la mise en file d'attente de tous les travaux devant être effectués par le serveur et faire tourner les serveurs de travail, chacun pouvant exécuter autant d'instances du processus de travail que nécessaire, et augmenter le nombre de les serveurs de travail selon les besoins et les ralentir lorsqu'ils ne sont pas nécessaires. Cela me permet également d'arrêter complètement les processus de travail lorsque cela est nécessaire et de mettre les travaux en file d'attente jusqu'à ce que les travailleurs reviennent en ligne.

Chris Rutherfurd
la source
-4

PHP est un langage à thread unique, il n'y a donc pas de moyen officiel de démarrer un processus asynchrone avec lui autre que d'utiliser execou popen. Il y a un article de blog à ce sujet ici . Votre idée de file d'attente dans MySQL est également une bonne idée.

Votre exigence spécifique ici est d'envoyer un e-mail à l'utilisateur. Je suis curieux de savoir pourquoi vous essayez de le faire de manière asynchrone, car l'envoi d'un e-mail est une tâche assez triviale et rapide à effectuer. Je suppose que si vous envoyez des tonnes de courriers électroniques et que votre FAI vous bloque en cas de suspicion de spam, cela pourrait être une raison de faire la queue, mais à part cela, je ne vois aucune raison de le faire de cette façon.

Marc W
la source
L'e-mail n'était qu'un exemple, car les autres tâches sont plus complexes à expliquer, et ce n'est pas vraiment le but de la question. Comme nous avions l'habitude d'envoyer des e-mails, la commande e-mail ne revenait pas tant que le serveur distant n'acceptait pas les e-mails. Nous avons constaté que certains serveurs de messagerie étaient configurés pour ajouter de longs délais (comme des délais de 10 à 20 secondes) avant d'accepter le courrier (probablement pour lutter contre les spambots), et ces délais seraient ensuite transmis à nos utilisateurs. Maintenant, nous utilisons un serveur de messagerie local pour mettre en file d'attente les mails à envoyer, donc celui-ci ne s'applique pas, mais nous avons d'autres tâches de nature similaire.
davr
Par exemple: l'envoi d'e-mails via Google Apps Smtp avec SSL et le port 465 prend plus de temps que d'habitude.
Gixty