web-dev-qa-db-fra.com

Courir PHP Tâche asynchrone

Je travaille sur une application Web quelque peu volumineuse, et le back-end est principalement en PHP. Il y a plusieurs endroits dans le code où je dois effectuer certaines tâches, mais je ne souhaite pas que l'utilisateur attend le résultat. Par exemple, lors de la création d'un nouveau compte, je dois leur envoyer un email de bienvenue. Mais quand ils appuient sur le bouton 'Terminer l'enregistrement', je ne veux pas leur faire attendre que l'e-mail soit réellement envoyé, je veux juste démarrer le processus et renvoyer immédiatement un message à l'utilisateur.

Jusqu'à présent, dans certains endroits, j'ai utilisé ce qui ressemble à un piratage avec exec (). Faire essentiellement 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 place les tâches dans une table MySQL en file d'attente et un script distinct PHP 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 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?

131
davr

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, ce qui vous permet de gérer votre charge de manière assez efficace si vous pouvez partitionner des "tâches qui ne sont pas urgentes" facilement.

Rouler vous-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 être une option populaire, voir les commentaires ci-dessous.
  • ActiveMQ si vous voulez une file d’attente de messages open source complètement éclatée. 
  • ZeroMQ - c'est une bibliothèque de socket assez cool qui facilite l'écriture de code distribué sans trop s'inquiéter de la programmation de socket elle-même. Vous pouvez l'utiliser pour la mise en file d'attente de messages sur un hôte unique. Vous avez simplement votre application Web. Diffusez quelque chose dans une file d'attente qu'une application de console fonctionnant en continu consomme à la prochaine opportunité appropriée.
  • beanstalkd - seulement trouvé celui-ci en écrivant cette réponse, mais a l'air intéressant
  • 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 mis à jour (2017) autour de divers systèmes de file 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 fin prématurée, bien que cela ait pour effet de prolonger la page. charger du point de vue de l'utilisateur.

76
Paul Dixon

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

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 appeler ces fonctions PHP dans les premières lignes:

ignore_user_abort(true);
set_time_limit(0);

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

20
Markus

Une autre façon de créer des processus consiste à utiliser curl. Vous pouvez configurer vos tâches internes en tant que service Web. Par exemple:

Ensuite, dans les scripts auxquels l'utilisateur a accédé, appelez le service:

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

Votre service peut suivre la file d'attente des tâches avec mysql ou ce que vous voulez, c'est que tout est inclus dans le service et que votre script ne consomme que des URL. Cela vous permet de déplacer le service sur 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 de laisser vos tâches consommées par d'autres personnes/services (si vous le souhaitez). Vous pouvez aller plus loin et ajouter un service de surveillance pour suivre les événements. file d'attente et statut de la tâche.

Cela demande un peu de travail d’installation, mais les avantages sont nombreux.

17
rojoca

J'ai utilisé Beanstalkd pour un projet et j'avais déjà prévu de le faire. J'ai trouvé que c'était un excellent moyen d'exécuter des processus asynchrones. 

Quelques choses que j'ai faites avec:

  • Redimensionnement de l'image - et avec une file d'attente peu chargée passant à un script PHP basé sur CLI, redimensionner de grandes images (2 Mo +) fonctionnait parfaitement, mais essayer de redimensionner les mêmes images dans une instance de mod_php s'exécutait régulièrement en mémoire- problèmes d'espace (j'ai limité le processus PHP à 32 Mo, et le redimensionnement a pris plus que cela)
  • contrôles dans un avenir rapproché - beanstalkd dispose de retards (assurez-vous que ce travail ne peut être exécuté qu'après X secondes) - je peux donc exécuter 5 ou 10 contrôles pour un événement, un peu plus tard

J'ai écrit un système basé sur Zend-Framework pour décoder une URL 'Nice', ainsi, par exemple, pour redimensionner une image, il appellerait QueueTask('/image/resize/filename/example.jpg'). L'URL a d'abord été décodée dans 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 long a ensuite récupéré le travail dans la file d'attente, l'a exécuté (via Zend_Router_Simple) et, si nécessaire, a mis des informations dans memcached pour que le site Web PHP puisse être récupéré comme il se doit.

Une erreur que j’ai également mise en place est que le script cli n’exécutait que 50 boucles avant de redémarrer, mais s’il souhaitait 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 j'avais exit(0) (la valeur par défaut pour exit; ou die();), la première pause serait de deux ou trois secondes.

7
Alister Bulman

S'il s'agit simplement de fournir des tâches coûteuses, si php-fpm est supporté, pourquoi ne pas utiliser fastcgi_finish_request() function? 

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

Vous n'utilisez pas vraiment asynchronicity de cette façon:

  1. Faites tout votre code principal en premier. 
  2. Exécutez fastcgi_finish_request().
  3. Faire tous les trucs lourds.

Encore une fois, php-fpm est nécessaire.

7
Denys Gorobchenko

Voici une classe simple que j'ai codée pour mon application Web. Il permet de falsifier 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);
        }
    }
}
5
Andrew Moore

C’est la même méthode que j’utilise depuis quelques années maintenant et je n’ai rien vu de mieux ni trouvé meilleur. Comme certains l’ont dit, PHP utilise un seul thread, vous ne pouvez donc rien faire d’autre.

En fait, j'ai ajouté un niveau supplémentaire à cela, qui consiste à récupérer et à stocker l'identifiant du 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'id du processus n'existe plus). Cela est utile dans les cas où la longueur du script entraînerait l'expiration du navigateur, mais l'utilisateur doit attendre que ce script soit terminé avant la prochaine étape. (Dans mon cas, il s'agissait de traiter de gros fichiers Zip avec des fichiers CSV, qui ajoutaient jusqu'à 30 000 enregistrements à la base de données, après quoi l'utilisateur devait 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 courrier électronique, sauf en cas de problème réel avec un SMTP lent. Au lieu de cela, je pourrais utiliser une table en tant que file d'attente, puis un processus s'exécutant toutes les minutes pour envoyer les courriels dans la file d'attente. Vous devez être vigilant en envoyant deux fois des courriels ou d’autres problèmes similaires. J'envisagerais un processus similaire de mise en file d'attente pour d'autres tâches.

4
Darryl Hein

PHP A multithreading, ce qui n’est pas activé par défaut, il existe une extension appelée pthreads qui fait exactement cela . Vous aurez besoin de php compilé avec ZTS. (Fil de sécurité) Liens:

Exemples

Un autre tutoriel

pthreads PECL Extension

3
Omar A. Shaban

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);
}

?>
2
Kjeld

Malheureusement PHP ne dispose d'aucun type de fonctionnalité de thread natif. Je pense donc que dans ce cas, vous n'avez pas d'autre choix que d'utiliser un type de code personnalisé pour faire ce que vous voulez faire.

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

1
Peter D

Si vous définissez l'en-tête HTTP Content-Length dans votre réponse "Merci pour votre inscription", le navigateur doit alors fermer la connexion après avoir reçu le nombre d'octets spécifié. 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 terminer son travail sans faire attendre l'utilisateur final.

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

Cette approche présente l’avantage de pas vous obliger à gérer une file d’attente «front-end», et même s’il est peut-être nécessaire de travailler en arrière-plan pour empêcher les processus enfants HTTP de s’exercer mutuellement, c besoin de faire déjà, de toute façon.

1
Peter

Si vous ne voulez pas de l'ActiveMQ complet, je vous recommande de considérer RabbitMQ . RabbitMQ est une messagerie légère qui utilise le standard AMQP .

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

1
phpPhil

je pense que vous devriez essayer cette technique, cela vous aidera à appeler autant de pages que vous aimez, toutes les pages fonctionneront en même temps indépendamment sans attendre chaque réponse de page comme asynchrone.

cornjobpage.php // mainpage

    <?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 voulez envoyer des paramètres d’URL sous forme de boucle, suivez la réponse suivante: https://stackoverflow.com/a/41225209/6295712

0
Hassan Saeed

Créer de nouveaux processus sur le serveur à l'aide de exec() ou directement sur un autre serveur à l'aide de curl ne s'adapte pas du tout. Si vous optez pour exec, vous remplissez votre serveur de longs processus pouvant être gérés par d'autres serveurs Web. , et l'utilisation de curl attache un autre serveur, sauf si vous installez une sorte d'équilibrage de la charge.

J'ai utilisé Gearman dans quelques situations et je le trouve mieux pour ce type de cas d'utilisation. Je peux utiliser un serveur de file d'attente de tâches unique pour gérer la mise en file d'attente de toutes les tâches devant être effectuées par le serveur et faire tourner les serveurs de travail, chacun pouvant exécuter autant de fois que nécessaire le processus de travail et augmenter le nombre de tâches. serveurs de travail au besoin et les réduire lorsqu'ils ne sont pas nécessaires. Cela me permet également de fermer 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.

0
Chris Rutherfurd