Super Speed Symfony - ReactPHP 13/04/2016 symfony

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:

$loop->run() : makes our HTTP server run inside an infinite loop (that's how long running processes work)

: makes our HTTP server run inside an infinite loop (that's how long running processes work) $socket->listen(1337) : opens a socket by listening to a port (that's how servers work)

: opens a socket by listening to a port (that's how servers work) $http->on('request', $callback) : for each HTTP Request received, executes the given callback

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:

convert the ReactPHP request to a Symfony one call a HttpKernelInterface implementation to get a Symfony response 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:

Requests per second: 273.76 #/sec

Time per request: 3.653 ms

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:

Wall Time: 12.5ms

CPU Time: 11.4ms

I/O Time: 1.09ms

Memory: 2.2MB

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:

Requests per second: 2098.17 #/sec

Time per request: 0.477 ms

Finally we can profile it:

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

We get the following results:

Wall Time: 1.51ms

CPU Time: 1.51ms

I/O Time: 0.001ms

Memory: 0.105MB

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:

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:

a HTTP server (e.g. Apache, nginx, etc) receives the Request it starts a new PHP process, variable super globals, (e.g. $_GET , $_POST , etc) are created using data from the Request the PHP process executes our code and produces an output 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:

memory consumption: each new process starts with a fresh empty memory which is freed once it exits (memory leaks can be ignored)

fatal errors: a process crashing won't affect other processes (but if they encounter the same error they'll also crash)

statefullness: static and global variables are not shared between processes

code updates: each new process starts with the new code

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:

starting a process

starting PHP (loading configuration, starting extensions, etc)

starting our application (loading configuration, initializing services, autoloading, etc)

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:

run 4 ReactPHP servers on ports 5500 , 5501 , 5502 and 5503

, , and it restarts them automatically when they crash (will try a maximum of 3 times, then give up)

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:

stop the server after X requests (put a counter in the callback and once the server stops, Supervisord will restart a new one)

stop the server once a given memory limit is reached (then supervisord will restart a new one)

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: