The slow boot times of PHP have been countered with OPcache for a long time. Still, performance often wasn’t good enough. But not anymore with Laravel Octane! I’ve implemented Laravel Octane support combined with FrankenPHP and the results are impressive.
Introduction
At SecurityHive I’m building security SaaS solutions for B2B. There’s a lot to tell you about the interesting things I’m doing over there, but for this article it’s enough to know that we use Laravel as our backend where the magic happens.
The problem
Recently, SecurityHive switched to a new mechanism to ship events from our solutions deployed at customers to the Laravel backend. This mechanism basically reads JSON logs structured with a Protobuf definition and sends them to an HTTPS endpoint (the Laravel backend).
When the backend is not reachable due to an issue on the customer’s side, in transit, or on SecurityHive’s side, the mechanism goes into a backoff mode where it keeps retrying until it succeeds. Due to the nature of the events, this backoff happens quite fast.
A situation occurred where multiple devices couldn’t reach the backend for a short while, and once it was reachable again, thousands of events were sent to the HTTPS endpoint simultaneously. While there are many things we can improve to prevent this, our pods crashed and we basically DDoS’ed ourselves.
One of the issues I identified is that PHP can be very slow. As the load on our backend grew, I saw response times of 200 ms with outliers up to 480 ms — unacceptable. Because PHP is an interpreted language, each time the code is called, the interpreter needs to translate the written code to something the CPU understands. As the project grows, it can become slower and slower.
I had already migrated our deployment from running on NGINX + PHP-FPM to a FrankenPHP container, but I hadn’t had time yet to explore Laravel Octane. I could have created a small Go application to handle the incoming requests and create a Laravel job, but I wanted to keep the developer experience as simple as possible.
Implementation of Laravel Octane & FrankenPHP
Hooray, I found an excuse to research Laravel Octane.
Laravel Octane boots the application once and keeps it in memory. This way, the code doesn’t have to be interpreted every time it’s called, which saves time and resources. It can be combined with various runners, including FrankenPHP, a modern PHP app server written in Go.
The installation was quite easy.
First, install Laravel Octane via Composer:
composer require laravel/octane
Second, configure Octane in Laravel:
php artisan octane:install --server=frankenphp
Third, set up a FrankenPHP Dockerfile, which can be as easy as the following:
FROM dunglas/frankenphp
RUN install-php-extensions \
pcntl
# Add other PHP extensions here...
COPY . /app
ENTRYPOINT ["php", "artisan", "octane:frankenphp"]
I’ve got some more magic in my Dockerfile, but I won’t bother you with it.
Basically, that’s it. You’ve got Laravel running with blazing fast performance. Right…?
Yes, but it’s very important to be aware of a new potential issue. We get fast performance by loading the code into memory. This also means we’re now vulnerable to memory leaks. It’s important to verify the way you’re using dependency injection in your Laravel project to prevent memory leaks from happening.
The results
The first time I clicked through the application I couldn’t believe what was happening. When I verified the change with the API latency graph in Sentry, I was shocked by the results.
This shows the decrease of latency of API requests
The change went live at around 19:10. As you can see in the screenshot above, the latency is reduced dramatically for both the average and p95. I’ve been able to reduce the latency from 110 ms to 17 ms (−85%) on average and from 345 ms to 29 ms (−92%) for p95.
This looks great! But does it also work in practice? Has our little self-inflicted DDoS problem been resolved?
I ran ApacheBench (ab) to verify, and the Laravel backend was able to process all requests just fine.