web-dev-qa-db-fra.com

Meilleur moyen de gérer un script php de longue durée?

J'ai un script PHP qui prend beaucoup de temps (5-30 minutes). Juste au cas où cela serait important, le script utilise curl pour extraire les données d’un autre serveur. C'est la raison pour laquelle cela prend si longtemps; il doit attendre que chaque page soit chargée avant de la traiter et de passer à la suivante.

Je veux pouvoir lancer le script et le laisser jusqu'à ce qu'il soit terminé, ce qui définira un indicateur dans une table de base de données.

Ce que j'ai besoin de savoir, c'est comment pouvoir mettre fin à la requête http avant que le script ne soit exécuté. Aussi, un script php est-il le meilleur moyen de le faire?

68
kbanman

Certes, cela peut être fait avec PHP, mais vous NE devez PAS le faire en tâche de fond: le nouveau processus doit être dissocié du groupe de processus où il est lancé.

Puisque les gens continuent à donner la même mauvaise réponse à cette FAQ, j'ai écrit une réponse plus complète ici:

http://symcbean.blogspot.com/2010/02/php-and-long-running-processes.html

D'après les commentaires: 

La version courte est Shell_exec('echo /usr/bin/php -q longThing.php | at now'); mais les raisons pour lesquelles l'inclusion est un peu longue sont ici.

100
symcbean

La manière rapide et sale serait d’utiliser la fonction ignore_user_abort en php. Ceci dit en gros: Ne vous souciez pas de ce que l'utilisateur fait, exécutez ce script jusqu'à ce qu'il soit terminé. C'est un peu dangereux s'il s'agit d'un site public (car il est possible que les versions 20 ++ du script s'exécutent simultanément si le script est lancé 20 fois).

La méthode "propre" (du moins à mon humble avis) consiste à définir un indicateur (dans la base de données, par exemple) lorsque vous souhaitez lancer le processus et exécuter un cronjob toutes les heures (ou presque) pour vérifier si cet indicateur est défini. Si IS est défini, le script à exécution longue démarre. S'il n'est PAS défini, rien ne se produit.

11
FlorianH

Vous pouvez utiliser exec ou system pour démarrer un travail en arrière-plan, puis effectuer le travail correspondant. 

En outre, il existe de meilleures approches pour gratter le Web que celle que vous utilisez. Vous pouvez utiliser une approche par thread (plusieurs threads ne font qu'une page à la fois) ou une autre en utilisant un eventloop (un seul thread fait plusieurs pages à la fois). Mon approche personnelle en utilisant Perl serait d'utiliser AnyEvent :: HTTP .

ETA: symcbean a expliqué comment détacher correctement le processus d’arrière-plan ici .

8
Leon Timmermans

Non, PHP n'est pas la meilleure solution.

Je ne suis pas sûr de Ruby ou de Perl, mais avec Python, vous pourriez réécrire votre grattoir de page pour qu'il soit multi-thread et qu'il fonctionnerait probablement 20 fois plus vite. Écrire des applications multi-threadées peut s'avérer un défi, mais la toute première application Python que j'ai écrite était un grattoir de pages multi-threads. Et vous pouvez simplement appeler le script Python depuis votre page PHP en utilisant l’une des fonctions d’exécution du shell.

5
jamieb

Oui, vous pouvez le faire en PHP. Mais en plus de PHP, il serait sage d'utiliser un gestionnaire de files d'attente. Voici la stratégie:

  1. Divisez votre tâche importante en tâches plus petites. Dans votre cas, chaque tâche pourrait charger une seule page.

  2. Envoyez chaque petite tâche à la file d'attente.

  3. Exécutez vos travailleurs de file d'attente quelque part.

L'utilisation de cette stratégie présente les avantages suivants:

  1. Pour les tâches de longue durée, il a la capacité de récupérer en cas de problème fatal au milieu de l'exécution - inutile de recommencer à zéro.

  2. Si vos tâches ne doivent pas nécessairement être exécutées de manière séquentielle, vous pouvez exécuter plusieurs travailleurs pour les exécuter simultanément.

Vous avez une variété d'options (en voici quelques unes):

  1. RabbitMQ ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html )
  2. ZeroMQ ( http://zeromq.org/bindings:php )
  3. Si vous utilisez le framework Laravel, les files d'attente sont intégrées ( https://laravel.com/docs/5.4/queues ), avec les pilotes pour AWS SES, Redis, Beanstalkd.
4
aljo f

PHP peut être ou ne pas être le meilleur outil, mais vous savez comment l'utiliser, et le reste de votre application est écrit en l'utilisant. Ces deux qualités, associées au fait que PHP est "assez bon", plaident plutôt en faveur de son utilisation, au lieu de Perl, Ruby ou Python.

Si votre objectif est d'apprendre une autre langue, choisissez-en une et utilisez-la. Toutes les langues que vous avez mentionnées feront le travail, pas de problème. J'aime bien Perl, mais ce que vous aimez peut être différent.

Symcbean a quelques bons conseils sur la façon de gérer les processus d’arrière-plan sur son lien.

En bref, écrivez un script CLI PHP pour gérer les bits longs. Assurez-vous qu'il enregistre le statut d'une manière ou d'une autre. Créez une page php pour gérer les mises à jour de statut, en utilisant AJAX ou des méthodes traditionnelles. Votre script de lancement lancera le processus en cours d'exécution dans sa propre session et renverra une confirmation de son exécution. 

Bonne chance.

3
daotoad

Vous pouvez l'envoyer sous forme de demande XHR (Ajax). Les clients n'ont généralement pas de délai d'expiration pour les XHR, contrairement aux requêtes HTTP normales.

1
JAL

Je me rends compte que la question est assez ancienne, mais j'aimerais tenter le coup. Ce script tente de répondre rapidement à l'appel initial du coup d'envoi et de réduire la charge en morceaux plus petits. Je n'ai pas testé cette solution.

<?php
/**
 * crawler.php located at http://mysite.com/crawler.php
 */

// Make sure this script will keep on runing after we close the connection with
// it.
ignore_user_abort(TRUE);


function get_remote_sources_to_crawl() {
  // Do a database or a log file query here.

  $query_result = array (
    1 => 'http://exemple.com',
    2 => 'http://exemple1.com',
    3 => 'http://exemple2.com',
    4 => 'http://exemple3.com',
    // ... and so on.
  );

  // Returns the first one on the list.
  foreach ($query_result as $id => $url) {
    return $url;
  }
  return FALSE;
}

function update_remote_sources_to_crawl($id) {
  // Update my database or log file list so the $id record wont show up
  // on my next call to get_remote_sources_to_crawl()
}

$crawling_source = get_remote_sources_to_crawl();

if ($crawling_source) {


  // Run your scraping code on $crawling_source here.


  if ($your_scraping_has_finished) {
    // Update you database or log file.
    update_remote_sources_to_crawl($id);

    $ctx = stream_context_create(array(
      'http' => array(
        // I am not quite sure but I reckon the timeout set here actually
        // starts rolling after the connection to the remote server is made
        // limiting only how long the downloading of the remote content should take.
        // So as we are only interested to trigger this script again, 5 seconds 
        // should be plenty of time.
        'timeout' => 5,
      )
    ));

    // Open a new connection to this script and close it after 5 seconds in.
    file_get_contents('http://' . $_SERVER['HTTP_Host'] . '/crawler.php', FALSE, $ctx);

    print 'The cronjob kick off has been initiated.';
  }
}
else {
  print 'Yay! The whole thing is done.';
}
1
Francisco Luz

Je suis d'accord avec les réponses qui disent que cela devrait être exécuté dans un processus en arrière-plan. Mais il est également important que vous rendiez compte de l'état pour que l'utilisateur sache que le travail est en cours. 

Lorsque vous recevez la demande PHP pour lancer le processus, vous pouvez stocker dans une base de données une représentation de la tâche avec un identificateur unique. Ensuite, démarrez le processus de nettoyage d’écran en lui passant l’identificateur unique. Indiquez à l'application iPhone que la tâche a été démarrée et qu'elle devrait vérifier une URL spécifiée, contenant le nouvel ID de tâche, pour obtenir le dernier statut. L'application iPhone peut désormais interroger (ou même "interroger de long") cette URL. Entre-temps, le processus d'arrière-plan met à jour la représentation de la tâche dans la base de données, en fonction du pourcentage d'achèvement, de l'étape en cours ou de tout autre indicateur de statut souhaité. Et quand il aura fini, il mettra un drapeau terminé.

1
Jacob

Je voudrais proposer une solution légèrement différente de symcbean, principalement parce que je souhaite en outre que le processus d'exécution longue soit exécuté en tant qu'utilisateur différent et non en tant qu'utilisateur Apache/www-data.

Première solution utilisant cron pour interroger une table de tâches en arrière-plan:

  • Page Web PHP insérée dans une table de tâches en arrière-plan, état 'SUBMITTED'
  • cron s'exécute une fois toutes les 3 minutes, avec un autre utilisateur, en exécutant le script CLI PHP qui vérifie la présence de lignes "SUBMITTED" dans la table des tâches en arrière-plan.
  • PHP CLI mettra à jour la colonne d'état de la ligne en 'PROCESSING' et commencera le traitement. Une fois terminé, il sera mis à jour en 'COMPLETED'

Deuxième solution utilisant Linux inotify facility:

  • La page Web PHP met à jour un fichier de contrôle avec les paramètres définis par l'utilisateur, ainsi que l'identifiant de la tâche.
  • Le script shell (en tant qu'utilisateur non www) exécutant inotifywait attendra que le fichier de contrôle soit écrit.
  • après l'écriture du fichier de contrôle, un événement close_write sera déclenché et le script Shell continuera
  • Le script shell exécute la CLI PHP pour exécuter le processus long
  • La CLI de PHP écrit la sortie dans un fichier journal identifié par l'ID de la tâche ou met à jour la progression dans une table d'état.
  • La page Web PHP peut interroger le fichier journal (en fonction de l'ID de la tâche) pour afficher la progression du processus d'exécution longue, ou interroger une table d'état.

Quelques informations supplémentaires pourraient être trouvées dans mon post: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html

1
YudhiWidyatama

J'ai fait la même chose avec Perl, double fork () et la séparation du processus parent. Tout le travail de récupération http doit être effectué dans un processus forké.

0
Alexandr Ciornii

si vous avez un long script, divisez le travail des pages à l’aide du paramètre d’entrée pour chaque tâche (chaque page se comporte comme un fil) transmettez ce mot-clé de magic ou cornjobpage.php (dans l'exemple suivant)

et pour les travailleurs d’arrière-plan, je pense que vous devriez essayer cette technique. Cela vous aidera d’appeler autant de pages que vous aimez, toutes les pages s’exécuteront en même temps, 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

ce que j’utilise TOUJOURS est l’une de ces variantes (parce que différents types de Linux ont des règles différentes sur la gestion de la sortie/certains programmes produisent différemment):

Variante I @exec ('./ myscript.php\1>/dev/null\2>/dev/null &');

Variante II @exec ('php -f myscript.php\1>/dev/null\2>/dev/null &');

Variante III @exec ('Nohup myscript.php\1>/dev/null\2>/dev/null &');

Vous pourriez avoir installer "Nohup". Mais par exemple, lorsque j'automatisais les conversions vidéo FFMPEG, l'interface de sortie n'était pas gérée à 100% en redirigeant les flux de sortie 1 et 2;.

0
dr burns

Ce n'est pas la meilleure approche, comme beaucoup l'ont indiqué ici, mais cela pourrait aider:

ignore_user_abort(1); // run script in background even if user closes browser
set_time_limit(1800); // run it for 30 minutes

// Long running script here
0
Lucas Bustamante

Utilisez un proxy pour déléguer la demande. 

0
zerodin