Mehrere Scripte via Cronjob parallel aufrufen
Heute ein kleines Problemchen mit Cronjobs und Parallelität. Normalerweise empfehle ich Gearman wenn es darum geht, mehrere Scripte im Hintergrund laufen zu lassen, aber nehmen wir an dass wir es mit normalen Cronjobs ohne Gearman lösen wollen.
Wir haben also das Script work.php. Wir möchten alle 5 Minuten die Datenbank prüfen ob es Arbeit gibt, und wenn dem so ist, dann soll die Arbeit erledigt werden. Das geht relativ einfach mit einem Cronjob
*/5 * * * * /usr/bin/php /data/work.php
und in der work.php findet dann die Datenbankabfrage statt. Wenn X Ergebnisse in der Datenbank gefunden werden, wird eine Schleife X mal durchlaufen um alles abzuarbeiten. So weit so gut.
Nun sei die eigentliche Arbeit aber relativ zeitaufwändig, sodass ein Schleifendurchlauf 2 Minuten dauert, und bei 5 Aufträgen dauert es also 10 Minuten (wir arbeiten ja seriell in einer Schleife), der Aufruf von work.php überlappt und wir bekommen ein Problem. Angenommen die Aufgabe ist parallelisierbar, d.h. wenn man alle 5 Aufgaben zeitgleich starten würde gäbe es keine Probleme, und die ganze Arbeit wäre nach 2 Minuten erledigt. Wir könnte man soetwas einfach realisieren?
Wir teilen einfach das Script in 2 Teile. Das erste Script dispatcher.php befragt die Datenbank, und startet dann weitere PHP Prozesse parallel, die die eigentliche Arbeit erledigen. Wenn nur 2 Aufgaben anstehen werden 2 Prozesse gestartet, bei 15 Aufgaben sind es 15 Prozesse. Der Cronjob sähe so aus:
*/5 * * * * /usr/bin/php /data/dispatcher.php
Der Dispatcher:
<?php // connect to database $dbhandle = mysql_connect('mysqlserver', 'username', 'pass'); $db = mysql_select_db('App1', $dbhandle); // get work $result = mysql_query('SELECT id, data FROM work'); while ($row = mysql_fetch_array($result)) { $param = escapeshellarg($row['data']); exec('/usr/bin/php /data/work.php '. $param .' >> /var/log/worker.out &'); mysql_query("DELETE FROM work WHERE id=".$row['id']); }
(Beim Schreiben dieses Quelltextes ist mir aufgefallen dass ich schon lange keine direkten mysql_* Funktionen mehr benutzt habe, Zend Framework sei Dank…)
Man beachte das angehängte &, das den Befehl gibt, in den Hintergrund zu verschwinden. Und die work.php nimmt einfach den ihr übergebenen Parameter und erledigt die Arbeit:
<?php $data = $argv[1]; // start to work here echo $data."\n"; sleep(10); echo $data."\n";
In der Logdatei sieht man dass die fünf work.php parallel laufen:
data1 data2 data3 data4 data5 data1 data2 data3 data4 data5
Man könnte auch Kindprozesse forken mit den pcntl_* Funktionen, aber die sind nicht überall verfügbar. Falls man übrigens vorher keine Datenbank befragen muss und weiß, was und wieviele Arbeiten parallel erledigt werden sollen, kann man natürlich auch einfach X Crontab-Einträge machen. So in der Art:
*/5 * * * * /usr/bin/php /data/work.php 1 */5 * * * * /usr/bin/php /data/work.php 2 */5 * * * * /usr/bin/php /data/work.php 3 */5 * * * * /usr/bin/php /data/work.php 4 */5 * * * * /usr/bin/php /data/work.php 5
oder aber ein Wrapper-Script, dann hat man nur einen Crontab-Eintrag:
*/5 * * * * /usr/bin/php /data/wrapper.php
und in diesem Fall ein PHP Script, das die Prozesse startet. Es könnte natürlich auch ein Shellscript sein.
<?php exec("/usr/bin/php /data/work.php 1 >> /var/log/php.out &"); exec("/usr/bin/php /data/work.php 2 >> /var/log/php.out &"); exec("/usr/bin/php /data/work.php 3 >> /var/log/php.out &"); exec("/usr/bin/php /data/work.php 4 >> /var/log/php.out &"); exec("/usr/bin/php /data/work.php 5 >> /var/log/php.out &");
Es gibt also viele Wege ans Ziel, es muss für einfache Aufgaben nicht immer gleich eine Gearman-Umgebung sein.
Ich hatte auch so ein ähnliches Problem in letzter Zeit zu lösen und hab es aber auf etwas andere Weise gelöst. Du schreibst bei der ersten Lösung, sie würde die Jobs parallel abarbeiten, aber das ist ja so nicht richtig, die Jobs werden einfach im Hintergrund abgearbeitet aber ich könnte z.B. nicht zwei separate Worker gleichzeitig laufen lassen, d.h. wenn ein Lauf gestartet wurde und während dieser noch läuft der nächste gestartet wird, kann es zu Problemen kommen, weil die zwei Prozesse evtl. die selben Jobs bearbeiten.
Besser ist es da die Lösung, eine Datei anzulegen, die während einer Verarbeitung gelocked ist, damit auch andere Prozesse einen Überblick haben, ob dieser Job abgearbeitet werden darf oder noch läuft.
Just my 2 cents 😉
Dominik
19 Jul 11 at 10:47
Mann könnte die Einträge für jeden einzelnen Worker auch reservieren. Die Funktion getmypid() liefert dir zum Beispiel die PID des Prozesses der den Worker ausführt. Also reservierst du zum Beispiel 100 Einträge für den ausgeführten Worker, die er dann abzuarbeiten hat. Wenn jetzt ein anderer Prozess gestartet wird reservierst du für diesen die nächsten 100.
Christian Kaps
19 Jul 11 at 11:09
Ich meinte natürlich „Man könnte“. nicht das sich hier die Frauen benachteiligt fühlen (o;
Christian Kaps
19 Jul 11 at 11:40
Für die Probleme mit gleichzeitig ablaufenden Prozessen finde ich das Kapitel über Semaphore immer sehr erhellend: http://de.php.net/manual/de/book.sem.php
Nico
19 Jul 11 at 12:21
Ich benutze immer gerne flock.
http://php.net/manual/de/function.flock.php
Oliver
19 Jul 11 at 13:54
Ich benutze hier gern die ZendX-Lib. Da gibt es eine Klasse für CLI-Scripte. Mit dieser ist es schön möglich eben aus einem „Aufseher“ mehrere „Arbeiter“ zu erstellen und zu prüfen. Daemons lassen sich damit sehr schön realisieren.
Man muss nur aufpassen. Die aktuelle Version der Klasse ist fehlerhaft. Eine ältere von Anfang 2010 tuts aber.
Denis
20 Jul 11 at 09:48
@Oliver ja genau flock meinte ich bzw. die Instanzmethode flock aus der SplFileObject Klasse.
@Denis hab ich mir auch mal angeschaut, im Grunde ist die ZendX_Console_Process_Unix (ich denke mal die meinst du) auch nur eine Abstraktion des pcntl Moduls. Aber ist mal einen Blick wert. Was ist daran momentan fehlerhaft?
Dominik
20 Jul 11 at 10:26
[…] Mehrere Scripte via Cronjob parallel aufrufen. […]
Linkhub – Woche 29-2011 | PehBehBeh
24 Jul 11 at 20:20
[…] PCNTL funktionieren nicht unter Windows und sind schwer zu bedienen. Man kann sich eventuell mit exec() behelfen und damit weitere Prozesse starten, verliert dann jedoch die Möglichkeit, die Prozesse zu […]
Richtige Threads in PHP einfach erstellen mit pthreads | PHP Gangsta - Der PHP Blog mit Praxisbezug
13 Mrz 13 at 12:26
Hi!
Habe dein Beispiel exakt übernommen:
exec(„/usr/bin/php /data/work.php 1 >> /var/log/php.out &“);
exec(„/usr/bin/php /data/work.php 2 >> /var/log/php.out &“);
exec(„/usr/bin/php /data/work.php 3 >> /var/log/php.out &“);
Natürlich habe ich meine Script-Dateien übertragen.
Aber leider wird keine php.out angelegt und die Scripte arbeiten auch nicht. Ich habe drei kleine PHP-Scripte geschrieben, die serial funktionieren. Alle werden mit exec ausgeführt. Die sollen aber parallel arbeiten. Gibt es da irgendwas noch zu beachten. Ich arbeite im Übrigen mit Xampp.
Gruß Thor (Ja, so heiße ich wirklich!)
Thor
11 Jul 13 at 00:31
@Thor: Vielleicht hast du open_basedir aktiv, sodass du nicht aus deinem Basisverzeichnis „ausbrechen“ darfst? Prüf mal dein PHP ErrorLog. Außerdem solltest du vielleicht die Rückgabe prüfen, wie das geht steht im Manual:
http://de2.php.net/manual/en/function.exec.php
Vielleicht gibt es ein Permission-Problem, die exec() Funktion ist gesperrt oder so.
Michael Kliewe
11 Jul 13 at 10:49
Hallo Michael!
Danke für die schnelle Antwort.
Ich bin schon etwas weiter gekommen.
exec(„c:/xampp/… c:/xampp/htdocs/test.php 1 > out1.txt &“);
exec(„c:/xampp/… c:/xampp/htdocs/test.php 2 > out2.txt &“);
exec(„c:/xampp/… c:/xampp/htdocs/test.php 3 > out3.txt &“);
* (!) Leicht verkürzt dargestellt.
Wenn ich diese drei Zeilen untereinander schreibe und im Browser starte, werden die Scripte serial abgearbeitet. In jedem dieser Scripte befindet sich eine Anbindung an die Datenbank. Ich sehe dort im Logfile sofort, welches Script arbeitet. Und da ist zweifelsfrei zu erkennen, dass die Scripte nacheinander ablaufen. Es ändert auch nichts daran, wenn man statt zwei eckige Klannern „>>“ nur eine „>“ verwendet. Wenn ich drei Tabs im Browser öffne und jedes Script einzeln starte, laufen die Scripte parallel ab. Müsste nicht eigentlich ein PHP-Script, worin nur diese drei Zeilen drin sind, nach dem Start sofort beendet sein? Ich meine ja, was nämlich das Zeichen dafür wäre, dass der Interpreter nicht wartet, oder? Muss in Windows7 vielleicht was anderes ins CMD geschrieben werden? Bin jetzt 5 Stunden am Fummeln und habe bei Google leider nichts gefunden. Ein Permisson-Problem kann es – so denke ich – nicht sein, weil die Funktion exec ja ausgeführt wird! In den out.txt-Dateien steht drin, was normaler Weise im Brauser ausgeben wird. Bei mir sind es die Werte, die mittels echo, var_dump oder print_r ausgebe.
Ich würde mich sehr freuen, wenn die Anstrengungen noch zum Ziel führen würden.
Gruß Thor
Thor
11 Jul 13 at 11:24
@Thor: Sag doch sofort dass du Windows verwendest… Da funktioniert das so nicht.
Das letzte Zeichen bei dem Aufruf (&) sorgt unter Linux dafür dass der Befehl im Hintergrund abgearbeitet wird. Dann wird also ein PHP Prozess gestartet, in den Hintergrund verschoben, und dann kommt direkt der zweite exec() Aufruf.
Unter Windows funktioniert das mit dem & etwas anders, das & sorgt nicht dafür dass es im Hintergrund weiter ausgeführt wird, es bleibt im Vordergrund, und demnach wartet das exec() darauf bis das PHP-Script beendet wurde.
Wenn du nach „Windows cmd background“ googlest findest du z.B. diese Aussage:
„In case anyone has this problem in future, I finally found out the solution. The START command in Windows command line allows you to start another command window running any command; and the /B option can start the command without the extra command window, so you get similar behavior to Unix’s background processes. Look it up for more details.“
Heißt so viel wie: „start“ davor schreiben und hinten das & entfernen. Und vielleicht noch /B hinzufügen hinter das start… Mußt du mal ausprobieren, hab gerade kein Windows zur Hand.
Michael Kliewe
11 Jul 13 at 11:32
Michael, ich liebe dich! 😉
Du glaubst ja gar nicht wie froh ich bin. Dank deiner Hilfe hab ich es nun endlich geschafft.
Für die Nachwelt: (PHP5)
function cmd($cmd)
{
pclose(popen(„start /B „. $cmd, „r“));
}
cmd(„c:/xampp/php/php.exe c:/xampp/htdocs/test1.php > out1.txt“);
cmd(„c:/xampp/php/php.exe c:/xampp/htdocs/test2.php > out2.txt“);
cmd(„c:/xampp/php/php.exe c:/xampp/htdocs/test3.php > out3.txt“);
Funktioniert unter: Windows 7 64
1000 Dank!!
Gruß Thor
Thor
12 Jul 13 at 01:59
Vielleicht noch eine kurze Ergänzung für Windows-Nutzer: Wie meine Vorredner ja bereits festgestellt haben, läuft über Windows so einiges anders als bei Linux 😉
Eine Crontab-Datei, wie es sie unter Linux gibt, werden Windows-Nutzer zum Beispiel nicht finden. Wer doch auf Windows angewiesen ist, und dort ebenfalls einen Cronjob durchführen möchte, kann Windows AT, quasi die Cron-Alternative, verwenden. Ich habe das Ganze mal in einem kurzen Tutorial beschrieben.
Leider werden Windows-Nutzer damit dennoch auf einige Funktionen verzichten müssen. Zwar ist der Zugriff auf Remote-Computer echt gut gemacht, aber der minimale Abstand zwischen den Cronjobs liegt bei einer Stunde.
Tristan
18 Aug 14 at 17:58