Programmer un démon en PHP

Un démon (ou daemon en anglais) est un programme fonctionnant en permanence en arrière plan et qui n’est pas contrôlé par l’utilisateur.

PHP étant un langage de script, on ne peut pas lancer des tâches de fond depuis les requêtes faites sur le serveur http. Quand un processus nécessite des traitements nombreux et n’a pas besoin, ou ne peux pas, être réalisé immédiatement, on peut facilement renseigner cette tâche dans une pile (dans une base de donnée ou un fichier) qu’un démon se chargera de réaliser.

Voyons comment réaliser ce démon.

L’avantage d’un démon par rapport à un script lancé régulièrement via une crontab est que ce dernier ne peut pas être lancé plus d’une fois par minute et que l’on est certain qu’un script démoniaque s’éxecute de manière unique. A l’inverse, des scripts lancés en crontab mettant beaucoup de temps à s’exécuter peuvent se recouvrir. (certes, on peut imaginer poser des verrous : au début du script je pose un verrou – j’écris un fichier .lck quelque part par exemple – et à la fin je l’efface ; mais imaginer si notre script meurt prématurément … On peut également contrôler la durée maximum d’exécution du script mais c’est une contrainte supplémentaire).

Principe du démon

Dans un démon on trouvera toujours un code de ce genre :

ini_set ('max_execution_time', 0); // cette valeur peut être positionnée dans php.ini
while (true)
{

  // votre code démoniaque ici
  myClass::workHardPlease();
  // repos du démon
  sleep(2);

}

On voit ici que le programme une fois démarré va tourner indéfiniment jusqu’à :

  • atteindre une limite de PHP comme max_execution_time (dans l’exemple plus haut on est malin, on fixe cette valeur à l’infini) ou memory_limit,
  • être « tué » par un autre processus,
  • faire planter le serveur (gasp !).

Est ce bien raisonnable ?

On lancerait ce démon avec la commande nohup par exemple :

nohup php mon_demon.php &

Cela posent plusieurs problèmes :

  1. si mon code démoniaque ne libère pas bien sa mémoire je risque de saturer ma machine (c’est d’autant plus vrai si le code utilisé embarque des librairies externes, peu d’entre elles ont été testées dans un tel contexte ; car habituellement un script PHP libère automatiquement toute sa mémoire en se terminant),
  2. si mon code démoniaque nécessite beaucoup de ressources, je vais saturer mes ressources matérielles, certes je lui laisse un peu de repos entre deux itérations mais il a une priorité d’exécution similaire à d’autres processus,
  3. si je mets à jour le code source de mon démon je dois tuer le processus à la main, c’est pénible.

Comment faire ?

L’astuce consiste à tuer régulièrement votre démon et à s’assurer qu’il soit immédiatement relancer en le déclarant comme un service de votre système linux.

Premièrement, on va simplement installer un petit compteur qui terminera notre démon au bout d’un certain temps. Voilà a quoi va ressembler notre code au final :

ini_set ('max_execution_time', 1200);
$start_time = time();
while (true)
{

  // votre code démoniaque ici
  myClass::workHardPlease();
  // repos du démon
  sleep(2);

  if ((time() - $start_time) > 600) {
    exit(0);
  }
}

J’ai mis une limite maximum d’exécution à 20 minutes, c’est le temps maximum que je donne pour une exécution de myCLass::workHardPlease(). Toutefois, si j’arrive à la fin de ma boucle démoniaque, je quitte le programme d’un magnifique exit(0) correspondant à une fin « normale ». Vous allez me dire, certes quitter le script va m’assurer de ne pas saturer la mémoire mais bon … il ne tourne plus là. Et c’est là qu’est l’astuce !

Deuxièmement, ce démon va être lancé comme un service de votre machine Linux en le référençant dans le fichier /etc/inittab. Voici un exemple que je décris plus bas (à ajouter à la fin du fichier) :

cj:5:respawn:nice -n 15 su  www-data -c '/usr/bin/php /home/user/mon_demon.php'

Il faut naturellement être connecté en root pour éditer ce fichier.

Le premier argument correspond à un identifiant unique sur deux lettres. Le second correspond au mode démarrage choisi. Ce chiffre est variable selon votre distribution, mais pour Debian et RedHat, 5 correspond au niveau normal. Le troisième, respawn, indique que le processus sera relancé, a chaque fois que celui ci se terminera (vous voyez ou je veux en venir ….). Enfin le quatrième argument est la commande en elle même. Celle ci est lancée via la commande nice et su. Su permet de ne pas lancer notre démon en root. Nice permet d’ajuster la priorité, ainsi notre démon n’accaparera pas toutes les ressources de notre serveur.

Bingo !

Le système cumule tous les avantages :

  • toutes les dix minutes, si notre script gère mal sa mémoire, le garbage collector de PHP le fera à la fin du script,
  • si on met à jour le code source, les nouveautés seront prises en compte sous dix minutes,
  • notre script s’exécute avec un faible niveau de priorité, il ne perturbera pas le fonctionnement du serveur et s’exécutera à vitesse maximum quand celui ci sera disponible,
  • Au pire, le script a une durée maximum de vingt minutes.

Que pensez vous de ce système ?

Publicités

9 réflexions au sujet de « Programmer un démon en PHP »

  1. NiKo

    Je comprends pas bien le coup du sleep(2) mais sinon ça m’a l’air pas mal 🙂

    On pourrait aussi interroger à chaque itération la charge mémoire consommée par le script via la commande « memory_get_usage » et n’arrêter la boucle qu’une fois une limite définie en amont atteinte 🙂

    Enfin, Xavier a trouvé des solutions assez sexy dans son plugin sfJobQueue pour Symfony, faudrait lui demander des billes…

    Répondre
  2. olivier

    sleep est une variable d’ajustement de « l’agressivité » de ton démon. On peut réduire ou supprimer cette commande, c’est à ajuster selon les besoins.

    Je n’aime pas utiliser memory_get_usage. La fonction a des comportements étrange sur certains systèmes (notamment windows) et je préfère que mon script génère une belle fatal error quand il atteint cette limite haute 😉 car je qualifie ça en comportement anormal. Mais sinon c’est une bonne idée. (après c’est les goûts et les couleurs)

    Le plugin de Xavier sfJobQueuePlugin me parait être super et faire vraiment le café ! (je ne l’ai pas encore testé). remember, il est issue d’un vieux projet sur lequel nous avons travaillé tous les trois avec Tristan :-). Le seul soucis de ce plugin est que je crois, il manque la fonction qui permet de quitter régulièrement ; pour cela il est encore mal adapté, selon moi, à une utilisation démoniaque !

    Répondre
  3. Esteban

    public function startDeamon() {
    echo ‘restart the deamon’;
    $i = 0;

    while( TRUE )
    {
    $pidfile = ‘/var/run’ . « /jobs- » . $i . « .pid »;
    if( !file_exists( $pidfile ) )
    break;
    $i++;
    }

    $runpath = __DIR__ . ‘/./../bin’;
    $runuser = ‘root’;

    return 1-exec( « /sbin/start-stop-daemon –start –make-pidfile –pidfile {$pidfile} –chdir {$runpath} –background –chuid root –startas /usr/bin/php5 — {$runpath}/deamon.php daemon » );

    }

    public function run()
    {
    /**
    * Iterate while the memory used is less than 10meg
    */
    while ( memory_get_usage() startDaemon();
    }

    Voila c’est ca la vrai approche pour faire un deamon

    Répondre
  4. Julien

    Merci Olivier et Esteban for the update!

    J’ai un PHP daemon (Gearman Worker) que je dois redemarrer regulierement. Ce worker load un boostrap d’une application symfony, mais le code ne fonctionne plus apres 15min. D’où mon besoin de redemarrer le deamon. (Le worker vit sur un autre serveur que le serveur Gearman).

    I tried:

    start-stop-daemon -start -make-pidfile -pifile /var/run/test1.pid -chdir /usr/bin/ -background -chuid root -startas /usr/bin/php5 /mnt/www/backend/batch/gearman/notify_worker.php –daemon

    And received :
    start-stop-daemon: signal value must be numeric or name of signal (KILL, INT, …)

    Avez vous une idea pour corriger cette commande?
    Le fait que le code Gearman Worker ne marche pas dans la durée, est ce un comportement normal?

    Répondre
  5. Julien

    Désolé, J’ai oublié d’utiliser — dans la ligne de commande.

    Mais le fait que le code du Daemin ne marche pas dans la durée, est ce un comportement normal?

    Répondre

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

w

Connexion à %s