Async Expressive with Swoole

Have you used Node.js?

For those of my readers unfamiliar with Node.js, it's a server-side JavaScript framework that provides the ability to create, among other things, network services. To do so, it provides an event loop, which allows for such things as asynchronous processing.

In the PHP ecosystem, a group of Chinese developers have been creating an extension that provides many of the same capabilities as Node.js. This extension, called Swoole, allows you to create web servers with asynchronous capabilities. In many cases, the asynchronous capabilities are handled via coroutines, allowing you to write normal, synchronous code that still benefits from the asynchronous nature of the system event loop, allowing your server to continue responding to new requests as they come in!

We've been gradually adding and refining our Swoole support in Expressive, and recently issued a stable release that will work with any PSR-15 request handler. In this post, I'll enumerate what I feel are the reasons for considering Swoole when deploying your PHP middleware application.

I feel there are three key advantages to Swoole, and, by extension, any async PHP runtime:

  • Application-specific servers
  • Performance
  • Async processing

Application-specific servers

There are a few general architectures for applications:

  • A single web server sitting in front of many web applications.
  • A single web server sitting in front of a single web application.
  • A load balancer sitting in front of many servers. Some servers might serve the same application, to provide redundancy. (Often, today, these may even be identical docker containers.)

nginx serving many PHP sites

The first scenario is common in internal networks and development, and in many shared hosting scenarios. It's generally considered less secure, however, as a vulnerability in one application can potentially escalate to affect all applications hosted on the server. Additionally, it means that any updates to PHP versions must be tested on all applications, which often means updates are few and far between — which is also problematic from a security standpoint.

When you want to isolate the environment, you'll move to a single web server, single PHP application model:

nginx serving a single PHP site

And when you start scaling, this becomes a load balancer sitting in front of many of these web server/PHP application pairs:

A load balancer in front of many nginx+php sites

In each of these last two scenarios, there's one thing I want to point out: your application consists of at least two distinct services: the PHP processes, and a web server.

You may have other services as well, such as an RDBMS or document database, cache, search, etc. But generally these are on separate servers and scaled separately. As such, they're outside of this discussion.

In these scenarios, this means each "server" is actually a composite. And when you are adding redundancy to your architecture, this adds significant complexity. It's one more process on each and every node that can fail, and additional configuration you need when deploying.

When we start thinking about microservices, this becomes more problematic. Microservices should be quick and easy to deploy; one service per container is both typical and desired.

What Swoole lets us do is remove one layer of that complexity.

A load balancer in front of php servers

We can have a service per container, and that container can be built with only PHP. We start the Swoole HTTP server, and it's ready to go. We then tell the reverse proxy or load balancer how to route to it, and we're done.

This is useful in each of the scenarios, including the one web server/mulitiple applications scenario, as we can have different PHP runtimes per application. Our "web server" becomes a reverse proxy instead.

Application-specific servers allow us to simplify our deployment, and ship microservices quickly.

Performance

Remember when PHP 7 came out, and it was like doubling the performance of your application?

What if you could do that again?

In our initial benchmarks of Expressive applications, we found that they performed four times better under Swoole than under a traditional nginx+php-fpm pair. More interesting: when benchmarking with a high number of concurrent requests, we also found that Swoole had fewer failed requests. This means you get both better performance and better resilience!

And the hits keep rolling in: when we enabled Swoole's coroutine support and benchmarked endpoints that made use of functionality backed by that coroutine support, we observed up to a ten-fold increase!

The coroutine support covers primarily network I/O operations. As such, operations that hit cache servers, use PDO, or make web requests benefit from it immediately, with no changes to your code.

Swoole makes this possible in a couple of ways. First, because you are firing up a server exactly once, you lose the price of bootstrapping your application that you normally incur on each and every request; your application is bootstrapped from the moment you start accepting requests. Bootstrapping often accounts for the greatest single amount of resource usage in your application.

Second, Swoole runs as an event loop, just like Node.js, allowing it to defer processing of long-running requests in order to respond to new, incoming requests. This leads into my last point.

Async processing

Swoole's event loop provides async functionality to PHP applications. While a number of userland libraries have popped up over the past five years or so that provide async capabilities for PHP, Swoole's is done as a native C extension, and works regardless of the operating system.

When you have an event loop, you can defer processing, which allows the server to respond to additional requests. Commonly, deferment can be explicit:

public function handle(ServerRequestInterface $request) : ResponseInterface
{
    $ts = new DateTimeImmutable();
    \Swoole\Event::defer($this->createCacheDeferment($ts));
    return new EmptyResponse(202);
}

public function createCacheDeferment(DateTimeImmutable $ts) : callable
{
    return function () use ($ts) {
        sleep(5);
        $now = new DateTimeImmutable();
        $item = $this->cache->getItem('ts');
        $item->set(sprintf(
            "Started: %s\nEnded: %s",
            $ts->format('r'),
            $now->format('r')
        ));
        $this->cache->save($item);
    };
}

In this example, we calculate the content to return, defer caching, and return a response immediately. This means your user does not need to wait for you to finish caching content.

Logging is another use case. In the Expressive Swoole bindings, we do access logging after we mark the response complete. This ensures that logging does not impact response times.

Another use case is webhooks. Your application can accept a payload immediately, but finish processing of it after sending the response back to the client.

Swoole also provides async-enabled versions of common filesystem operations, Mysql, Redis, and an HTTP client. In each of these, you provide a callback indicating what should be done once the operation is complete:

use Swoole\Http\Client as HttpClient;

$client = new HttpClient('https://example.com');
$client->setHeaders([
    'Accept' => 'application/json',
    'Authorization' => sprintf('Bearer %s', $token),
]);

// Make the request, telling it what code to execute once
// it is complete:
$client->get('/api/resource', function ($response) {
    // process the response 
});

// This code executes before the request completes:
$counter++;

Code like the above has led to the term "callback hell" when you have many such deferments that depend on each other. So, what do you do if if you want your code to be "non-blocking", but don't want to write callbacks all the time? Well, recent versions of Swoole allow you to enable coroutine support for most I/O operations. What this means is that you can write your code just like you would in a synchronous environment, but whenever code that triggers a coroutine occurs, the server will advance the event loop, allowing it to answer additional requests before the current one completes its work, and then resume execution once it has.

// This spawns a coroutine:
$statement = $pdo->query($sql);

Async functionality may not directly improve the performance of your application, but it will let your application answer more requests, allowing you to handle greater volumes of traffic!

zend-expressive-swoole

We released zendframework/zend-expressive-swoole 1.0.0 two weeks ago. This library acts as a zend-httphandlerrunner RequestHandlerRunner implementation, which means:

  • It can be used with any PSR-15 application.
  • It can be used with any PSR-7 implementation.

In other words, if you want to use Swoole with the upcoming Slim 4 or with equip/dispatch or with northwoods/broker or any of the myriad PSR-15 dispatch systems out there, you can.

The library provides some interesting features for users:

  • Serving of static resources, with HTTP client-side caching headers.
  • Configurable logging.
  • Abiility to restart worker processes.

I've been running applications on versions of it for the past two months, and have noted that it has been stable and reliable. I definitely think it's worth giving it a spin!

Fin

I'm really excited about the possibilities of Swoole and other async systems, as I think they afford us better performance, better reliability, and the ability to defer functionality that doesn't need to complete before we respond to clients. I'd love to hear YOUR experiences, though, particularly in the form of blog posts! Send me a link to a blog post via a comment, or by tweeting at me, and I'll add it to the ZF newsletter.

Updates

  • 2018-10-17: Fixed typo in first sentence.