Beste Weg, um lang laufende PHP-Skript zu verwalten?

81

Ich habe ein PHP-Skript, dessen Fertigstellung lange dauert (5-30 Minuten). Nur für den Fall, dass es darauf ankommt, verwendet das Skript Curl, um Daten von einem anderen Server zu kratzen. Dies ist der Grund, warum es so lange dauert; Es muss warten, bis jede Seite geladen ist, bevor es verarbeitet und zur nächsten übergegangen wird.

Ich möchte in der Lage sein, das Skript zu initiieren und es so lange laufen zu lassen, bis es fertig ist, wodurch ein Flag in einer Datenbanktabelle gesetzt wird.

Ich muss wissen, wie ich die http-Anforderung beenden kann, bevor das Skript ausgeführt wird. Ist ein PHP-Skript auch der beste Weg, dies zu tun?

kbanman
quelle
1
Obwohl Sie es in den von Ihrem Server unterstützten Sprachen nicht erwähnt haben, werden Sie wahrscheinlich Node.js hinzufügen, wenn Sie Ruby und Perl ausführen können. Dies klingt für mich nach einem perfekten Anwendungsfall für Javascript : Ihr Skript verbringt die meiste Zeit damit, auf die Fertigstellung von Anforderungen zu warten. Dies ist ein Bereich, in dem sich das asynchrone Paradigma auszeichnet. Kein Thread bedeutet einfache Synchronisation, Parallelität bedeutet Spead.
DJFM
Sie können dies mit PHP tun. Ich würde Goutteund GuzzleParallelitätsthreads verwenden und implementieren. Sie können auch einen Blick darauf werfen Gearman, um parallele Anfragen in Form von Arbeitern zu starten.
Andre Garcia

Antworten:

114

Natürlich kann dies mit PHP durchgeführt werden, Sie sollten dies jedoch NICHT als Hintergrundaufgabe tun - der neue Prozess muss aus der Prozessgruppe entfernt werden, in der er initiiert wird.

Da die Leute immer wieder die gleiche falsche Antwort auf diese FAQ geben, habe ich hier eine ausführlichere Antwort geschrieben:

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

Aus den Kommentaren:

Die Kurzfassung ist shell_exec('echo /usr/bin/php -q longThing.php | at now');aber die Gründe dafür sind hier etwas lang.

symcbean
quelle
Dieser Blog-Beitrag ist die wahre Antwort. Das Exec & System von PHP weist zu viele potenzielle Fallstricke auf.
ungläubig
2
Gibt es eine Chance, die relevanten Details in die Antwort zu kopieren? Es gibt zu viele alte Antworten, die auf tote Blogs verweisen. Dieser Blog ist (noch) nicht tot, wird aber eines Tages sein.
Murphy
5
Die Kurzfassung ist shell_exec('echo /usr/bin/php -q longThing.php | at now');aber die Gründe dafür sind hier etwas lang.
Symcbean
1
Hochkarätige Antwort auf eine hochkarätige Frage, aber die Antwort enthält nicht viel mehr als einen Link zu einem Blogpost. Bitte fügen Sie die tatsächliche Antwort gemäß meta.stackexchange.com/questions/8231/… und / oder der Hilfe hinzu
Nanne
1
Darf ich wissen, was diese Option -q bewirkt?
Kiren Siva
11

Der schnelle und schmutzige Weg wäre, die ignore_user_abortFunktion in PHP zu verwenden. Dies sagt im Grunde: Es ist egal, was der Benutzer tut, führen Sie dieses Skript aus, bis es fertig ist. Dies ist etwas gefährlich, wenn es sich um eine öffentlich zugängliche Site handelt (da möglicherweise 20 ++ Versionen des Skripts gleichzeitig ausgeführt werden, wenn es 20 Mal gestartet wird).

Die "saubere" Methode (zumindest IMHO) besteht darin, ein Flag zu setzen (z. B. in der Datenbank), wenn Sie den Prozess starten und jede Stunde (oder so) einen Cronjob ausführen möchten, um zu überprüfen, ob dieses Flag gesetzt ist. Wenn es gesetzt ist, startet das Skript mit langer Laufzeit. Wenn es NICHT gesetzt ist, passiert nichts.

FlorianH
quelle
Die Methode "ignore_user_abort" würde es dem Benutzer also ermöglichen, das Browserfenster zu schließen. Kann ich jedoch eine HTTP-Antwort an den Client zurückgeben, bevor die Ausführung abgeschlossen ist?
Kbanman
1
@ kbanman Ja. Sie müssen die Verbindung schließen : header("Connection: close", true);. Und vergessen Sie nicht zu spülen ()
Benubird
8

Sie können exec oder system verwenden , um einen Hintergrundjob zu starten und dann die Arbeit darin zu erledigen.

Es gibt auch bessere Ansätze zum Scraping des Webs als das, das Sie verwenden. Sie können einen Thread-Ansatz verwenden (mehrere Threads führen jeweils eine Seite aus) oder einen Eventloop (ein Thread führt mehrere Seiten gleichzeitig aus). Mein persönlicher Ansatz bei der Verwendung von Perl wäre die Verwendung von AnyEvent :: HTTP .

ETA: symcbean erklärte , wie der Hintergrundprozess richtig lösen hier .

Leon Timmermans
quelle
5
Fast richtig. Wenn Sie nur exec oder system verwenden, werden Sie wieder auf den Arsch gebissen. Siehe meine Antwort für Details.
Symcbean
5

Nein, PHP ist nicht die beste Lösung.

Ich bin mir bei Ruby oder Perl nicht sicher, aber mit Python könnten Sie Ihren Seitenschaber so umschreiben, dass er über mehrere Threads verfügt, und er würde wahrscheinlich mindestens 20-mal schneller laufen. Das Schreiben von Multithread-Apps kann eine Herausforderung sein, aber die allererste Python-App, die ich geschrieben habe, war ein Seitenschaber mit mehreren Threads. Und Sie können das Python-Skript einfach von Ihrer PHP-Seite aus aufrufen, indem Sie eine der Shell-Ausführungsfunktionen verwenden.

Jamieb
quelle
Der eigentliche Verarbeitungsteil meines Scrapings ist sehr effizient. Wie ich oben erwähnt habe, ist es das Laden jeder Seite, das mich umbringt. Ich habe mich gefragt, ob PHP so lange ausgeführt werden soll.
Kbanman
Ich bin ein bisschen voreingenommen, weil ich seit dem Erlernen von Python PHP absolut verabscheue. Wenn Sie jedoch mehr als eine Seite (in Serie) kratzen, erzielen Sie mit ziemlicher Sicherheit eine bessere Leistung, wenn Sie dies parallel zu einer Multithread-App tun.
Jamieb
1
Könnten Sie mir vielleicht ein Beispiel für einen solchen Seitenschaber schicken? Es würde mir viel helfen, da ich Python noch nicht berührt habe.
Kbanman
Wenn ich es umschreiben müsste, würde ich einfach Eventlet verwenden. Es macht meinen Code über 10x einfacher: eventlet.net/doc
jamieb
5

Ja, Sie können es in PHP tun. Zusätzlich zu PHP ist es jedoch ratsam, einen Warteschlangenmanager zu verwenden. Hier ist die Strategie:

  1. Teilen Sie Ihre große Aufgabe in kleinere Aufgaben auf. In Ihrem Fall könnte jede Aufgabe das Laden einer einzelnen Seite sein.

  2. Senden Sie jede kleine Aufgabe an die Warteschlange.

  3. Führen Sie Ihre Warteschlangenarbeiter irgendwo aus.

Die Verwendung dieser Strategie hat folgende Vorteile:

  1. Bei Aufgaben mit langer Laufzeit kann das Gerät wiederhergestellt werden, falls mitten im Lauf ein schwerwiegendes Problem auftritt. Sie müssen nicht von vorne beginnen.

  2. Wenn Ihre Aufgaben nicht nacheinander ausgeführt werden müssen, können Sie mehrere Worker ausführen, um Aufgaben gleichzeitig auszuführen.

Sie haben verschiedene Möglichkeiten (dies sind nur einige):

  1. RabbitMQ ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html )
  2. ZeroMQ ( http://zeromq.org/bindings:php )
  3. Wenn Sie das Laravel-Framework verwenden, sind Warteschlangen mit Treibern für AWS SES, Redis und Beanstalkd integriert ( https://laravel.com/docs/5.4/queues )
aljo f
quelle
3

PHP kann das beste Tool sein oder auch nicht, aber Sie wissen, wie man es verwendet, und der Rest Ihrer Anwendung wird damit geschrieben. Diese beiden Eigenschaften, kombiniert mit der Tatsache, dass PHP "gut genug" ist, sind ein ziemlich starkes Argument für die Verwendung anstelle von Perl, Ruby oder Python.

Wenn Sie eine andere Sprache lernen möchten, wählen Sie eine aus und verwenden Sie sie. Jede Sprache, die Sie erwähnt haben, erledigt den Job, kein Problem. Ich mag Perl, aber was du magst, kann anders sein.

Symcbean hat unter seinem Link einige gute Ratschläge zum Verwalten von Hintergrundprozessen.

Kurz gesagt, schreiben Sie ein CLI-PHP-Skript, um die langen Bits zu verarbeiten. Stellen Sie sicher, dass der Status auf irgendeine Weise gemeldet wird. Erstellen Sie eine PHP-Seite, um Statusaktualisierungen entweder mit AJAX oder mit herkömmlichen Methoden durchzuführen. Ihr Kickoff-Skript startet den Prozess, der in einer eigenen Sitzung ausgeführt wird, und gibt die Bestätigung zurück, dass der Prozess ausgeführt wird.

Viel Glück.

daotoad
quelle
1

Ich stimme den Antworten zu, die besagen, dass dies in einem Hintergrundprozess ausgeführt werden sollte. Es ist aber auch wichtig, dass Sie über den Status berichten, damit der Benutzer weiß, dass die Arbeit erledigt wird.

Wenn Sie die PHP-Anforderung zum Starten des Prozesses erhalten, können Sie eine Darstellung der Aufgabe mit einer eindeutigen Kennung in einer Datenbank speichern. Starten Sie dann den Screen-Scraping-Prozess und übergeben Sie ihm die eindeutige Kennung. Melden Sie der iPhone-App, dass die Aufgabe gestartet wurde und dass eine angegebene URL mit der neuen Aufgaben-ID überprüft werden sollte, um den neuesten Status zu erhalten. Die iPhone-Anwendung kann diese URL jetzt abfragen (oder sogar "lange abrufen"). In der Zwischenzeit aktualisiert der Hintergrundprozess die Datenbankdarstellung der Aufgabe, da sie mit einem Abschlussprozentsatz, einem aktuellen Schritt oder anderen gewünschten Statusindikatoren funktioniert. Und wenn es fertig ist, würde es ein abgeschlossenes Flag setzen.

Jakob
quelle
1

Sie können es als XHR-Anfrage (Ajax) senden. Clients haben im Gegensatz zu normalen HTTP-Anforderungen normalerweise keine Zeitüberschreitung für XHRs.

JAL
quelle
1

Mir ist klar, dass dies eine ziemlich alte Frage ist, aber ich würde sie gerne ausprobieren. Dieses Skript versucht, sowohl den ersten Kick-Off-Aufruf zu adressieren, um schnell fertig zu werden, als auch die schwere Last in kleinere Teile zu zerlegen. Ich habe diese Lösung nicht getestet.

<?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.';
}
Francisco Luz
quelle
@symcbean Ich habe den von Ihnen vorgeschlagenen Beitrag gelesen und würde gerne Ihre Gedanken zu dieser alternativen Lösung hören.
Francisco Luz
Zunächst haben Sie mir eine Startidee für meinen ersten Bot (Teehee) gegeben. Zweitens, wie haben Sie die Leistung Ihrer Lösung gefunden? Haben Sie weiter daran gearbeitet und mehr gelernt? Ich bin daran interessiert, etwas Ähnliches zu implementieren, wie 26.000 Bilder (1,3 GB) auszubaggern, verschiedene Operationen auszuführen usw. Es wird eine Weile dauern. Dein ist die einzige Lösung , die nicht Hacky scheint, verwenden Sie exec () Schauder oder erfordern Linux (einige von uns Verlierer noch mit Windows verwenden). Ich lerne lieber aus deinem Kopfstoß als aus meinem eigenen: P
Just Plain High
@ HighPriestessofTheTech Hallo Kumpel, ich bin nicht weiter gegangen. Zu der Zeit, als ich das schrieb, habe ich gerade ein Gedankenexperiment durchgeführt.
Francisco Luz
1
Oh je ... Also werde ich von meinem eigenen Headbashing lernen ... Ich werde dich wissen lassen, wie es geht;)
Just Plain High
1
Ich habe es versucht und finde es sehr nützlich.
Alex
1

Ich möchte eine Lösung vorschlagen, die sich ein wenig von der von symcbean unterscheidet, hauptsächlich weil ich zusätzlich die Anforderung habe, dass der lang laufende Prozess als ein anderer Benutzer und nicht als Apache / www-Datenbenutzer ausgeführt werden muss.

Erste Lösung mit cron zum Abrufen einer Hintergrundaufgabentabelle:

  • Einfügungen von PHP-Webseiten in eine Hintergrundaufgabentabelle mit dem Status "SUBMITTED"
  • cron wird alle 3 Minuten mit einem anderen Benutzer ausgeführt und führt ein PHP-CLI-Skript aus, das die Hintergrundaufgabentabelle auf 'SUBMITTED'-Zeilen überprüft
  • PHP CLI aktualisiert die Statusspalte in der Zeile in 'PROCESSING' und beginnt mit der Verarbeitung. Nach Abschluss wird sie auf 'COMPLETED' aktualisiert.

Zweite Lösung mit Linux inotify:

  • Die PHP-Webseite aktualisiert eine Steuerdatei mit den vom Benutzer festgelegten Parametern und gibt auch eine Aufgaben-ID an
  • Das Shell-Skript (als Nicht-WWW-Benutzer), das inotifywait ausführt, wartet auf das Schreiben der Steuerdatei
  • Nachdem die Steuerdatei geschrieben wurde, wird ein close_write-Ereignis ausgelöst und das Shell-Skript fortgesetzt
  • Das Shell-Skript führt die PHP-CLI aus, um den lang laufenden Prozess auszuführen
  • PHP CLI schreibt die Ausgabe in eine Protokolldatei, die durch die Task-ID gekennzeichnet ist, oder aktualisiert alternativ den Fortschritt in einer Statustabelle
  • Die PHP-Webseite könnte die Protokolldatei (basierend auf der Task-ID) abfragen, um den Fortschritt des lang laufenden Prozesses anzuzeigen, oder sie könnte auch die Statustabelle abfragen

Einige zusätzliche Informationen finden Sie in meinem Beitrag: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html

YudhiWidyatama
quelle
0

Ich habe ähnliche Dinge mit Perl, double fork () und dem Trennen vom übergeordneten Prozess gemacht. Alle http-Abrufarbeiten sollten in einem gegabelten Prozess ausgeführt werden.

Alexandr Ciornii
quelle
0

Verwenden Sie einen Proxy, um die Anforderung zu delegieren.

Zerodin
quelle
0

Was ich IMMER benutze, ist eine dieser Varianten (weil verschiedene Linux-Varianten unterschiedliche Regeln für den Umgang mit Ausgaben haben / einige Programme unterschiedlich ausgeben):

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 &');

Möglicherweise haben Sie "nohup" installiert. Als ich zum Beispiel FFMPEG-Videokonvertierungen automatisierte, wurde die Ausgabeschnittstelle durch die Umleitung der Ausgabestreams 1 und 2 nicht zu 100% behandelt, sodass ich nohup verwendet UND die Ausgabe umgeleitet habe.

Dr. brennt
quelle
0

Wenn Sie ein langes Skript haben, teilen Sie die Seitenarbeit mit Hilfe von Eingabeparametern für jede Aufgabe (dann verhält sich jede Seite wie ein Thread). Wenn die Seite eine lange Prozessschleife mit lac product_keywords hat, erstellen Sie anstelle der Schleife eine Logik für ein Schlüsselwort und übergeben Sie dieses Schlüsselwort von magic oder cornjobpage.php (im folgenden Beispiel)

und für Hintergrundarbeiter denke ich, dass Sie diese Technik ausprobieren sollten. Es wird hilfreich sein, so viele Seiten aufzurufen, wie Sie möchten. Alle Seiten werden unabhängig voneinander gleichzeitig ausgeführt, ohne auf jede Seitenantwort als asynchron zu warten.

cornjobpage.php // Hauptseite

    <?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: Wenn Sie URL-Parameter als Schleife senden möchten, folgen Sie dieser Antwort: https://stackoverflow.com/a/41225209/6295712

Hassan Saeed
quelle
0

Nicht der beste Ansatz, wie viele hier angegeben haben, aber dies könnte helfen:

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
Lucas Bustamante
quelle
0

Wenn die gewünschte Ausgabe Ihres Skripts eine Verarbeitung ist, keine Webseite, dann besteht meiner Meinung nach die gewünschte Lösung darin, Ihr Skript einfach wie folgt über die Shell auszuführen

php my_script.php

MrMartin
quelle