Traitements parallèles grâce a Gearman

Je me suis déjà frotté au multitâche avec PHP, et il faut dire, avec un certain recul et pas mal de temps de tests, que je suis plutot déçu par la stabilité du système mis en place. J’avais développé ça sur une base de forks de process. Ça marchait bien jusqu’à un certain point, lorsque le serveur était un peu chargé (à peine plus que d’habitude) certain de mes process plantaient aléatoirement, jamais le même process. Peut-être que j’abusais un peu, mais bon j’en ai besoin de mes 200 process moi.

Dans ma quête de stabilité, je me suis dit qu’utiliser Gearman serait une bonne idée, il est censé être simple, scalable, stable, et tout et tout.

Voici la structure mise en place :

  • Un client, qui en fait est un script PHP, qui envoie envoie des jobs (des tâches) au serveur gearman.
  • Un serveur gearman qui s’occupe d’empiler les jobs et de les distribuer au workers.
  • Plusieurs workers qui eux aussi sont des scripts PHP. Ils reçoivent les jobs que le client à envoyé au serveur. Une fois le job terminé, le worker qui avait le job en charge retourne le résultat au client via le serveur.

On verra pas la suite que le client n’est pas obligé d’attendre le résultat de ses jobs. Dans ce cas le client ne sait pas si le job s’est bien passé et il ne connaitra pas non plus le résultat du job.

Dans mon cas je n’ai pas besoin du résultat, j’ai juste besoin d’empiler des jobs afin de les lancer en parallèle et ce n’est pas la peine d’attendre que chaque job ait fini son travail.

Installation

L’installation décrite ici est pour Linux, pour Ubuntu plus exactement, voici la démarche a suivre :

Il faut ajouter ce dépôt à vos sources dans le fichier /etc/apt/sources.list :  https://launchpad.net/~gearman-developers/+archive/ppa

On met à jour puis on installe gearman avec apt-get :

apt-get update
apt-get upgrade
apt-get install gearman-job-server

On installe l’extension pour PHP

apt-get install php5-dev
apt-get install libgearman-dev
wget http://pecl.php.net/get/gearman-0.8.0.tgz
tar xzf gearman-X.Y.tgz
cd gearman-X.Y
phpize
./configure
make
make install

Si tout se passe bien, on a juste à a jouter l’extension au fichier php.ini :

extension="gearman.so"

Et on n’oublie pas de recharger son serveur web pour prendre en compte la nouvelle extension.

Pour vérifier que tout est bien en place, on peut utiliser cette commande :

print gearman_version();

Le client

Voila le code du client utilisé :

$client= new GearmanClient();
$client->addServer("127.0.0.1", 4730);
 
$task_id = 0;
while($image_id = get_new_image())
{
    $params = array(
        "db_config" => $db_config[$options['environment']],
        "image_id" = $image_id,
    );
 
    $client->addTaskBackground("process_image", serialize($params), null, "T".$task_id++);
}
$client->runTasks();
exit(0);

On commence par créer un objet GearmanClient, il se connectera à notre serveur grâce au addServer.

Dans cet exemple, on empile des traitements sur des images, c’est la fonction process_image qui sera appelée par chaque job.

On crée aussi un tableau de paramètres qui sera sérialisé avant d’être envoyé au job. Les paramètres que j’ai mis ici sont juste des exemples, on peut y mettre ce qu’on veut pourvu que ce qu’on y met soit sérialisable.

Le runTasks() achève le travail et indique au serveur de lancer les tâches.

Les workers

Pour gérer les workers, j’ai fait un petit script qui en lance un certain nombre à coup de fork().

echo "Starting {$nb_workers} workers : ";
for($i=0; $i<$nb_workers; $i++)
{
     $pid = pcntl_fork();
      if($pid == -1)
      {
          echo "Fork error !\n";
      }
      else if($pid)
      {
         $pids[] = $pid;
         echo "Worker pid : {$pid}\n";
      }
      else
      {
         call_user_func_array("run", array());
         exit(0);
     }
 }
 
 [...]
 
 function run()
 {
     $worker= new GearmanWorker();
     $worker->addServer("127.0.0.1", 4730);
     $worker->addFunction("process_image", "do_work");
     while($worker->work());
}
 
function do_work($job)
{
    $params = unserialize($job->workload());    
 
    // Code du traitement de l'image
}

A vous de l’adapter selon vos besoins, il n’y a la rien de bien compliqué.

Conclusion

Finalement, la mise en place d’un tel système s’avère assez simple et rapide. Il ne nous aura pas fallu plus d’une journée pour réaliser l’ensemble alors que nous n’avions jamais touché à Gearman auparavant.

La stabilité à l’air d’être au rendez-vous, nous n’avons eu aucun plantage de job ou autre depuis sa mise en tests et nous lançons environs 700 jobs par jours repartis sur 10 workers. C’est pas énorme mais on a prévu de charger un peu plus la bête dans un second temps.

Pour plus d’informations, voici quelques liens :

Read 10 comments

  1. Ca a l’air pas mal du tout comme systeme, par contre dans ton code, comment tu fais le controle que la tache s’est bien deroulée et que donc toutes les images ont bien ete traitées?

  2. Dans mon cas je n’ai pas besoin d’avoir le retour du traitement, c’est pour ça que j’utilise addTaskBackground() dans le client.

    Pour attendre le résultat d’un worker, il faut utiliser addTask() couplé à un setCompleteCallback() :

    $client->addTask("process_image", serialize($params), null, "T".$task_id++); 
    $client->setCompleteCallback("complete"); 
    $gmclient->runTasks(); 
    
    function complete($task) 
    { 
      print "Terminé : " . $task->unique() . ", " . $task->data() ; 
    }

    Voir la doc php c’est bien expliqué.

  3. Pingback: À lire cette semaine 10 | www.freva.net

  4. Salut,
    Nous aussi on s’amuse avec Gearman en ce moment. Personnellement, j’adore même s’il est un peu difficile à installer sur CentOS (j’ai dû compiler Boost et Gearman à partir des sources).

    Ca m’intéresserait de lire la suite de votre test, à savoir si votre architecture tient le coup. De notre côté, on n’aura pas beaucoup de workers, peut-être 15 par serveurs et 3 serveurs de workers (donc 45 workers) mais ça sera des taches qui durent longtemps.

    Je posterai un truc sur mon blog quand on aura mis tout ça en place.
    Bonne continuation.

  5. Bonjour,
    merci pour cette intro qui m’a bien aidé dans la mise en place d’un serveur Gearman.
    Mon problème était de pouvoir envoyer vers Gearman un certain nombre de tâches, soit parce qu’elles consommaient beaucoup de ressources, soit parce qu’elles dépendaient de serveurs extérieurs (notamment des API de réseaux sociaux). Par exemple :

    $user->synchronize();
    

    ou

    $mail->send();
    

    Voici donc le code que j’ai utilisé. Si ça peut aider ou inspirer quelqu’un, tant mieux.
    Coté Client :
    un wrapper :

    class bzkObjectWrapperException extends Exception { }
    class bzkObjectWrapper {
            public $object; //(object)  un objet
            public $method; // (string) le nom de la méthose
            public $args; // (array) les arguments de la méthode, dans un tableau
        
        public function __construct($object, $method, $args = array() )  { 
            global $_display_lang;
            $this->object = $object;
            $this->method = $method;
            $this->args = $args;
        }
        
        public function execute() {
            $object = $this->object;
            $method = $this->method;
            $args   = is_array($this->args) ? $this->args : array($this->args);
            $classname = get_class($object);
            try {
                $reflectionMethod = new ReflectionMethod($classname, $method);
                return $reflectionMethod->invokeArgs($object, $args);    
            } catch(bzkObjectWrapperException $e){
                return false;
            }
        }
    }
    

    que j’appelle ainsi

    // envoie la tache  $user->synchronize()  au serveur Gearman 
    $wrapper = new bzkObjectWrapper($sn,'syncContacts');
    $client->addTaskBackground("do_method", serialize($wrapper), null, "T".$task_id++);
    

    et coté Worker

    $worker->addFunction("do_method", "do_method");
    
    function do_method($job) {
        $wrapper = unserialize( $job->workload() ); 
        return $wrapper->execute();
    }
    
  6. desolé, je n’ai pas pu mettre en forme. je n’ai pas vu si tu as un éditeur HTML sur ce blog, et les balises sont filtrées. Si tu peux le faire, ce sera toujours plus agréable à voir.

Laisser un commentaire