Bring High Performance Into Your PHP App (with ReactPHP)
In this article I want to show you how you can get the maximum performance out of your PHP application. Most apps don't really use the whole power of PHP, instead just activate APC and think that is the most you can get. Keep reading if you want to be surprised.
TL;DR
You'll get with this approach almost 2.000 requests/s instead of 130 on a large Symfony app.
Architecture
First of all, some history.
Normal usage of PHP these days is with well-known web servers like Apache, Nginx, lighttpd, etc that handle the HTTP protocol and redirect only dynamic requests to PHP. Together with rewrite engines like Apache's mod_rewrite it becomes very powerful.
There are several ways to configure these web servers to run PHP:
- mod_php (apache only)
- f(ast)cgi
- PHP-FPM
A very common setup is FCGI with SuExec because of the security this offers. Each interpreter process runs under a specific per-site user. This separation makes it possible to have mass-hosting without a VM for each customer. Having said that, it's quite common to have mod_php or PHP-FPM for local development machines or single app web server (a server with a web server that handles only apps from the same vendor).
Well, all those setups are quite common throughout the whole industry. The very big drawback here is that even if you have an "opcode cache" you have to declare classes, instantiate your objects, read your caches, etc for every single request. As you can surely imagine this is very time consuming and far away from being a perfect setup for high performance.
Think Outside The Box
So, why are we actually doing that? Why, after every request, are we destroying the memory used by PHP for our application and re-creating it every time? Well, it's because PHP wasn't designed to be a server itself but only a template engine, a set of tools, when it was created. Also PHP itself isn't really designed to be asynchronous - almost all functions are "blocking". Over the years, things have changed dramatically. Today, we have very powerful template engines written in PHP. We have a big ecosystem of thousands of useful libraries easily installable thanks to Composer. We have implemented very powerful design patters in PHP coming from Java and other languages (Hi Symfony & Co) and we even have libraries for asynchronous web servers in PHP.
Wait, What?
Wait, we have asynchronous tools in PHP? Yes, we have. ReactPHP is one of the most promising libraries to me. It brings the powerful concept of event-driven, non-blocking I/O (Hi NodeJS) to PHP. With this technology in mind we are able to write our own HTTP stack in PHP and have control back over the memory without destroying it at the end of each request.
I guess it's pretty easy to understand, that when we don't have to instantiate all of our objects, read the cache, etc we would gain much more performance as most of the time the bootstrap of our application is a significant factor of our response time. When we remove that factor we would be in the same boat as Java, NodeJS & Co are, which means performance! Yay.
What's The Deal With That?
It's pretty easy. What we need is ReactPHP, downloadable at http://reactphp.org. Use composer to install it.
$ composer require 'react/react=*'
We are then going to create a PHP file and call it server.php
:
<?php
require 'vendor/autoload.php';
$i = 0;
$app = function ($request, $response) use ($i) {
$response->writeHead(200, array('Content-Type' => 'text/plain'));
$response->end("Hello World $i\n");
$i++;
};
$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket, $loop);
$http->on('request', $app);
echo "Server running at http://127.0.0.1:1337\n";
$socket->listen(1337);
$loop->run();
Now you can start your first PHP web server with php server.php
. You should now be able to open http://127.0.0.1:1337
in your
browser and see the "Hello World" message. $app
is your "main" function which is the entry point for each incoming request.
That's it. It's really easy as that. Period.
Benchmark Time
You might think now "Well, PHP uses only one cpu core/thread. It doesn't utilize the whole power of my multi-core server.". Well this is actually true, but
What we need now is a server that spawns several workers to have multi core support. I've written a process manager for that with a rough Symfony "bridge" so it's possible to have Symfony apps running with the power of a nuclear reactor. ;o)
https://github.com/marcj/php-pm
And this is how its setup looks:
Setup:
- Intel(R) Xeon(R) CPU L5630, 6 Cores
- 8GB RAM
- PHP 5.4.4 with APC
- Debian 7.1
- nginx/1.2.1
Both PHP-FPM and the React server have 6 worker children.
Tests are made with ab - Apache HTTP server benchmarking tool
.
What we are going to test now is a pretty serious web app written in Symfony 2.4+. We're talking about a full featured CMS bundle: https://github.com/kryncms/KrynCmsBundle with many services, event listeners, caching, templates, database access, etc.
The caching is configured to only cache basic things; The results of views are not cached, so we don't have a full cache running.
Our command to start the react server is:
$ php ./bin/ppm start /path/to/symfony/ --bridge=symfony -vvv
for hhvm:
$ hhvm ./bin/ppm start /path/to/symfony/ --bridge=symfony -vvv
And here is the result:
Requests Per Second
Memory Usage
Legend:
fpm
- is the regular PHP-FPM server behind an nginx server.react
- is the react server with builtin load balancer (as shown in the setup image).react+nginx
- is the react server with nginx as load balancer. Our react server only spawns worker processes; nginx then talks directly with those workers.hhvm
- is the same asreact
but started withhhvm
.hhvm+nginx
- is the same asreact+nginx
but started withhhvm
.
hhvm
uses stream_select
event loop and php
uses libevent
.
So, do not use the builtin webserver of hhvm.
As we can see, the react server with nginx as load balancer is over 15 times faster than old school PHP-FPM + APC.
With react+nginx (PHP module libevent), we can handle now almost 2.000 requests each second. If you sum this up:
2.000 requests / second
7.200.000 requests / hour
86.400.000 requests / half day
172.800.000 requests / day
2.678.400.000 requests / half month
5.356.800.000 requests / month
The memory usage is almost the same. The continuous increase of memory here is probably caused due to a memory leak in the CmsBundle. It's not yet optimized to be in such a environment. See below to get more information about the issues with this approach.
This means, we have a dramatic performance increase with our Symfony app. Should be worth a try.
I think I don't have to say anything else. Those images say more than thousand words. ;)
Nginx as Load-Balancer
For the react+nginx
and hhvm+nginx
I've used following configuration.
What we do here is to proxy only requests that don't point to a local file.
upstream backend {
server 127.0.0.1:5501;
server 127.0.0.1:5502;
server 127.0.0.1:5503;
server 127.0.0.1:5504;
server 127.0.0.1:5505;
server 127.0.0.1:5506;
}
server {
root /path/to/symfony/web/;
server_name servername.com
location / {
if (!-f $request_filename) {
proxy_pass http://backend;
break;
}
}
}
Issues With This Approach
There are several issues with this approach you have to bear in mind.
First, we have to handle memory better to avoid leaks. Newer versions of PHP do pretty well, but the old way of destroying the whole application's memory after the request has been handled doesn't pay much attention to this topic. So, take more care about your data in your variables.
Second, we have to restart our server every time a file changes, because in PHP it's not possible to redefine symbols (classes, functions). I plan to find a solution for that via the PHP process manager we use to start our workers.
Third, when an uncaught exception gets thrown we have to restart our server somehow. I'm planning to find a solution for that via the PHP process manager, too.
Fourth, even though ReactPHP gives us the ability to write asynchronous code, most libraries (including Symfony) aren't written this way. This means, if you want to get the real maximum out of your web app you have to rewrite your app to be asynchronous. This is achievable and in general faster, but you can soon end up in callback hell. So, take a look how Node.js apps do it and decide on your own if it's worth undertaking their approach. If want my opinion: DO IT.
Fifth, if you want to work with a heavy-duty framework in combination with ReactPHP you should make sure that this framework is able to separate
its internal data per request. Symfony for example is one of those Request/Response frameworks
. This means if you haven't
yet used a framework like those you have to rethink how you develop applications with PHP. That is a
dramatic change. What this means is basically that you must not use $_POST, $_GET, $_SERVER anymore.
It might be that your current framework just does not support that.
If is this the case: throw it away and take Symfony. ;) It's totally worth it.
Final Thoughts
This approach should not replace your Apache/NGiNX/Lighttp & co. We have only a HTTP Server running with ReactPHP to omit/kill the expensive bootstrap of our application, because this is actually always one of the most time consuming parts. Additional to that we'd have then a new cache layer: PHP vars. What we have done before with user cache of APC could now be done with old school PHP arrays. Just remember here that you still have to have a distributed invalidation system to invalidate caches.
ReactPHP is not yet stable. I encountered some issues where the CPU usage is raised up to 100% without incoming connections. So, please, if you have the knowledge, participate on that awesome project and help to get it stable & done: https://github.com/reactphp/react
// Edit, the 100% CPU issue has been fixed in PR #273
Also, the bridge to Symfony I've written (that converts a React Request/Response to a Symfony Request/Response object) does not yet fully work. React refactors at the moment some parts of the HTTP server. It's sure we can get anything working with Symfony, the question is only: when. Feel free to contribute! :-)
Special thanks to Drarok!