Book Image

High Performance with Laravel Octane

By : Roberto Butti
5 (2)
Book Image

High Performance with Laravel Octane

5 (2)
By: Roberto Butti

Overview of this book

Laravel Octane is a very powerful component in the Laravel ecosystem that can help you achieve remarkable app performance. With Laravel Octane, you will find tools (queues, cache, and tables) that facilitate a new asynchronous approach for improving application performance. This book highlights how Laravel Octane works, what steps to take in designing an application from the start, what tools you have at your disposal, and how to set up production environments. It provides complete coverage of the strategies, tools, and best practices to make your apps scalable and performant. This is especially important as optimization is usually the overlooked part in the application development lifecycle. You will explore the asynchronous approach in Laravel and be able to release high-performing applications that have a positive impact on the end-user experience. By the end of this book, you will find yourself designing, developing, and releasing high-performance applications.
Table of Contents (14 chapters)
1
Part 1: The Architecture
3
Part 2: The Application Server
6
Part 3: Laravel Octane – a Complete Tour
9
Part 4: Speeding Up

Getting to know the application server for Laravel Octane

In the PHP ecosystem, we have several application servers.

Laravel Octane, which handles server configuration, startup, and execution, integrates mainly with two of them: Swoole and RoadRunner.

We will deal with the installation, configuration, and use of these two application servers in detail later on.

For now, it is enough for us to know that once the application servers are installed, Laravel Octane will take care of their management. Laravel Octane will also take care of their proper startup via the following command:

php artisan octane:start

The octane:serve command is added when Laravel Octane is installed.

In other words, Laravel Octane has a strong dependency on application servers such as RoadRunner or Swoole.

At startup, Laravel Octane via Swoole or RoadRunner activates some workers, as shown in the following figure:

Figure 1.1: The activation of workers

Figure 1.1: The activation of workers

What are workers?

In Octane, a worker is a process that takes charge of handling the requests associated with it. A worker has the responsibility of starting the framework and initializing framework objects.

This has an extremely positive impact from a performance standpoint. The framework is instantiated on the first request assigned to the worker. The second (and subsequent) requests assigned to that worker reuse the objects already instantiated. The side effect of this is that the worker shares instances of global objects and static variables between requests. This means that different calls to the controller can access the data structures that are shared between requests.

To complicate matters, there is the fact that requests assigned to the same worker share a global state, but different workers are independent and have scope independent of each other. So, we can say that not all requests share the same global state. Requests share a global state when associated with the same worker. Two requests from two different workers share nothing.

In order to minimize the side effect, Laravel Octane has the responsibility of managing the reset of classes/objects owned directly by the framework across the requests.

However, Octane can’t manage and reset classes owned directly by the application.

That’s why the main thing to pay attention to when using Octane is the scope and lifecycle of variables and objects.

To understand this better, I will give you a very basic example.

Example with a shared variable

This example, in the routes/web.php file, creates a route for path / and returns a human-readable timestamp. To simplify the explanation, we are going to write the logic directly into the route file instead of calling and delegating the logic to a controller:

$myStartTime = microtime(true);
Route::get('/', function () use($myStartTime) {
    return DateTime::createFromFormat('U.u', $myStartTime)
    ->format("r (u)");
});

In the routes/web.php routing file (web.php is already stored in the routes directory in the Laravel root folder project), a $myStartTime variable is instantiated and assigned the current time expressed in milliseconds. This variable is then inherited by the route/management function via the use clause.

In the performance of the function associated with route/, the contents of the $myStartTime variable are returned and then displayed.

With the classic behavior of the Laravel application, at each invocation/execution, the variable is regenerated and reinitialized (each time with a new value).

To start the Laravel application in the classic mode, simply run the following:

php artisan serve

Once the web server is started, go to the following URL via the browser: http://127.0.0.1:8000

By continuously reloading the page, a different value is displayed each time; basically, the timestamp is displayed with each request.

Instead of using the development web server provided by Laravel, you would use Laravel Octane and have a different result. At each page refresh (reloading of the web page), you would always see the same value. The value is relative to the timestamp of the first request served. This means that the variable is initialized with the first request and then the value is reused across the requests.

If you try to refresh multiple times, in some cases, you could see a new value.

If this happens, it means that the request was managed by the second (or a new) worker. This means that this behavior is quite unpredictable because Octane acts as a load balancer. When a request comes from the network, the application server will decide which worker (of those available) to assign the request to.

In addition to this, another element that could cause a new value to be generated is when you hit the maximum number of requests managed by a single worker. We will see how to define the maximum number of requests later, and in general, we will have a deep dive session (in Chapters 2 and 3) into Laravel Octane configuration.

The behavior whereby variables are shared across workers until the application server is restarted is valid only for global variables or objects stored in the application service container. The local variables (the variables for which the scope is limited to a function or a method) are not affected.

For example, in the code previously shown, I’m going to declare a $myLocalStartTime variable in the function called by the routing mechanism. The scope of the $myLocalStartTime variable and its lifecycle is limited to the Closure function:

$myStartTime = microtime(true);
Route::get('/', function () use($myStartTime) {
    $myLocalStartTime = microtime(true);
    return DateTime::createFromFormat('U.u', $myStartTime)
    ->format("r (u)") . " - " .
    DateTime::createFromFormat('U.u', $myLocalStartTime)
    ->format("r (u)");
});

Execute the following command with the classic Laravel web server:

php artisan serve

You will see that both values will change on each new request. You can see that when you open a browser to http://127.0.0.1:8000.

Launch Octane as a server with the following command:

php artisan octane:start

You will see, in your browser at http://127.0.0.1:8000, two different dates/times with milliseconds. If you refresh the page, you will see a change in just the second one ($myLocalStartTime).

You have to be aware of this behavior when you are building an application based on Octane.

Another example to better understand this behavior is creating a class with a static property.

Creating a class with a static property

In order to keep this example as simple as possible, I created a MyClass class in the routes/web.php file.

I’m going to add new routes that call the add() method of the MyClass object and then call and return the value of the static property retrieved by the get() method.

In routes/web.php, add the following class:

class MyClass
{
    public static $number = 0;
    public function __construct()
    {
        print "Construct\n";
    }
    public function __destruct()
    {
        print "Deconstruct\n";
    }
    public function add()
    {
        self::$number++;
    }
    public function get()
    {
        return self::$number;
    }
}

Then, in the routes/web.php file, declare the new route as follows:

Route::get('/static-class', function (MyClass $myclass) {
    $myclass->add();
    return $myclass->get();
});

Next, you can launch Laravel in a classic way using the following command:

php artisan serve

Now, if you access the URL http://127.0.0.1:8000/static-class multiple times, the value 1 will be shown. This is because, classically, for every request, the MyClass object is instanced from scratch.

Launch Laravel Octane using the following command:

php artisan octane:serve

If you then access the URL http://127.0.0.1:8000/static-class multiple times, you will see the value 1 in the first request, 2 in the second, 3 in the third, and so on. This is because, with Octane, MyClass is instanced for every request, but the static values are kept in memory.

With a non-static property, we can see the difference as follows:

class MyClass
{
    public static $numberStatic = 0;
    public $number = 0;
    public function __construct()
    {
        print "Construct\n";
    }
    public function __destruct()
    {
        print "Deconstruct\n";
    }
    public function add()
    {
        self::$numberStatic++;
        $this->number++;
    }
    public function get()
    {
        return self::$numberStatic . " - " . $this->number;
    }
}

After calling the page five times, the result shown in the browser will be as follows:

Construct Deconstruct 5 – 1

This is quite simple but, in the end, good for understanding the behavior of static variables under the hood.

The use of static variables is not so unusual. Just think of singleton objects or the app container of Laravel.

To avoid unexpected behavior – as in this specific example with static variables but more generally, with global objects (Laravel makes extensive use of them) – explicit re-initialization must be taken care of. In this case, the static variable is initialized in the constructor. My suggestion is to use explicit initialization of the properties in the constructor. This is because it is the developer’s responsibility to take care of the re-initialization of the variables in the case of global states (objects and variables).

class MyClass
{
    public static $numberStatic = 0;
    public $number = 0;
    public function __construct()
    {
        print "Construct\n";
        self::$numberStatic = 0;
    }
    public function __destruct()
    {
        print "Deconstruct\n";
    }
    public function add()
    {
        self::$numberStatic++;
        $this->number++;
    }
    public function get()
    {
        return self::$numberStatic . " - " . $this->number;
    }
}

We have seen just some very basic examples of the impact on the code if you are going to install and use Laravel Octane. The examples shown earlier were purposely very simple, but with the goal of being easy to understand. In the chapter where we will use Octane in a real scenario, we will cover more realistic examples.

Now we will analyze the impact on performance. So, by installing Octane, what kind of improvement could we have in terms of performance?