wake-up-neo.com

Lauf PHP Task asynchron

Ich arbeite an einer etwas großen Webanwendung, und das Backend ist meistens in PHP. Es gibt mehrere Stellen im Code, an denen ich eine Aufgabe erledigen muss, aber ich möchte nicht, dass der Benutzer auf das Ergebnis wartet. Wenn Sie beispielsweise ein neues Konto erstellen, muss ich ihnen eine Begrüßungs-E-Mail senden. Wenn sie jedoch auf die Schaltfläche "Registrierung abschließen" klicken, möchte ich sie nicht warten lassen, bis die E-Mail tatsächlich gesendet wird. Ich möchte nur den Vorgang starten und dem Benutzer eine Nachricht zurückschicken.

Bisher habe ich an manchen Orten etwas benutzt, das sich wie ein Hack mit exec () anfühlt. Grundsätzlich Dinge zu tun wie:

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

Was scheint zu funktionieren, aber ich frage mich, ob es einen besseren Weg gibt. Ich überlege mir, ein System zu schreiben, das Aufgaben in einer MySQL-Tabelle in eine Warteschlange stellt, und ein separates PHP -Skript, das diese Tabelle einmal pro Sekunde abfragt und alle neuen gefundenen Aufgaben ausführt. Dies hätte auch den Vorteil, dass ich die Aufgaben in Zukunft auf mehrere Arbeitsmaschinen aufteilen könnte, wenn es nötig wäre.

Erneue ich das Rad neu? Gibt es eine bessere Lösung als der exec () - Hack oder die MySQL-Warteschlange?

131
davr

Ich habe die Warteschlangenmethode verwendet, und es funktioniert gut, da Sie die Verarbeitung verschieben können, bis die Serverlast leer ist. Dadurch können Sie Ihre Last effektiv verwalten, wenn Sie "Aufgaben, die nicht dringend sind" problemlos partitionieren können.

Es ist nicht zu schwierig, selbst zu rollen, hier sind ein paar andere Optionen, die Sie ausprobieren sollten:

  • GearMan - Diese Antwort wurde 2009 geschrieben, und seitdem scheint GearMan eine beliebte Option zu sein, siehe Kommentare unten.
  • ActiveMQ wenn Sie eine vollständige Open-Source-Nachrichtenwarteschlange wünschen. 
  • ZeroMQ - Dies ist eine ziemlich coole Socketbibliothek, die das Schreiben von verteiltem Code erleichtert, ohne sich zu sehr um die Socketprogrammierung selbst kümmern zu müssen. Sie können es für die Nachrichtenwarteschlange auf einem einzelnen Host verwenden. Sie müssen lediglich Ihre webapp in eine Warteschlange verschieben, die eine fortlaufende Konsolenanwendung bei der nächsten geeigneten Gelegenheit in Anspruch nehmen würde
  • beanstalkd - fand diese nur beim Schreiben dieser Antwort, sieht aber interessant aus
  • dropr ist ein auf PHP basierendes Nachrichtenwarteschlangenprojekt, das jedoch seit September 2010 nicht aktiv gewartet wurde
  • php-enqueue ist ein kürzlich (2017) gewarteter Wrapper für eine Vielzahl von Warteschlangensystemen
  • Zum Schluss noch ein Blogbeitrag über die Verwendung von memcached für die Warteschlange von Nachrichten

Eine andere, vielleicht einfachere Vorgehensweise ist die Verwendung von ignore_user_abort -. Sobald Sie die Seite an den Benutzer gesendet haben, können Sie die endgültige Verarbeitung ohne Angst vor einer vorzeitigen Beendigung durchführen. Dies hat jedoch den Effekt, dass die Seite verlängert wird Laden aus der Benutzersicht.

76
Paul Dixon

Wenn Sie nur eine oder mehrere HTTP-Anforderungen ausführen möchten, ohne auf die Antwort warten zu müssen, gibt es auch eine einfache PHP -Lösung.

Im aufrufenden Skript:

$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

In der aufgerufenen script.php können Sie diese PHP - Funktionen in den ersten Zeilen aufrufen:

ignore_user_abort(true);
set_time_limit(0);

Dadurch wird das Skript ohne Zeitlimit weiter ausgeführt, wenn die HTTP-Verbindung geschlossen wird.

20
Markus

Eine andere Möglichkeit, Prozesse zu verzweigen, ist das Aufrollen. Sie können Ihre internen Aufgaben als Webservice einrichten. Zum Beispiel:

Dann rufen Sie in Ihrem Benutzer, auf den Skripts zugreifen, den Dienst auf:

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

Ihr Dienst kann die Warteschlange von Aufgaben mit mysql verfolgen, oder was auch immer Ihnen gefällt: Der Dienst ist alles innerhalb des Dienstes und Ihr Skript verbraucht nur URLs. Dadurch können Sie den Dienst bei Bedarf auf einen anderen Computer/Server verschieben (dh leicht skalierbar).

Durch das Hinzufügen einer HTTP-Autorisierung oder eines benutzerdefinierten Autorisierungsschemas (wie z. B. die Web-Services von Amazon) können Sie Ihre Aufgaben für andere Personen/Services (wenn Sie möchten) verwenden, und Sie können es weiterführen und einen Überwachungsdienst hinzufügen, um den Überblick zu behalten Warteschlangen- und Taskstatus.

Es ist etwas Einrichtungsarbeit erforderlich, aber es gibt viele Vorteile.

17
rojoca

Ich habe Beanstalkd für ein Projekt verwendet und habe es wieder geplant. Ich habe festgestellt, dass dies ein hervorragender Weg ist, um asynchrone Prozesse auszuführen. 

Ein paar Dinge, die ich damit gemacht habe, sind:

  • Bildgröße ändern - und mit einer leicht geladenen Warteschlange, die an ein CLI-basiertes PHP -Skript übergeben wurde, funktionierte die Größenänderung großer (2 MB +) Bilder gut, aber der Versuch, die gleichen Bilder in einer mod_php-Instanz zu skalieren, lief regelmäßig in den Arbeitsspeicher. Speicherplatzprobleme (Ich beschränkte den PHP -Prozess auf 32 MB, und die Größenänderung nahm mehr als das in Anspruch.)
  • prüfungen in der nahen Zukunft - beanstalkd hat Verzögerungen zur Verfügung (diesen Job erst nach X Sekunden ausführen) - so kann ich 5 oder 10 Prüfungen für ein Ereignis auslösen, etwas später

Ich habe ein auf Zend-Framework basierendes System geschrieben, um eine 'Nice'-URL zu decodieren, um zum Beispiel die Größe eines Images zu ändern, würde es QueueTask('/image/resize/filename/example.jpg') heißen. Die URL wurde zuerst in ein Array (Modul, Controller, Aktion, Parameter) dekodiert und anschließend in JSON konvertiert, um sie in die Warteschlange selbst einzuspeisen.

Ein lang laufendes cli-Skript hat dann den Job aus der Warteschlange geholt, ausgeführt (über Zend_Router_Simple) und, falls erforderlich, Informationen in memcached für die Website PHP abgelegt, um sie nach Bedarf abholen zu können.

Eine der Falten, die ich auch eingefügt habe, war, dass das cli-script vor dem Neustart nur für 50 Loops ausgeführt wurde. Wenn es jedoch wie geplant neu starten soll, würde dies sofort geschehen (über ein Bash-Script ausgeführt). Wenn es ein Problem gab und ich exit(0) (den Standardwert für exit; oder die();) verwendet habe, würde es zunächst einige Sekunden anhalten.

7
Alister Bulman

Wenn es nur eine Frage der Bereitstellung teurer Aufgaben ist, im Falle von php-fpm, warum nicht die Funktion fastcgi_finish_request() verwenden? 

Diese Funktion löscht alle Antwortdaten an den Client und beendet die Anforderung. Dadurch können zeitaufwändige Aufgaben ausgeführt werden, ohne dass die Verbindung zum Client geöffnet bleibt.

Sie verwenden Asynchronität auf diese Weise nicht wirklich:

  1. Machen Sie zuerst Ihren Hauptcode. 
  2. Führen Sie fastcgi_finish_request() aus.
  3. Mache alle schweren Sachen.

Wieder ist php-fpm nötig.

7

Hier ist eine einfache Klasse, die ich für meine Webanwendung codiert habe. Es erlaubt, PHP Skripts und andere Skripte zu fälschen. Funktioniert unter UNIX und 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

Dies ist die gleiche Methode, die ich seit einigen Jahren verwende, und ich habe nichts Besseres gesehen oder gefunden. Wie die Leute gesagt haben, ist PHP ein einzelner Thread, es gibt also nicht mehr viel, was Sie tun können.

Ich habe tatsächlich eine zusätzliche Ebene hinzugefügt, und die Prozess-ID wird abgerufen und gespeichert. Dadurch kann ich auf eine andere Seite umleiten und den Benutzer auf dieser Seite sitzen lassen. Mit AJAX können Sie überprüfen, ob der Prozess abgeschlossen ist (Prozess-ID ist nicht mehr vorhanden). Dies ist nützlich in Fällen, in denen die Länge des Skripts zu einem Timeout des Browsers führen würde, der Benutzer jedoch warten muss, bis das Skript abgeschlossen ist, bevor der nächste Schritt ausgeführt wird. (In meinem Fall verarbeitete es große Zip-Dateien mit CSV-ähnlichen Dateien, die der Datenbank bis zu 30.000 Datensätze hinzufügen, woraufhin der Benutzer einige Informationen bestätigen muss.)

Ich habe auch einen ähnlichen Prozess zur Berichterstellung verwendet. Ich bin nicht sicher, ob ich "Hintergrundverarbeitung" für etwas wie eine E-Mail verwenden würde, es sei denn, es besteht ein echtes Problem mit einem langsamen SMTP. Stattdessen kann ich eine Tabelle als Warteschlange verwenden und dann einen Prozess ausführen, der jede Minute ausgeführt wird, um die E-Mails in der Warteschlange zu senden. Sie müssen sich darauf verlassen, dass Sie zweimal E-Mails senden oder ähnliche Probleme haben. Ich würde einen ähnlichen Warteschlangenprozess auch für andere Aufgaben in Betracht ziehen.

4
Darryl Hein

PHP HAT Multithreading, es ist einfach nicht standardmäßig aktiviert, es gibt eine Erweiterung namens pthreads, die genau das tut. (Thread Safe) Links:

Beispiele

Ein weiteres Tutorial

pthreads PECL-Erweiterung

3
Omar A. Shaban

Es ist eine gute Idee, cURL wie von rojoca vorgeschlagen zu verwenden.

Hier ist ein Beispiel. Sie können text.txt überwachen, während das Skript im Hintergrund ausgeführt wird:

<?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

Leider hat PHP keine nativen Threading-Funktionen. Ich denke, in diesem Fall haben Sie keine andere Wahl, als einen benutzerdefinierten Code zu verwenden, um das zu tun, was Sie tun möchten.

Wenn Sie im Internet nach PHP Threading-Sachen suchen, haben einige Leute Möglichkeiten gefunden, Threads auf PHP zu simulieren.

1
Peter D

Wenn Sie den Content-Length-HTTP-Header in Ihrer Antwort "Danke für die Registrierung" festlegen, sollte der Browser die Verbindung schließen, nachdem die angegebene Anzahl von Bytes empfangen wurde. Dadurch wird der serverseitige Prozess ausgeführt (vorausgesetzt, ignore_user_abort ist festgelegt), sodass die Arbeit beendet werden kann, ohne dass der Endbenutzer warten muss.

Natürlich müssen Sie vor dem Rendern der Header die Größe Ihres Antwortinhalts berechnen. Dies ist jedoch für kurze Antworten (Ausgabe in einen String schreiben, Aufruf strlen (), call header (), Renderstring) ziemlich einfach.

Dieser Ansatz hat den Vorteil, dass not Sie dazu zwingt, eine "Front-End" -Warteschlange zu verwalten, und obwohl Sie möglicherweise einige Arbeiten am Back-End durchführen müssen, um zu verhindern, dass laufende untergeordnete HTTP-Prozesse aufeinander treten, ist dies etwas, was Sie tun musste sowieso schon getan werden.

1
Peter

Wenn Sie nicht die volle ActiveMQ wollen, empfehle ich RabbitMQ . RabbitMQ ist ein leichtes Messaging, das den AMQP-Standard verwendet.

Ich empfehle einen Blick in php-amqplib - eine beliebte AMQP-Client-Bibliothek, um auf AMQP-basierte Nachrichtenbroker zuzugreifen.

1
phpPhil

ich denke, Sie sollten diese Technik ausprobieren, es wird hilfreich sein, so viele Seiten aufzurufen, wie Sie möchten, dass alle Seiten auf einmal unabhängig ausgeführt werden, ohne auf jede Antwort 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

0
Hassan Saeed

Das Erstellen neuer Prozesse auf dem Server mit exec() oder direkt auf einem anderen Server mit curl lässt sich nicht so gut skalieren. Wenn Sie sich für exec entscheiden, füllen Sie Ihren Server mit langwierigen Prozessen auf, die von anderen Servern ohne Webzugriff verarbeitet werden können und curl bindet einen anderen Server an, es sei denn, Sie bauen eine Art Lastverteilung ein.

Ich habe Gearman in einigen Situationen verwendet und finde es für diese Art von Anwendungsfall besser. Ich kann einen einzigen Job-Warteschlangenserver verwenden, um im Wesentlichen die Warteschlangen aller Jobs, die vom Server erledigt werden müssen, sowie die Hochlauf-Worker-Server zu handhaben, von denen jeder beliebig viele Instanzen des Worker-Prozesses ausführen und die Anzahl der Server erhöhen kann Worker-Server nach Bedarf und drehen sie herunter, wenn sie nicht benötigt werden. Außerdem kann ich die Arbeitsprozesse bei Bedarf vollständig herunterfahren und die Jobs in die Warteschlange stellen, bis die Arbeiter wieder online sind.

0