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.