TomButler's avatar

Proof of concept: Application server - ~2400% laravel startup speed increase

A couple of years ago I started playing around making an application server and I just tested it with the basic Laravel hello world "Laravel 5" page:

Server Software:        Apache/2.4.20
Server Hostname:        localhost
Server Port:            80

Document Path:          /laravel-default/public/
Document Length:        1023 bytes

Concurrency Level:      20
Time taken for tests:   5.015 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      2042146 bytes
HTML transferred:       1023000 bytes
Requests per second:    199.41 [#/sec] (mean)
Time per request:       100.294 [ms] (mean)
Time per request:       5.015 [ms] (mean, across all concurrent requests)
Transfer rate:          397.69 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.8      0       8
Processing:    44   99  27.5     99     268
Waiting:       44   99  27.0     99     268
Total:         44  100  27.5     99     268

With my Application server:

Server Software:        Apache/2.4.20
Server Hostname:        localhost
Server Port:            80

Document Path:          /laravel-appserver/public/
Document Length:        1023 bytes

Concurrency Level:      20
Time taken for tests:   0.206 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      1393000 bytes
HTML transferred:       1023000 bytes
Requests per second:    4863.53 [#/sec] (mean)
Time per request:       4.112 [ms] (mean)
Time per request:       0.206 [ms] (mean, across all concurrent requests)
Transfer rate:          6616.11 [Kbytes/sec] received

The results are fairly self-evident. This was on a hex-core machine so the results are exaggerated, however even on a quad or dual core you should see a large performance increase. This requires a linux server and the extension=sysvmsg.so PHP extension uncommented in php.ini

The way this works is that the application is run in memory avoiding the need to bootstrap each request. Traditional PHP scripts need to do a lot of bootstrapping before processing the request: requireing dozens of files, loading configuration, connecting to the databasse, etc. By using an application server all this is done once. Then, each request connects to the application server and the appserver processes the request without needing to go through the entire bootstrap process.

N.b. This won't be a drop-in speed increase for real apps because developers are lazy and assume things will be destroyed, but it's something to consider

To try it, inside httpdocs or public_html firstly create two laravel installs for a comparison:

composer create-project laravel/laravel laravel-default

composer create-project laravel/laravel laravel-appserver

Now install the appserver:

cd laravel-appserver
composer require level-2/aphplication @dev

Create a appserver.php inside laravel-appserver, this is laravel's public/index.php as a server:

<?php
require 'vendor/autoload.php';
class MyApplication implements \Aphplication\Aphplication {
    //Application state. This will be kept in memory when the application is closed
    //This can even be MySQL connections and other resources

    private $kernel;

    public function __construct() {
        $app = require_once __DIR__.'/bootstrap/app.php';
        $this->kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
    }


    public function accept($appId, $sessionId, $get, $post, $server, $files, $cookie) {
        $response = $this->kernel->handle(
            $request = Illuminate\Http\Request::capture()
        );

        ob_start();
        $response->send();
        return ob_get_clean();

    }

}


//Now create an instance of the server 
$server = new \Aphplication\Server(new MyApplication());

//Check argv to allow starting and stopping the server
if (isset($argv[1]) && $argv[1] == 'stop') $server->shutdown();
else $server->start(); 

And then replace public/index.php with:

<?php
require_once '../vendor/level-2/aphplication/Aphplication/Client.php';

(There's no point in the overhead of an autoloader to load one file)

That's all the PHP stuff done. Start the server

php appserver.php

Once the server is started you can go to /laravel-appserver/public/index.php and should see the "Laravel 5" default page. Visiting /laravel-default/public/index.php will also display the page but not using the appserver.

Run some benchmarks:

ab -n 1000 -c 20 http://localhost/laravel-appserver/public/
ab -n 1000 -c 20 http://localhost/laravel-default/public/

And you'll notice a nice increase in speed as the bootstrapping logic is done once. This also allows keeping things like result sets in memory and running a proper application instead of one-shot scripts, but the performance increase alone is interesting :)

** This won't work with GET/POST/SERVER as I didn't want to mess about with Illuminate\Http\Request::capture() to not use superglobals but it would just be a matter of passing them in as args.**

Comments/suggestions are welcome! Laravel was one of the only frameworks that let me do this out of the box which is why I posted here :) I tried with a few others but they assume too much that the script will end or arbitrarily call exit() to make my test feasible.

0 likes
9 replies
ohffs's avatar

Good stuff :-) There have been quite a few php app servers/workers appearing lately - you might want to have a look at some for inspiration, php-pm for instance :-)

bashy's avatar

I know this is different but Laravel on a ramdisk?

TomButler's avatar

I know this is different but Laravel on a ramdisk?

Yes. On a ramdisk, every file included with require will still need to be loaded an parsed. With an appserver, the PHP script that ran require keeps running in memory so all the php include files are only parsed when the server starts, not every time someone visits a page.

Good stuff :-) There have been quite a few php app servers/workers appearing lately - you might want to have a look at some for inspiration, php-pm for instance :-)

Yeah, I've been working with Node.js recently and got used to the server approach. I created this over a year ago and thought I'd see if I could use it in the same way as Node :) This should be considerably faster than React as this uses sysv and react uses comparatively slower sockets.

ohffs's avatar

There was another post on here with another app server - but now I can't find it. I think it worked in a similar way to yours - but it's a bit hazy ;-) Is the code up on the web somewhere?

ohffs's avatar

I shall try and have a play tomorrow - thanks! I've got the week off work so something to play with will help me avoid doing all the DIY and housework I was planning... ;-)

bashy's avatar

Oh yeah of course ramdisk is no different than normal (in terms of loading files) but just wondering how fast it would be to respond to requests :P

jimmck's avatar

@TomButler Hi Tom, What is the memory model? How do applications share the same PHP VM? How can multiple calls to the webserver share the bootstrap without stepping on each other? In JAVA the classloader manages creating instances of a class running in the same VM. Am I just missing something here? Caching a result involves memory mapping to be truly effective. Memcache is nice but you still have to serialize it and copy it. Its no true caching because there is no shared address space.

TomButler's avatar

@jimmck Yes, it doesn't use memcache because you can't share resources and serialization has some overhead. Here's an example of how it works.

Let's take a sample script:

<?php
require 'config.php';
require 'database.php';

$config = new Config();
$config->parse('x.json');

$database = new Database($config->getDbInfo());


//Now process the actual request

$result = $database->query('select foo from bar');

return json_encode($result);

This connects to the database and returns a resultset as JSON. Every time this script is run, config.php and database.php are parsed/executed and two objects, $database and $config are created. Only then can the script actually process the request.

Aphplication removes this bootstrapping logic. Here's how it works:

This code is moved into a server:

<?php
require 'config.php';
require 'database.php';

class App implements \Aphplication\Aphplication {
    private $config;
    private $database;

    public function __construct() {
        $this->config = new Config();
        $this->config->parse('x.json');
        $this->database = new Database($config->getDbInfo());
    }
    
    public function accept($appId, $sessionId, $get, $post, $server, $files, $cookie) {
        //Now process the actual request

        $result = $database->query('select foo from bar');

        return json_encode($result);
    }
}


//Now create an instance of the server 
$server = new \Aphplication\Server(new App());

//Check argv to allow starting and stopping the server
if (isset($argv[1]) && $argv[1] == 'stop') $server->shutdown();
else $server->start(); 

You'll see there are two parts: The boostrapping code in the constructor and the accept function.

The clever part here is the $server = new \Aphplication\Server(new App()); line. This will use the $app instance and create 24 PHP processes that keep running in memory in a paused state listening for messages using SYSV (think of it a bit like a database server, running in the background listening for connections).

When someone connects to the website, they don't connect to server.php they connect to client.php. This file is as simplistic as possible and contains some incredibly basic code. It sends a SYSV message saying "Run the accept function and give me the result`. The message is received by one of the active, running threads.

Because of the way that SYSV works, all 24 threads can listen to the same location. Whichever thread gets the message first (so the one that's least busy) will get the message, run the accept method and then use SYSV to send a message back to the the PHP scrip that sent the message in the first place (the client.php requested in the browser). Because the threads are always running, all that bootstrap code is already done. Each time someone connects, one of the threads run only the acccept method without needing to load config files, process all the includes or set up large object graphs.

Once the thread sends the result back, it pauses again and waits for the next message, always running in the background :) Each time someone connects the only code that's run on the server is the code in the accept method. Everything before that has already been done. And, once a request triggers the autoloader, that class is loaded and doesn't need to be loaded again until the server is restarted.

Please or to participate in this conversation.