Loïc Faugeron Technical Blog

Super Speed Symfony - ReactPHP 13/04/2016

TL;DR: Run your application as a HTTP server to increase its performances.

HTTP frameworks, such as Symfony, allow us to build applications that have the potential to achieve Super Speed.

A first way to make use of it is to run our application as a HTTP server. In this article we'll take a Symfony application and demonstrate how to run it as HTTP server using ReactPHP.

ReactPHP HTTP server

We're going to use ReactPHP's HTTP component:

composer require react/http:^0.5@dev

It helps us build HTTP servers:

#!/usr/bin/env php
<?php
// bin/react.php

require __DIR__.'/../vendor/autoload.php';

$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket);

$callback = function ($request, $response) {
};

$http->on('request', $callback);
$socket->listen(1337);
$loop->run();

Starting from the last line, we have:

Note: HTTP servers usually use the 80 port, but nothing prevents us from using a different one. Since there might be some HTTP servers already running on our computers (e.g. Apache or nginx), we'll use 1337 in our examples to avoid conflicts.

Hello World example

The application logic has to be written in the callback. For example, here's how to write a Hello World!:

#!/usr/bin/env php
<?php
// bin/react.php

require __DIR__.'/../vendor/autoload.php';

$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket);

$callback = function ($request, $response) {
    $statusCode = 200;
    $headers = array(
        'Content-Type: text/plain'
    );
    $content = 'Hello World!';

    $response->writeHead($statusCode, $headers);
    $response->end($content);
};

$http->on('request', $callback);
$socket->listen(1337);
$loop->run();

If we run it now:

php bin/react.php

Then we can visit the page at http://localhost:1337/, and see a Hello World! message: it works!

Symfony example

Let's recreate the same project, but using the Symfony Standard Edition:

composer create-project symfony/framework-standard-edition super-speed
cd super-speed
composer require react/http:^0.5@dev --ignore-platform-reqs

Since Symfony is a HTTP framework, wrapping it inside the callback is quite natural. We only need to:

  1. convert the ReactPHP request to a Symfony one
  2. call a HttpKernelInterface implementation to get a Symfony response
  3. convert the Symfony response to a ReactPHP one

As we can see, this is quite straightforward:

#!/usr/bin/env php
<?php
// bin/react.php

require __DIR__.'/../app/autoload.php';

$kernel = new AppKernel('prod', false);
$callback = function ($request, $response) use ($kernel) {
    $method = $request->getMethod();
    $headers = $request->getHeaders();
    $query = $request->getQuery();
    $content = $request->getBody();
    $post = array();
    if (in_array(strtoupper($method), array('POST', 'PUT', 'DELETE', 'PATCH')) &&
        isset($headers['Content-Type']) && (0 === strpos($headers['Content-Type'], 'application/x-www-form-urlencoded'))
    ) {
        parse_str($content, $post);
    }
    $sfRequest = new Symfony\Component\HttpFoundation\Request(
        $query,
        $post,
        array(),
        array(), // To get the cookies, we'll need to parse the headers
        $request->getFiles(),
        array(), // Server is partially filled a few lines below
        $content
    );
    $sfRequest->setMethod($method);
    $sfRequest->headers->replace($headers);
    $sfRequest->server->set('REQUEST_URI', $request->getPath());
    if (isset($headers['Host'])) {
        $sfRequest->server->set('SERVER_NAME', explode(':', $headers['Host'])[0]);
    }
    $sfResponse = $kernel->handle($sfRequest);

    $response->writeHead(
        $sfResponse->getStatusCode(),
        $sfResponse->headers->all()
    );
    $response->end($sfResponse->getContent());
    $kernel->terminate($request, $response);
};

$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket);

$http->on('request', $callback);
$socket->listen(1337);
$loop->run();

Note: Request conversion code from React to Symfony has been borrowed from M6Web PhpProcessManagerBundle.

And as easy as that, we can run it:

php bin/react.php

Finally we can visit the page at http://localhost:1337/, and see a helpful Welcome message: it works!

Benchmarking and Profiling

It's now time to check if we've achieved our goal: did we improve performances?

Regular version

In order to find out, we can first benchmark the regular Symfony application:

SYMFONY_ENV=prod SYMFONY_DEBUG=0 composer install -o --no-dev --ignore-platform-reqs
php -S localhost:1337 -t web&
curl 'http://localhost:1337/app.php/'
ab -c 1 -t 10 'http://localhost:1337/app.php/'

We get the following results:

We can also profile the application using Blackfire to discover bottlenecks:

blackfire curl 'http://localhost:1337/app.php/'
killall -9 php

We get the following results:

Let's have a look at the graph:

As expected from an empty application without any logic, we can clearly see that autoloading is the number 1 bottleneck, with the Dependency Injection Container being its main caller (for which the EventDispatcher is the main caller).

ReactPHP version

Before we continue our benchmarks for the ReactPHP version of our application, we'll need to modify it a bit in order to support Blackfire:

#!/usr/bin/env php
<?php
// bin/react.php

require __DIR__.'/../app/autoload.php';

$kernel = new AppKernel('prod', false);
$callback = function ($request, $response) use ($kernel) {
    $method = $request->getMethod();
    $headers = $request->getHeaders();
    $enableProfiling = isset($headers['X-Blackfire-Query']);
    if ($enableProfiling) {
        $blackfire = new Blackfire\Client();
        $probe = $blackfire->createProbe();
    }
    $query = $request->getQuery();
    $content = $request->getBody();
    $post = array();
    if (in_array(strtoupper($method), array('POST', 'PUT', 'DELETE', 'PATCH')) &&
        isset($headers['Content-Type']) && (0 === strpos($headers['Content-Type'], 'application/x-www-form-urlencoded'))
    ) {
        parse_str($content, $post);
    }
    $sfRequest = new Symfony\Component\HttpFoundation\Request(
        $query,
        $post,
        array(),
        array(), // To get the cookies, we'll need to parse the headers
        $request->getFiles(),
        array(), // Server is partially filled a few lines below
        $content
    );
    $sfRequest->setMethod($method);
    $sfRequest->headers->replace($headers);
    $sfRequest->server->set('REQUEST_URI', $request->getPath());
    if (isset($headers['Host'])) {
        $sfRequest->server->set('SERVER_NAME', explode(':', $headers['Host'])[0]);
    }
    $sfResponse = $kernel->handle($sfRequest);

    $response->writeHead(
        $sfResponse->getStatusCode(),
        $sfResponse->headers->all()
    );
    $response->end($sfResponse->getContent());
    $kernel->terminate($request, $response);
    if ($enableProfiling) {
        $blackfire->endProbe($probe);
    }
};

$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket);

$http->on('request', $callback);
$socket->listen(1337);
$loop->run();

This requires Blackfire's SDK:

SYMFONY_ENV=prod SYMFONY_DEBUG=0 composer require -o --update-no-dev --ignore-platform-reqs 'blackfire/php-sdk'

Now let's run the benchmarks:

php bin/react.php&
curl 'http://localhost:1337/'
ab -c 1 -t 10 'http://localhost:1337/'

We get the following results:

Finally we can profile it:

curl -H 'X-Blackfire-Query: enable' 'http://localhost:1337/'
killall -9 php

We get the following results:

Let's have a look at the graph:

This time we can see that most of the time is spent in event listeners, which is expected since that's the only lace in our empty application where there's any logic.

Comparison

There's no denial, we've made use of our potential to achieve Super Speed: by converting our application into a HTTP server using ReactPHP we improved our Symfony application by 8!

Alternatives to ReactPHP

After running some silly benchmarks, we've picked ReactPHP as it was seemingly yielding better results:

ReactPHP is faster than Aerys which is faster than IcicleIO which is faster than PHP FastCGI

However since we don't actually make use of the true potential of any of those projects, it's worth mentioning them and their differences:

Not mentioned in the graph, there's also:

Note: To check the benchmarks, have a look at Bench Symfony Standard. Each project has its own branch with the set up used and the benchmarks results.

Why does ReactPHP improve performances?

To understand how turning our application into a HTTP server can increase performances, we have to take a look how the alternative works. In a regular stack (e.g. "Apache / mod_php" or "nginx / PHP-FPM"), for each HTTP request:

  1. a HTTP server (e.g. Apache, nginx, etc) receives the Request
  2. it starts a new PHP process, variable super globals, (e.g. $_GET, $_POST, etc) are created using data from the Request
  3. the PHP process executes our code and produces an output
  4. the HTTP server uses the output to create a Response, and terminates the PHP process

Amongst the advantages this brings, we can list not having to worry (too much) about:

Killing the PHP process once the Response is sent means that nothing is shared between two Requests (hence the name "shared-nothing" architecture).

One of the biggest disadvantages of such a set up is low performance., because creating a PHP process for each HTTP Requests means adding a bootstraping footprint which includes:

With ReactPHP we keep our application alive between requests so we only execute this bootstrap once when starting the server: the footprint is absent from Requests.

However now the tables are turned: we're vulnerable to memory consumption, fatal error, statefulness and code update worries.

Making ReactPHP production ready

So turning our application into a HTTP server means that way have to be mindful developers: we have to make it stateless and we need to restart the server for each updates.

Regarding fatal errors and memory consumption, there is a simple strategy to we can use to mitigate their impact: automatically restart the server once it's stopped.

That's usually a feature included in load balancers (for example in PHP-PM, Aerys and appserver.io), but we can also rely on Supervisord.

On Debian based distributions it can easily be installed:

sudo apt-get install -y supervisor

Here's a configuration example (create a *.conf file in /etc/supervisord/conf.d):

[program:bench-sf-standard]
command=php bin/react.php
environment=PORT=55%(process_num)02d
process_name=%(program_name)s-%(process_num)d
numprocs=4
directory=/home/foobar/bench-sf-standard
umask=022
user=foobar
stdout_logfile=/var/log/supervisord/%(program_name)s-%(process_num)d.log              ; stdout log path, NONE for none; default AUTO
stderr_logfile=/var/log/supervisord/%(program_name)s-%(process_num)d-error.log        ; stderr log path, NONE for none; default AUTO
autostart=true
autorestart=true
startretries=3

It will:

Here's a nice resource for it: Monitoring Processes with Supervisord.

While PHP itself doesn't leak memory, our application might. The more memory a PHP application uses, the slower it will get, until it reaches the limit and crashes. As a safeguard, we can:

But a better way would be to actually hunt down memoy leaks, for example with PHP meminfo.

We also need to know a bit more about the tools we use such as Doctrine ORM or Monolog to avoid pitfalls (or use the LongRunning library to clean those automatically for us).

Conclusion

It only takes ~50 lines to turn our application into a HTTP server, ReactPHP is indeed a powerful library.

In fact we haven't even used its main features and still managed to greatly improve performances! But these will be the subject of a different article.

Note: Read-only APIs are a good candidate for such a set up.

In the next blog post, we'll have a look at a different way (not that we can't combine both) to achieve the Super Speed potential of our applications built with HTTP frameworks like Symfony.

In the meantime, here's some resources about turning our applications into HTTP applications: