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.
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
:
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.
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.
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.
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.
So let’s review some of these updates and their benefits to your next dev project.
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.
kv:
in-memory:
driver: memory
config: {}
users:
driver: redis
config:
addrs:
- "localhost:6379"
It’s quite easy to connect to caching driver from 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 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).
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.
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.
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.
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:
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:
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);
}
);
}
}
}
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).
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.
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
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.
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.
If, for example, you run inside Kubernetes, RoadRunner also provides a way to check the health of your application or a specific worker pool.
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.
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.
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.
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).
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).
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
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.
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.
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.