RoadRunner: power up your PHP app

RoadRunner: power up your PHP app

About four years ago, the Research & Development software team at Spiral Scout released the first version of an application server for PHP. RoadRunner was born and almost immediately it got an overwhelming response from the PHP community. This same community now has access to a wide variety of integrations with almost every PHP framework that has been built over the years.

Our team provided an alternative HTTP/PSR-7 based on runtime, which helped an application perform better and consume much fewer resources. After all these years of development, we’ve come to a point where our initial idea has provided way more benefits than just “let’s make HTTP faster.” So what’s new in RoadRunner 2.0? Let us take you on a ride and show you how you can use them in your next project.

Good ‘ol HTTP

Let’s start with the most common example of how software engineers are using RoadRunner – HTTP workers. This mode hasn’t changed much in terms of your PHP code. You still need your worker and a bit of configuration in .rr.yaml:

yaml
http:
  address: 127.0.0.1:8080

But once you lift up the hood you can see what an impact this has. First, we rewrote the entire HTTP plugin so that it is now way more optimized, consumes less memory, and provides a better middleware architecture. We have seen in benchmarking tests that the speed has increased almost 2.5x the time compared to the first version of RoadRunner 1 version. We also saw lower latencies and much higher stability.

Benchmarking test results

You now can freely use SSL, HTTP/2 connections, as well integrate LetsEncrypt SSL certs. This means that securely running applications will take almost no time.

yaml
version: "3"

http:
  # host and port separated by semicolon
  address: 127.0.0.1:8080

  ssl:
    # host and port separated by semicolon (default :443)
    address: :8892
    redirect: false
    cert: fixtures/server.crt
    key: fixtures/server.key
    root_ca: root.crt
    acme:
  	certs_dir: rr_le_certs
  	email: you-email-here@email
  	alt_tlsalpn_port: 443
  	challenge_type: http-01
  	domains: [
    		"your-cool-domain.here",
    		"your-second-domain.here"
  	]

  # optional support for http2
  http2:
    h2c: false

In addition to that, we also included a number of features from the open source community that follows Roadrunner such as X-Sendfile header, CORS configuration, Gzip, and caching support. Our goals weren’t to recreate web servers such as Nginx, but we thought from the beginning that it was vitally important to provide feature parity for the most important types of functionality that it supports.

This included a proper static file server for your assets, with caching and ETag support.

yaml
fileserver:
  address: 127.0.0.1:10101
  calculate_etag: true
  weak: false
  stream_request_body: true

  serve:
	- prefix: "/assets"
  	root: "app/public/assets"
	- prefix: "/fuser"
  	root: "app/storage/users/avatars"

While the increase in HTTP speed might be a good enough reason for some of Roadrunners users, the software challenges we are solving at Spiral Scout and the types of complex software projects that we and the rest of the Roadrunner community are building extend way farther than the HTTP domain.

By 2023, RoadRunner now includes a number of beneficial options for vertical and horizontal scalability. While some of the benefits are easier than others to implement, overall, our end goal stayed the same, which was to make best-in-class instruments to write large-scale PHP applications and share that with the PHP community.

PHP Application

So let’s review some of these updates and their benefits to your next dev project.

Caching and Key/Value stores

Caching your data is probably the most commonly used optimization practice in PHP applications. It’s been a no-brainer for us to add such functionality to the application server since we can provide better service and unification than composer driver packages. Now if you want to cache anything inside your workers or other parts of your application you don’t need to install any extensions or drivers. The server will manage all of your storage, persistent connections, and brokers for you. From an engineering perspective, you can use the PSR-16 interface.

yaml
kv:
  in-memory:
	driver: memory
	config: {}
  users:
	driver: redis
	config:
  	addrs:
    	- "localhost:6379"

It’s quite easy to connect to caching driver from PHP:

php
use Spiral\Goridge\RPC\RPC;
use Spiral\RoadRunner\KeyValue\Factory;

require __DIR__ . '/vendor/autoload.php';

$factory = new Factory(RPC::create('tcp://127.0.0.1:6001'));

$cache = $factory->select’(‘users’);

// After that you can write and read arbitrary values:

$cache->set('totalFriends', $user->getTotalFriends());

echo $cache->get('k'totalFriends'ey');

We support a number of brokers (Redis, Memcache, BoltDB) as well as in-memory storage, which can be used to share data between PHP processes. This plugin is simple, compared to others, yet it provides a very simple way to offload some of the calculations from your code.

We have seen in numerous projects that when you combine proper caching with very fast HTTP workers, your performance is going to go way up.

But what if you need to scale horizontally or utilize a modern event-driven or microservice approach?

That’s where we have a treasure trove of ready-to-use implementations available for you. This is four years of non-stop work to complete that we will continue to diligently support and update in the years to come.

Queues, pipelines, and event-driven architectures

Queues are one of the most actively used extensions of RoadRunner besides HTTP worker pools.

This implementation fully offloads the communication within the queue broker from your PHP application. Using queues in your application opens a number of possibilities, from offloading heavy computations to the background all the way to building complex event-driven architectures.

RoadRunner manages all the queue options by itself, including durability, reconnecting, prioritization, retries, and much more.

This allows you, as a developer, to use pretty much any queue broker quickly without any additional work. The system will fill your PHP worker concurrently with task payloads and do all the heavy lifting behind the curtain.

You won’t need any drivers to build high-performance consumers from RabbitMQ, NATS, AWS SQS, Beanstalk, and Kafka. If you want to really experience what background task processing looks like without the hassle of an external broker – take a look at Memory Driver. It allows you to offload some of your processing from your main script to the background goroutines and it doesn’t require any extra configuration (meaning you can run it locally).

yaml
jobs:
  consume: [ "in-memory" ]
  pipelines:
	in-memory:
  	driver: memory
  	config:
    	priority: 10

The consumer on PHP requires very little setup since it’s similar to HTTP workers. You don’t need any external supervisor, cron restarters, etc. Straight out of the box, you get multi-threaded support with automatic task balancing and prioritization between workers. The next time you go to make 100K/s consumers, you will see how simple this has become.

gRPC

If you dabbled in the domain of microservices you probably have already heard about gRPC. This protocol is a perfect alternative to REST as you build communication between your services (in the case that the event bus is not enough for your use case). It’s way faster, provides strict data definitions, and, out of the box, can generate client code for almost every existing language.

Using gRPC in RoadRunner is really simple and isn’t very different from HTTP. The main difference is that you have to describe your services and their payloads in the form of proto files.

yaml
grpc:
  listen: "tcp://127.0.0.1:9001"
  proto:
	- "service.proto"
            - “services/*.proto”

Many dev teams will actively use gRPC in their applications since it provides sub-millisecond response times but also because it’s much easier to build convention-based API specs across multiple teams, especially in times of remote work. And as a side note, you no longer have to worry about which REST verb to use.

Temporal

Besides gRPC and Queues, RoadRunner also provides an extremely powerful instrument to build distributed applications and microservices. This is the Temporal orchestration platform.

This engine allows you to reliably call external services, exchange data between them, and, most importantly, fully guarantee the transparency of your execution. Because the implementation of the workflow is abstracted and decoupled from the rest of the application, this allows you to easily modify the flows and test it without affecting the entire application.

Moreover, the Temporal workflow engine supports workflows and activities written in multiple programming languages, including Go, Java, Python, TypeScript, and more, making it a really versatile solution for any type of complex project and tech stack. Let’s take the example where you are building a workflow related to the flow of money within an application. The following is an example solution to this type of problem:

Here is an example of the config:

yaml
temporal:
	address: 127.0.0.1:7233
 	metrics:
		# Optional, default: prometheus. Available values: prometheus, statsd
	driver: prometheus
		address: 127.0.0.1:9091

Example workflow:

php
namespace Temporal\Samples\Subscription;

use Carbon\CarbonInterval;
use Temporal\Activity\ActivityOptions;
use Temporal\Exception\Failure\CanceledFailure;
use Temporal\Workflow;

/**
 * Demonstrates long-running process to represent user subscription process.
 */
class SubscriptionWorkflow implements SubscriptionWorkflowInterface
{
    private $account;

    public function __construct()
    {
        $this->account = Workflow::newActivityStub(
            AccountActivityInterface::class,
            ActivityOptions::new()
                ->withScheduleToCloseTimeout(CarbonInterval::seconds(2))
        );
    }

    public function subscribe(string $userID)
    {
        yield $this->account->sendWelcomeEmail($userID);

        try {
            $trialPeriod = true;
            while (true) {
                // Lower period duration to observe workflow behaviour
                yield Workflow::timer(CarbonInterval::days(30));

                if ($trialPeriod) {
                    yield $this->account->sendEndOfTrialEmail($userID);
                    $trialPeriod = false;
                    continue;
                }

                yield $this->account->chargeMonthlyFee($userID);
                yield $this->account->sendMonthlyChargeEmail($userID);
            }
        } catch (CanceledFailure $e) {
            yield Workflow::asyncDetached(
                function () use ($userID) {
                    yield $this->account->processSubscriptionCancellation($userID);
                    yield $this->account->sendSorryToSeeYouGoEmail($userID);
                }
            );
        }
    }
}

Observability

We are acutely aware that using modern tools and powerful instruments is not enough to build reliable and large-scale applications. It is critically important not only to run your code but also to understand how it runs and how it fails. For this purpose, we developed a whole set of instruments for better observability of your applications.

You can start with a simple logging tool that monitors all the RoadRunner internals. This is accessible from the PHP process making it possible to write to the shared application log (extremely useful for those engineers who are running RoadRunner apps in Docker, Kubernetes, or other container systems).

php
use Spiral\Goridge\RPC\RPC;
use RoadRunner\Logger\Logger;

$logger = new Logger(RPC::create('tcp://127.0.0.1:6001'));
$logger->log("Log message…");

In addition, we built an embedded plugin to expose a TON (and we mean it) of quality metrics and analytics around your application – memory usage, execution times, number of failed requests and many more. We do this for every worker pool we manage (from HTTP to Temporal and Centrifuge).

You can also expose your own metrics by using our simple interface.

yaml
metrics:
  address: localhost:2112
  collect:
	registered_users:
  	type: counter
  	help: "Total number of registered users."
	money_earned:
  	type: gauge
  	help: "The amount of earned money."
  	labels: ["project"]

To send metrics from the application

php
use Spiral\Goridge\RPC\RPC;
use Spiral\RoadRunner\Metrics\Metrics;

$metrics = new Metrics(RPC::create('tcp://127.0.0.1:6001'));

// Increment total registered users
$metrics->add('registered_users', 1);

All the metrics are shared in a Prometheus standard, which is extremely popular and already offers many ways to visualize it. For example, you can use Grafana to get robust insights about the state of your application – sample dashboard.

Metrics

Once you start diving into more complex applications, ones where your user requests might go between multiple services – take a look at the Open Telemetry integration we built. This will help you debug your issue across multiple services or queues.

Open Telemetry

If, for example, you run inside Kubernetes, RoadRunner also provides a way to check the health of your application or a specific worker pool.

yaml
status:
  address: 127.0.0.1:2114

By accessing the health-check endpoint at “http://127.0.0.1:2114/health?plugin=http”, the DevOps team can have a real-time view of the health of the workers and can take necessary actions to keep the application running smoothly.

Need Websockets?

Since version 2.12 of RoadRunner, we offer support and access to the Centrifugo API. You may ask yourself, why would I need RoadRunner if I can connect to it directly? Unlike the typically publish functionality, RoadRunner provides a bi-directional RPC to communicate with your real-time clients. You can not only publish messages but also receive them, authenticate users, and do much more.

yaml
centrifuge:
  proxy_address: "tcp://127.0.0.1:30000"
  grpc_api_address: tcp://127.0.0.1:30000

Check this tutorial on how to build a simple chat application with persistence using the Spiral Framework.

Other plugins

It might take a while to explain each new plugin added to RoadRunner over the last few years. There are a few important ones to mention. The first is shared logging sub-systems. This allows you to easily expose your PHP logs to the container level.

Service supervisor can manage a pool of processes related to your application inside the same host/container (think like a sidecar apps).

yaml
service:
  meilisearch:
	service_name_in_log: true
	remain_after_exit: true
	restart_sec: 1
	command: "./bin/meilisearch"
  centrifuge:
	service_name_in_log: true
	remain_after_exit: true
	restart_sec: 1
	command: "./bin/centrifugo --config=centrifugo.json"

Then there are TPC plugin that will retrieve and respond from PHP apps over any port (Buggregator uses this to emulate email servers).

yaml
tcp:
  servers:
	monolog:
  	addr: 127.0.0.1:9913
  	delimiter: "\n"
	var-dumper:
  	addr: 127.0.0.1:9912
  	delimiter: "\n"
	smtp:
  	addr: 127.0.0.1:1025

Current build plan and release process

There has been a ton of work completed that are outside of new features. Not only did we optimize critical hot paths, but we also introduced Endure – a service container for the Golang application, this container helps us to focus on each plugin individually, without worrying about side effects or cross-integration of features.

We also changed the way we compile RoadRunner and make custom builds for it. There is a new instrument, Velox, that allows us to make custom builds with user plugins without needing to look at the Golang code or manually register services.

You can find the build configuration here.

Our release process now includes multiple alpha and beta versions, prior to the larger quarterly releases. Each release cycle includes not only unit tests but also integration and very heavy performance tests to ensure seamless rollups.

Future roadmap and integrations

While we are still actively looking to expand RoadRunner’s functionality and its user base via new plugins (for example we are working to support Google Pub/Sub as a queue provider), one of our main focuses of interest going forward will be the support of SAPI.

Over the next 6-12 months, we will be working to make RoadRunner capable of serving any type of PHP application, not just architectures optimized for long-running. We are talking about WordPress, Drupal, and legacy PHP applications that need help.

While this is a huge task in front of us, it will allow us to bring more modern features to a much larger pool of PHP applications that can be modernized.

Another large milestone for us is the introduction of forked PHP processes inside worker pools. Based on our internal tests we can reach up to 300% memory reduction in RAM while serving the same amount of requests.

RoadRunner is free to use

Got a project that could benefit from all the features we built in Roadrunner? You can take a look at all the community-supported integrations here and use it with Vanilla PHP (i.e. no framework required).

We also built a full-stack PHP framework called Spiral that is optimized for long-running models and provides support for every RoadRunner plugin mentioned in this article. It has been in development for over 10 years and is battle tested and well-documented. More PHP engineers are starting to use it as we begin to promote it more in the open-source community.

We are pumped to build for the open-source community and give back. At the same time, we are excited to move the PHP community forward and be a pioneer in our own way. If you are using Roadrunner in a project, please reach out and tell us how things are working out. We are always excited to hear your success stories and listen to any requests for additional features. We don’t expect any payment for using Roadrunner, and our professional services company, Spiral Scout, funds all our R&D efforts, but if you wish to support RoadRunner and help us continue the work we are doing, please check our GitHub sponsorship page and we thank you in advance.