phpdaemon installation, configuration and usage tips & tricks

If you own a website with too many AJAX requests like autocomplete or real-time features like auction bidding that require quick response and you also use Zend Framework or any other PHP heavy library/framework on your backend, then you end up with way too many HTTP requests to the backend each of them requires framework bootstrapping. You can launch any PHP profiler like xdebug to see that 90% of time is spent on performing bootstrapping procedures. Therefore, high cpu load -> fewer requests processed. So the idea of keeping loaded application in memory seems obvious. Luckily, there are some libraries on the market to solve the problem. One of them is phpdaemon which can load your application into memory, launch several worker processes and act like HTTP or FastCGI server.

Let’s consider a web application built on Zend Framework – an online auction. We need to bid on any item, process the bid really fast and update user on other bids as quickly as possible. So the urls for these actions are „/deal/%auction_id%/buy“ and „/pull/%auction_id%“ (get the currently winning bidder). We’d like to send /pull requests every 150-200 ms. In this article we won’t consider such techniques as jssocket or jXMLSocket, we’ll use JSONP requests.

var DealWinner = {
        last    : 0,
        dealid  : 0,
        intervalID : 0,
        start   : function(dealid,timeout){
                DealWinner.dealid = dealid;
                setInterval('DealWinner.pull()',timeout);
        },
        pull    : function(){
                lastId = $('#deals li').first().attr('uid');
                $.getJSON(window.daemonHost+'/pull/'+DealWinner.dealid+'?last='+lastId+'&jsonp_callback=?',function(data){
 ...........
                }, 'json');
        },
......
DealWinner.start(deal_id,200);

Same thing with „/bid/%auction_id%“ url.

Installation of phpdaemon:

    [list_item]download the distribution from github. At the moment the latest version os 0.4.1[/list_item]
    [list_item]unpack and make sure bin/phpd is executable, if not chmod +x bin/phpd, copy conf folder to /etc/phpdaemon[/list_item]
    [list_item]You may want to use a different PHP distribution to work with phpdaemon. Note that it requires PHP >= 5.3 and some unstable PECL extensions installed (see below). So if you don’t want to recompile your production set project, you may compile a new php-cgi binary that will be used only by phpdaemon. You’ll need to install all PECL extenstions that your prod PHP version has.[/list_item]
    [list_item]Do „pecl install channel://pecl.php.net/libevent-0.0.4“ for your php version used by the daemon. You need to specify the path since this is not a stable package. If the compilation fails because of absent phpize , then install php-dev package, something like „apt-get install php5-dev“ in case of apt package manager. You may also need libevent-dev package[/list_item]
    [list_item] pecl install channel://pecl.php.net/proctitle-0.1.1[/list_item]
    [list_item]you may create a symlink to /path/where/you/extracted/phpd at /usr/bin/phpd: „ln -s /path/where/you/extracted/phpd /usr/bin/phpd“[/list_item]
    [list_item]add extension=proctitle.so, extension=libevent.so to your php.ini file.[/list_item]
    [list_item]edit phpd.conf in configuration folder, change the type of server you need, in our case it was FastCGI server, so just

    FastCGI {
            privileged;
     
            # you can redefine default settings for FastCGI
    }

    , set the correct groupname and username that phpdaemon will run on behalf of. You may tweak the number of workers.[/list_item]

Now you should be done. You can try to „phpd start“ and then „phpd fullstatus“ commands , you should see something like :

[STATUS] phpDaemon 0.4.1 is running (/var/run/phpd.pid).
Uptime: 14 hour. 22 min. 17 sec.
State of workers:
        Total: 20
        Idle: 20
        Busy: 0
        Shutdown: 3
        Pre-init: 0
        Wait-init: 0
        Init: 0

We’ll use a separate web server for handling all these JSONP requests in order to not load (even with proxy_pass) our main web server. Nginx is a fast web server, so we decided to use it. Let’s see the config of it to support these URLs:

server {
        listen   8091;
        server_name coolsite.com;
 
        access_log  /var/log/nginx/localhost.access.log;
 
        location ~ (/pull/[0-9]+|/deal/[0-9]+/buy|/ajax/synchtime) {
            if (!-e $request_filename) {
                rewrite ^.*$ /index.php last;
            }
        }
        location ~ (\.inc\.php|\.tpl|\.sql|\.tpl\.php|\.db)$ {
            deny all;
        }
        location ~ \.htaccess {
            deny all;
        }
        location ~ \.php$ {
                root /var/www/coolsite.com;
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME /var/www/coolsite.com$fastcgi_script_name;
                fastcgi_param APPNAME ZfconfApp;
                include fastcgi_params;
 
        }
}

As you can see it handles only mentioned above requests (regexp location) and forwards them to a FastCGI server implemented by phpdaemon. And also sets an environment variable APPNAME which is necessary for the daemonized application.

In order to tell phpdaemon how to serve our app, we need to create a so called class responder where we give instructions to the daemon on how to start , shutdown and run our app. I used a class written by this guy and it can be used by any ZF powered PHP application. Here is the code of it:

// Define path to application directory
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH', '/var/www2/dealday.com/application');
 
// Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));
 
 
set_include_path(implode(PATH_SEPARATOR, array(
    '/var/www2/dealday.com',
    '/var/www2/dealday.com/library/',
    get_include_path()
)));
class ZfconfApp extends AppInstance
{
    public $appFrontController = null;
 
    public function init()
    {
        Daemon::log(__CLASS__ . ' up');
        date_default_timezone_set('Atlantic/Azores');
        require_once 'Zend/Application.php';
        $application = new Zend_Application(
            APPLICATION_ENV,
            APPLICATION_PATH . '/configs/application.ini'
        );
        $bootstrap = $application->bootstrap()->getBootstrap();
 
        $this->appFrontController = $bootstrap->getResource('FrontController');
        $this->appFrontController->setParam('bootstrap', $bootstrap);
        $this->appFrontController->returnResponse(true);
    }
 
    public function onReady()    { /** after initialization */ }
    public function onShutdown() { return true; }
 
    public function beginRequest($request, $upstream)
    {
        return new ZfconfRequest($this, $upstream, $request);
    }
}
 
class ZfconfRequest extends HTTPRequest
{
    public $response = null;
 
    public function run()
    {
        $this->response = null;
        $this->appInstance->appFrontController->setRequest('Zend_Controller_Request_Http');
        $this->appInstance->appFrontController->setResponse('Zend_Controller_Response_Http');
        $this->appInstance->appFrontController->returnResponse(true);
        try{
            $this->response = $this->appInstance->appFrontController->dispatch();
        } catch(Exception $e) {
            Daemon::log('exception: '. $e->getMessage());
        }
        return Request::DONE;
    }
 
    public function onFinish()
    {
        //Daemon::log(__CLASS__ . ' finished request processing for ' . $_SERVER['REQUEST_URI']);
        try {
            if (null === $this->response) { throw new Exception('NULL response provided'); }
            $this->response->renderExceptions(false);
            $this->header('Content-Type: application/x-javascript; charset=utf-8',true);
            print $this->response->outputBody();
        } catch (Exception $e) {
            Daemon::log(__CLASS__ . ' Exception ' . $e->getMessage());
        }
    }
}
 
return new ZfconfApp;

You can use Daemon::log() handful method for debugging and logging in your app, however, I’d not recommend leaving any logging in onFinish method of HTTPRequest class as it will be launched on every request so your phpdaemon log will grow too fast. In the class we see an extended version of AppInstance class where we set bootstrapping and point to the class serving requests. In ZfconfRequest extedned from HTTPRequest we set ZF front controller request/response classes, tell ZF to return the response rather than put it to output and dispatch the request then. Be careful as dispatching requires correct setup of web server’s env variables like baseUrl, script name , etc. So you may refer to nginx config above to see how we pass SCRIPT_NAME variable. Also, pay attention at the $this->header line in onFinish() – we added it because we serve only json responses with this setup, if you’re planning to return html or something else, remove it then. This is just a demo of how to set an HTTP header from daemon.

Now if you start the daemon by /path/to/phpd start command, you should see that several workers (depending on phpd config, see /path/to/phpd/config.conf settings max-workers, min-workers, start-workers). Try to test it by navigating with your browser to any of urls with some parameters. In our case it is https://coolsite.com:8091/pull/11111. If you don’t see a response or there is an error you can debug your app with Daemon::log(). If you setup a separate PHP distribution to work with phpdaemon, be sure you install all PECL extensions you use in your app – I experienced an error that was hard to debug in this environment with absent memcache extension.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

This site uses Akismet to reduce spam. Learn how your comment data is processed.