En el anterior post comenté un poco por encima en qué consiste Gearman y instalamos el daemon, libgearman y el Gearman PHP module de pecl. Es posible que también hayas visto que existe otro módulo llamado Net_Gearman en PEAR. A diferencia del anterior, éste está escrito enteramente en PHP. De todos modos, son muy similares, y en este caso, trataremos el primero, que extiende de C.

En este artículo crearemos nuestro primer cliente, un worker, los integraremos en un entorno Zend Framework y haremos unas pequeñas pruebas de envíos de mensajes.

Arrancar el Job Server

Iniciar el daemon es tan sencillo como:

  $ gearmand -d

el parámetro -d indica que el proceso se ejecutará en background. Para ver la creación en modo verbose:

  $ gearmand -vvv

Existen multitud de parámetros de configuracion que puedes consultar con -h. Con esto, tenemos un Job Server listo para recibir tareas!

Integración en Zend Framework

Es probable que en la sección de descargas de Gearman hayas visto la existencia de un GearmanManager for PHP en Github. Está muy bien y encapsula gran parte de la lógica, de manera que tan sólo te tienes que preocupar de la lógica de negocio de tus workers. De todos modos, el instalador sólo funciona en algunas distros de Linux, y si trabajas con un equipo en distintos entornos, es posible que tengas que instalarlo de forma manual.

Es por ello que nosotros basaremos gran parte de la adaptación en el proyecto Zend_Gearman de Mike Willbanks. En su blog explica de una forma excelente cómo integrarlo, y su código es muy sencillo y fácil de entender, aunque en nuestro caso haremos algún pequeño retoque.

En primer lugar, crearemos un modelo llamado GearmanManager que encapsulará la lógica de creación de clients, workers y su configuración. Asumiremos que todos los workers se almacenarán en el directorio application/workers/.

/application/models/Gearman/Manager.php

<?php
class Model_Gearman_Manager
{
    private static $_gearmanServers = null;
    private static $_stdLogger = null;

    /**
      * Retrieves the current Gearman Servers
      *
      * @return array
      */
     public static function getServers()
     {
         if (self::$_gearmanServers === null) {
             /* TODO: Aquí deberías obtener los job Servers de tu
              configuración */
             $servers = "127.0.0.1:4730";
             self::$_gearmanServers = $servers;
         }
         return self::$_gearmanServers;
     }

     /**
      * Creates a GearmanClient instance and sets the job servers
      *
      * @return GearmanClient
      */
     public static function getClient()
     {
         $gmclient= new GearmanClient();
         $servers = self::getServers();
         $gmclient->addServers($servers);

         return $gmclient;
    }

    /**
     * Creates a GearmanWorker instance
     *
     * @return GearmanWorker
     */
    public static function getWorker()
    {
        $worker = new GearmanWorker();
        $servers = self::getServers();
        $worker->addServers($servers);

        return $worker;
    }

    /**
     * Given a worker name, it checks if it can be loaded. If it's possible,
     * it creates and returns a new instance.
     *
     * @param string $workerName
     * @param string $logFile
     * @return Model_Gearman_Worker
     */
    public static function runWorker($workerName, $logFile = null)
    {
        $workerName .= 'Worker';
        $workerFile = APPLICATION_PATH . '/workers/' . $workerName . '.php';

        if (!file_exists($workerFile)) {
            throw new InvalidArgumentException(
                "El Worker no existe: {$workerFile}"
            );
        }

        require $workerFile;

        if (!class_exists($workerName)) {
            throw new InvalidArgumentException(
                "La clase {$workerName} no existe en el archivo: {$workerFile}"
            );
        }

        return new $workerName($logFile);
    }

    public static function getLogger($logFile = 'php://output')
    {
        if (self::$_stdLogger === null) {
            self::$_stdLogger = new Zend_Log();
            $writer = new Zend_Log_Writer_Stream($logFile);
            $filter = new Zend_Log_Filter_Priority(Zend_Log::DEBUG);
            $writer->addFilter($filter)
            self::$_stdLogger->addWriter($writer);
        }
        return self::$_stdLogger;
    }
}

Habrás visto que el Manager tan sólo contiene métodos estáticos para crear y configurar un Cliente, un Worker, un logger (por si queremos guardar info en un fichero) y un método llamado runWorker() que se encargará de buscar una clase llamada xWorker dentro del directorio de Workers y instanciarla. El método getServers() se encargará de recoger de tu configuración (ficheros .ini, etc) un listado de servidores que actuarán como JobServers. En este caso los ponemos inline para simplificar el código.

Creación de Workers

Es más que probable que en nuestra aplicación tengamos más de un worker que realicen tareas distintas. Es por ello que gran parte de la lógica de la aplicación que sea idéntica para todos ellos (creación del worker, registro de funciones, loop de escucha de eventos…) será encapsulada en una clase que llamaremos Worker y de la que heredarán todos los Workers. Esta clase pla obtendremos del proyecto Zend_Gearman antes mencionado, aunque le hemos hecho algún pequeño cambio para aceptar varias funciones en un mismo Worker y permitir logging en un fichero.

/application/models/Gearman/Worker.php

<?php
class Model_Gearman_Worker
{
    /**
     * Register Function
     * @var string
     */
    protected $_registerFunction;

    /**
     * Gearman Timeout
     * @var int
     */
    protected $_timeout = 60000;

    /**
     * Alloted Memory Limit in MB
     * @var int
     */
    protected $_memory = 1024;

    /**
     * Error Message
     * @var string
     */
    protected $_error = null;

    /**
     * Gearman Worker
     * @var GearmanWorker
     */
    protected $_worker;

    /**
     * Memory limit activation
     * @var bool
     */
    protected $_enableMemoryLimit = false;

    protected $_logFile = null;

    /**
     * Constructor
     * Checks for the required gearman extension,
     * fetches the bootstrap and loads in the gearman worker
     */
    public function __construct($logFile = null)
    {
        $this->initRegisterFunctions();

        // Check extension
        if (!extension_loaded('gearman')) {
            throw new RuntimeException('The PECL::gearman extension is required.');
        }

        if (!empty($logFile)) {
            $this->_logFile = $logFile;
        }

        // Creates a new Gearman Worker and set its servers
        $this->_worker = Model_Gearman_Manager::getWorker();

        // Checks the registerFunction
        if (empty($this->_registerFunction)) {
            throw new InvalidArgumentException(get_class($this)
                    . ' must implement a registerFunction');
        }

        // allow for a small memory gap:
        if ($this->_enableMemoryLimit) {
            $memoryLimit = ($this->_memory + 128) * 1024 * 1024;
            ini_set('memory_limit', $memoryLimit);
        }

        // Registers
        if (is_array($this->_registerFunction)) {
            foreach ($this->_registerFunction as $alias => $func) {
                $this->_worker->addFunction($alias, array(&$this, $func));
            }
        } else {
            $this->_worker->addFunction($this->_registerFunction, array(&$this, 'work'));
        }

        $this->_worker->setTimeout($this->_timeout);

        $this->init();

        if (!empty($this->_logFile)) {
            Model_Gearman_Manager::getLogger($this->_logFile)->log('[Worker initialized]', Zend_Log::INFO);
        }

        while (@$this->_worker->work() || $this->_worker->returnCode() == GEARMAN_TIMEOUT) {

            // if a timeout ocurrs
            if ($this->_worker->returnCode() == GEARMAN_TIMEOUT) {
                $this->timeout();
                continue;
            }

            // if an error ocurrs
            if ($this->_worker->returnCode() != GEARMAN_SUCCESS) {
                $this->setError($this->_worker->returnCode() . ': ' . $this->_worker->getErrno() . ': ' . $this->_worker->error());
                break;
            }
        }

        $this->shutdown();
    }

     /**
     * Initialization of Register functions
     *
     * @return void
     */
    protected function initRegisterFunctions()
    {

    }

    /**
     * Initialization
     *
     * @return void
     */
    protected function init()
    {

    }

    /**
     * Handle Timeout
     *
     * @return void
     */
    protected function timeout()
    {
    }

    /**
     * Handle Shutdown
     *
     * @return void
     */
    protected function shutdown()
    {

    }

    /**
     * Set Error Message
     *
     * @param string $error
     * @return void
     */
    public function setError($error)
    {
        $this->_error = $error;
    }

    /**
     * Get Error Message
     *
     * @return string|null
     */
    public function getError()
    {
        return $this->_error;
    }
}

En el código verás que gran parte de la lógica está en el constructor de la clase. Éste carga un Worker configurado de nuestro GearmanManager, comprueba que el módulo pecl esté instalado, setea un límite de memoria (si lo especificas en el atributo $this->_memory, y registra un array de funciones ($this->_registerFunction) que son las que deberemos escribir posteriormente en nuestros Workers.

El constructor también setea un tiempo límite de espera ($this->_timeout), se registra su instanciación en el log y se lanza un bucle permanente a la espera de trabajos a realizar. Con todo esto, podemos crear un Worker que prácticamente contenga nuestra lógica de negocio (las funciones registradas), abstrayendo la parte común a todos ellos.

En este ejemplo, crearemos un Worker llamado “testWorker” (muy original, si :-P ) en el directiorio /application/workers/ con dos métodos registrados: asyncMessage y syncMessage, que permitirán la ejecución de tareas asíncronas y síncronas respectivamente.

/application/Workers/testWorker.php

<?php
class testWorker extends Model_Gearman_Worker
{
    protected $_timeout = 10000; // 10 seconds

    protected function initRegisterFunctions()
    {
        $this->_registerFunction = array(
            'asyncMessage' => '_asyncMessage',
            'syncMessage' => '_syncMessage'
        );
    }

    public function _asyncMessage($job)
    {
        $data = unserialize($job->workload());

        if (!empty($this->_logFile)) {
            Model_Gearman_Manager::getLogger($this->_logFile)->log(
                "[asyncMessage message='{$data['message']}' ]", Zend_Log::INFO
            );
        }

        // Do the work!
        echo $data['message'];
    }

    public function _syncMessage($job)
    {
        $data = unserialize($job->workload());

        if (!empty($this->_logFile)) {
            Model_Gearman_Manager::getLogger($this->_logFile)->log(
                "[asyncMessage message='{$data['message']}' ]", Zend_Log::INFO
            );
        }

        // Do the work!
        echo $data['message'];
        return strrev($data['message']);
    }
}

En la función initRegisterFunctions() se definen a qué funciones van registradas los alias usados en el envío de mensajes. Los métodos que hemos creado recibirán una instancia de GearmanJob de la cual podremos obtener la información recibida lista para procesar. En el ejemplo hemos logado la información recibida, y en el caso síncrono, retornamos el mensaje invertido para que el cliente lo reciba.

Los métodos registrados deberán ser públicos para que el Worker los pueda ejecutar. De este modo, crear un nuevo Worker tan sólo representa la creación de las propias tareas que va a realizar, registrar dichos métodos y definir una configuración opcional mediante parámetros de la clase. También hay funciones que puedes sobrecargar para el caso de suceder un tiempo de espera excedido, inicialización de clases u otras características que puedes consultar echándole una ojeada a la clase.

Enviar mensajes (Client)

Para este ejemplo crearemos un pequeño controlador de ejemplo que implementará dos acciones, una para enviar un mensaje síncrono y otra para uno asíncrono:

/application/controllers/TestGearman.php

<?php
class GearmanTestController extends Zend_Controller_Action
{
    /**
     * Initialization
     */
    public function init()
    {
        // Disables the render of any Layout and View
        $this->_helper->layout()->disableLayout();
        $this->_helper->viewRenderer->setNoRender(true);
    }

    /**
     * Asynchronous message example
     */
    public function sendAsyncAction()
    {
        $clientGearman = Model_Gearman_Manager::getClient();
        $data = array ('message' => "Hello World");
        $clientGearman->doBackground("asyncMessage", serialize($data));
    }

    /**
     * Synchronous message example
     */
    public function sendSyncAction()
    {
        $clientGearman = Model_Gearman_Manager::getClient();
        $data = array ('message' => "Hello World");
        echo $clientGearman->do("syncMessage", serialize($data));
    }
}

Sí, enviar mensajes con el Manager es una tarea sencillísima. También estaría correcto que el Manager se ubicara en un Service para envío de mensajes. Al final, tan sólo hay que obtener una instancia del Cliente y llamar a los métodos do() o doBackground() con el nombre de la función registrada en el Worker. Para enviar información compleja, es necesario serializar los parámetros.

Por tanto, llamadas a la url http://tudominio/gearmanTest/send-sync/http://tudominio/gearmanTest/send-async enviarán mensajes al Job Server síncronos y asíncronos respectivamente, con lo que podrás probar de enviar mensajes abriendo éstas url’s en tu navegador. Recuerda que aunque aun no tengas los workers ejecutándose, el Job Server guardará los mensajes hasta que encuentre un Worker disponible para consumirlos.

Instanciación del Worker y pruebas

Los Workers se inicializarán como ejecuciones de PHP en línea de comandos. Es decir, un pequeño script instanciará la clase y ésta quedará permanentemente activa escuchando tareas. Para ello crearemos un fichero llamado runWorker.php en un directorio, por ejemplo /application/processes/gearman/runWorker.php. Éste tan sólo se encargará de recibir por parámetros el nombre del Worker a instanciar y el entorno de la aplicación, y será el fichero que ejecutaremos cada vez que queramos crear un Worker en un server.

/application/processes/gearman/runWorker.php

<?php
/**
 * Initializes Gearman Workers based on a description name
 */
error_reporting(E_ALL);

/* Inicializar el proceso de bootstrap
 * Nota: Esto variará en función de vuestro entorno por lo que se
 * oculta en otro fichero al no ser relevante en el ejemplo
 */
require '../initialize.php';

if (count($argv) > 1) {

    $workerName = $argv[1];

    define('APPLICATION_ENVIRONMENT', (!empty($argv[2])) ? $argv[2] : 'DEVEL');

    $logFile = (!empty($argv[3])) ? $argv[3] : null;

// Params error
} else {
    $message = "
        Usage: php runWorker.php WORKER_NAME [ENVIRONMENT] [LOG_FILE] \n
        Example: php runWorker topsearches PRODUCTION \tmp\gearman.log \n
        Default Environment: DEVEL  \n
    ";
    die($message);
}

// Instantiates a new worker
Model_Gearman_Manager::runWorker($workerName, $logFile);

La última línea es la interesante. Ésta llama al GearmanManager para que cree la instancia de la clase junto con un posible fichero de log para registrar posibles eventos. La clase permanece en el método constructor a la espera de tareas.

Por tanto, para lanzar un Worker, tan sólo necesitaremos ejecutar en nuestra línea de comandos:

   $php /path/de/tu/aplicacion/application/processes/gearman/runWorker.php test DEVEL /path/logging/gearman.log

Fíjate que el nombre del primer parámetro es test, por lo que el método runWorker() del GearmanManager se encargará de buscar una clase llamada testWorker.php dentro del directorio de /Workers.

Puedes lanzar el proceso tantas veces como quieras y crear tantos Workers como creas conveniente, el Job Server se encargará de enviar la tarea al primer Worker disponible que encuentre. Incluso puedes instanciarlos en otras máquinas, siempre dispongan del código de tu aplicación y puedan escuchar el puerto del Job Server.

¡Ya lo tenemos! Haz la prueba en tu cliente de terminal, ejecuta las urls antes mencionadas para enviar mensajes y observa cómo el Worker te muestra por pantalla el mensaje recibido. Si añadiste por parámetro un fichero de log, podrás ver también la información recibida en dicho fichero.

Monitorización

Una vez tenemos un Worker trabajando para un Job Server, un par de acciones en un controlador para enviar mensajes, y hemos comprobado que se establece la comunicación, tenemos todo a punto para empezar a mover funcionalidades de tu aplicación. De todos modos, te preguntarás cómo consultar el estado de un Job Server para ver cómo de cargado está.

Existe una herramienta de monitorización del Job Server para la librería de PEAR, llamado Gearman-Monitor, aunque en nuestro caso (usamos la de pecl) utilizaremos el comando telnet para obtener información del estado.

$ telnet localhost 4730
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
status
asyncMessage	0	0	1
syncMessage	0	0	1

Mediante el comando status obtenemos un listado de las funciones registradas, donde las tres columnas numéricas informan de:

  • Número de jobs en cola
  • Núm. de jobs ejecutándose
  • Num. de workers disponibles

…y con todo esto, hemos montado un sencillo sistema para paralelizar tareas que escala de forma fácil y que a partir de ahora, crear nuevas tareas apenas supone escribir la lógica de la propia tarea. Moviendo aquellas partes de tu aplicación pesadas y que no requieren de su ejecución inmediata (como las mencionadas en el primer post), conseguiremos mejorar notablemente el rendimiento tanto en tiempo de ejecución como en aprovechamiento de los recursos.

Referencias

Integrating Gearman Into Zend Framework
http://blog.digitalstruct.com/2010/10/17/integrating-gearman-into-zend-framework/

Zend_Gearman on GitHub
https://github.com/mwillbanks/Zend_Gearman

Gearman-Monitor on GitHub
https://github.com/yugene/Gearman-Monitor

PHP Extension Docs
http://docs.php.net/manual/en/book.gearman.php

GearmanManager on GitHub
https://github.com/brianlmoon/GearmanManager

 

2 Comments

 

  1. 03/04/2012  8:19 AM by Grego Reply

    Pedazo post. Felicidades!!!!!! Usar Gearmans tiene un poquito de curro pero merece la pena!!!

Leave a reply

 

Your email address will not be published.