Erste Erfahrungen mit Laravel Octane

 

Nachdem Laravel Octane offiziell released ist, wollte auch ich in den Genuss des von Octane versprochenen Performance-Boost kommen und hatte dabei mit ein paar Details zu kämpfen. Die Probleme habe ich nicht wirklich gelöst, sondern mit zahlreichen Workarounds beseitigt.

Als Server habe ich Swoole in einem Docker-Container verwendet. Mit den ersten Erfahrungen bleibt der Performancegewinn bei den meisten Aufrufen aus, zumal ich offensichtlich an den DB-Queries oder am Caching noch Optimierungspotential habe: Octane alleine macht die Webseite nicht wesentlich schneller. Auch das Debugging wird mit Laravel Octane auf das nächste Komplexitäts-Level gehoben. Nachdem der Bootvorgang des Frameworks einmalig stattfindet und sich die Applikation dann im RAM befindet, ist es ev. nicht mehr so einfach den Code zu testen. Auch der Speicherverbrauch geht natürlich mit Octane etwas in die Höhe. Laravel Octane, siehe: https://laravel.com/docs/8.x/octane

Aktuelle Version Laravel Octane: 2.3.2 (gefunden: 30.01.2024)

Docker-Setup

Im Docker-Container habe ich folgende Zeilen hinzugefügt:

RUN pecl install --configureoptions 'enable-sockets="no" enable-openssl="no" enable-http2="no" enable-mysqlnd="no" enable-swoole-json="no" enable-swoole-curl="no"' swoole
#You should add "extension=swoole.so" to php.ini
RUN echo "extension=swoole.so" > /etc/php/${PHP_VERSION}/cli/conf.d/99-php.ini
RUN echo "extension=swoole.so" > /etc/php/${PHP_VERSION}/fpm/conf.d/99-php.ini

Der Start erfolgt über Supervisor, hier der relevante Block in supervisord.conf

[program:octane]
command=/usr/bin/php -d variables_order=EGPCS /var/www/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000
autostart=true
autorestart=true
stdout_logfile=/var/log/octane-stdout.log
stdout_logfile_maxbytes=0
stderr_logfile=/var/log/octane-stderr.log
stderr_logfile_maxbytes=0
exitcodes=0

$_SERVER

Die Variable $_Server beinhaltet nicht mehr alle Eigenschaften, z.B. habe ich $_SERVER['HTTP_X_REQUESTED_WITH'] mit \Request::header("x-requested-with") ersetzt, möglicherweise auch der Grund warum beim Einsatz eines Reverse-Proxy die Client-IP nicht mehr stimmt:

Reverse-Proxy 

Das erste Problem war der von mir eingesetzte Nginx-Reverse-Proxy. Es ist mir nicht möglich Request::ip() zu verwenden um die Client-IP auszulesen. Eigentlich müsste die Client-IP durch die Einstellung app/Http/Middleware/TrustProxies.php und folgenden Settings vom Nginx-Proxy über HEADER_X_FORWARDED_FOR geliefert werden:

class TrustProxies extends Middleware
{
    protected $proxies = '*';
    protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO;
}

Als IP wurde aber immer die IP des Docker-Containers mit Request::ip() ausgegeben. Als Workaround habe ich im AppServiceProvider: app/Providers/AppServiceProvider.php folgende Funktion hinzugefügt:

function clientIP() {
	return  explode(",",\Request::header("x-forwarded-for"))[0] ?? clientIP();
}

und diese dann für die IP-Settings anstelle von Request::ip() verwendet. Mir ist klar, dass das keine schöne Lösung ist, nach einer Stunde probieren bin ich dann dennoch diesen Kompromiss eingegangen.

meine nginx.conf schaut in etwa so aus:

server {
    listen 80 ;
    server_name _;

    location = /index.php {
        # Ensure that there is no such file named "not_exists" in your "public" directory.
        try_files /dev/null @swoole;
    }

    location / {
        try_files $uri $uri/ @swoole;
    }

    location @swoole {
        set $suffix "";
        if ($uri = /index.php) {
            set $suffix ?$query_string;
        }
        proxy_http_version 1.1;
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 120s;
        proxy_set_header Connection "keep-alive";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_set_header X-Requested-With $http_x_requested_with;
        proxy_pass http://127.0.0.1:8000$suffix;
        proxy_cookie_path / /;
    }

Trying to access array offset on value of type null: /var/www/vendor/laravel /telescope/src/Watchers /GateWatcher.php:61

Irgendwie kommt hier Telescope nicht ganz klar:

In routes/web.php verwende ich Route::group (spatie/laravel-permission) für die Berechtigungen:

Route::group(['middleware' => ['can:create_topics']], function () {

diese verursachen in Kombination mit Laravel Telescope den folgenden Fehler:

Trying to access array offset on value of type null: /var/www/vendor/laravel/telescope/src/Watchers/GateWatcher.php:61

Lösung: .env Datei

TELESCOPE_GATE_WATCHER=false

PHP Cookies

Auch verstehe ich aktuell noch nicht ganz, warum mit Laravel Octane keine Cookies mehr gesetzt werden: 

if (!isset($_COOKIE['nocache'])) {
	setcookie('cache', "NO", time()+60*60*24*365, "/");
}

Als Workaround setze ich die Cookies jetzt über Javascript:

document.cookie="cache=NO; expires=" + (new Date(new Date().getTime()+999999999999)) + ";path=/";

Auch das ist eine schnelle Hilflosenaktion, aber offensichtlich wollte ich Octane hier trotzdem schnell auf die Strasse bringen ...

currentPageResolver: Pagination

Zuletzt hatte meine Anpassung an der Pagination ein interessantes Phänomen:

Ich übersteuere an bestimmten Stellen der Seite den currentPageResolver: Wird auf der Seite dann eine Pagination ohne folgender Funktion verwendet, wird die Seitenzahl nicht mehr aktualisiert, die Pagination bleibt dann auf der zuletzt gesetzten Seite hängen: 

\Illuminate\Pagination\Paginator::currentPageResolver(function () use ($page) {
return $page;
});
siehe: Laravel : Pretty Pagination URLs

Noch ein Workaround: überall den currentPageResolver verwenden um die Seite zu setzten, auch wenn diese nicht übersteuert werden müsste. Klar die Lösung wäre eigentlich den currentPageResolver gar nicht zu verwenden, dennoch zeigt dieses Beispiel welche Themen mit Octane auftauchen könnten.

Fazit

Ich glaube ich war hier etwas voreilig mit dem Einsatz von Octane. Gepaart mit etwas Unwissenheit habe ich den Quellcode auf meiner Seite nicht gerade schöner gestaltet, aber dennoch: grundsätzlich funktioniert Octane. Auf Kosten des Arbeitsspeichers wird der Bootvorgang, bei mir ca. 40ms pro Request und nach einem einmaligen Laden der Seite, eingespart. Laravel Octane alleine beschleunigt die Seite bei mir insgesamt nur unwesentlich, bietet aber für bestimmte Abfragen die Möglichkeit mehr gleichzeitige Requests bedienen zu können. Nach mehr Performance kann mit dem Caching von statischen Files erreicht werden, siehe auch:  Webseite Stresstest - Performance messen Anfragen/Sekunde.

positive Bewertung({{pro_count}})
Beitrag bewerten:
{{percentage}} % positiv
negative Bewertung({{con_count}})

DANKE für deine Bewertung!

Fragen / Kommentare