Yielding higher-performance php

Ian Littman / @iansltx

NomadPHP US March 2017

http://ian.im/nom0317

We'll cover

  • FastCGI vs. no-FastCGI
  • Generators
    • In general
    • As coroutines
  • Event Loop Concepts
  • Rewriting the same app...
    • Slim 3
    • Aerys (AMPHP)
  • Demo + benchmarks!

We Won't Cover

  • Multiprocess options
  • Other non-fpm servers
    • PHPFastCGI*
    • ReactPHP
    • Icicle (abandoned)*
    • php -S
    • mod_php

* Older versions of this talk included this info, so you can find branches using those libs on the GitHub repo I'm demoing from.

Story TIme

Standard nginx/FPM Request Model

Client (or Load Balancer)

Web Server (nginx)

FastCGI Daemon (php-fpm)

HTTP

FastCGI

Pros

  • Common
  • Safe
  • Multicore
  • Shared-nothing
  • Fast for static resources
  • Library support
    • Slim\Http Req\Res
    • Aura.Sql
  • Don't worry (much) about blocking the thread

Cons

  • No in-request parallelism
  • Could be faster
  • Not 12-factor
    • Process manager (runit)
    • nginx
    • php-fpm

Demo time: ngx + fpm + Slim 3 App

Event Loops!

Important: Don't block the loop!

  • Async I/O
  • Don't do too much computation at one time

Event Loop Interaction Methods

  • Callbacks
  • Promises
  • Generators

Callbacks/Promises

  • Very common, but...
  • Hard to follow execution flow
    • Error callback convention
    • Messages at function borders
    • Callback Hell

What about yielding?

That's what generators do.

What's a Generator?

  • Resumable function
  • Uses yield rather than (or in addition to) return
  • Values and Exceptions can be sent/thrown in
  • Incremental, iterable results
  • Behaves a bit like an Iterator

Why a Generator?

  • Less used/familiar, but...
  • ...easier to follow...
    • Cleaner exceptions
    • Cleaner message passing
  • ...and actually widely supported cross-language
    • PHP 5.5+ (but you want 7.0+)
    • Was on HHVM and predecessors before PHP
    • ES2015 (aka ES6) via function*
    • C# (.NET 4.5+)
    • Python 2.2+

NoteS

If you call a function that yields, you will not execute that function. You'll get back a generator. To execute the function, you (or the event loop) will interact with that generator.

 

Also, "return" means something different for a generator than for a normal function. The yield keyword changes everything.

You Need PHP 7 for this

  • >= 5.5 has generators, but...
    • No returning values in a generator
    • No generator delegation (yield from)
  • ...so new generator-based PHP frameworks are 7-only

A Visual Example

Generator

Parent

$g = gen(1);

$a = $g->current();

2

$b = yield $arg1 + 1;

$c = $g->send($a + 1);

$d = yield $b + 2;

5

3

function gen($arg1)

$e = $g->send($c + 1);

6

return $d + 2;

null

echo $g->getReturn();

8

Yield From Flattens Stacked Generators

Using generators as coroutines

  • Generator::send(): emulate synchronous returns
  • Yield: stop execution until you get back a result
  • Yield from: stop execution until called generator is done
  • Return: use just like normal

Generators in an Event Loop

  1. Run until blocking I/O
  2. Yield promise representing blocking I/O
  3. Event loop skips coroutine until promise is resolved
  4. Event loop send()s promise result to coroutine
  5. Repeat from 1 until coroutine is complete (return)

Event Loop Extensions

  • Not required, but highly recommended
  • ev
  • php-uv

Amphp/Aerys Request Model

Client (or Load Balancer)

Application Server (PHP 7 + Aerys)

HTTP

Look ma, twelve factor app!

Pros

  • No per-req bootstrap time
  • Fewer moving parts (12F app)
  • Async execution
  • Generator based (!pyramid)
  • Async database access
  • Fast!

Cons

  • A little fragile
  • Requires port match
  • Single-threaded
  • Plenty to refactor
  • PHP 7.0+ only*

 

* Not actually a con. Upgrade already.

What about other frameworks?

  • React - promises/callbacks
  • Icicle - maintainer now works with amphp v2

Demo Time: Amp + Aerys

AMPHP V2 incoming!

Benchmarks!

  • Target: Vultr 1GB instance, 1 core, Dallas, Ubuntu 16.10
    • Docker 17.03ce running one container at a time
    • 5.7.17 running directly on VM
    • ~200 raffles, ~375 entrants, ~1000 items
    • HTTP only
    • Benchmarked a few times after container was started for warmup, highest request count run shown here
  • Load tester: different instance, same specs
    • siege 4.0.2
    • Hitting raffle info get page with a cookie
    • siege -c 15 -t 30S -b <url> <cookie header>

Slim Benchmark

** SIEGE 4.0.2
** Preparing 15 concurrent users for battle.
The server is now under siege...
Lifting the server siege...
Transactions:                   9198 hits
Availability:                   100.00 %
Elapsed time:                   29.99 secs
Data transferred:               551.68 MB
Response time:                  0.05 secs
Transaction rate:           306.70 trans/sec
Throughput:                     18.40 MB/sec
Concurrency:                    14.84
Successful transactions:        9198
Failed transactions:            0
Longest transaction:            0.90
Shortest transaction:           0.00

RAM Usage: 61.95MB, CPU usage: 15-35%

Aerys Benchmark

** SIEGE 4.0.2
** Preparing 15 concurrent users for battle.
The server is now under siege...
Lifting the server siege...
Transactions:                   9285 hits
Availability:                   100.00 %
Elapsed time:                   29.96 secs
Data transferred:               552.11 MB
Response time:                  0.05 secs
Transaction rate:           309.91 trans/sec
Throughput:                     18.43 MB/sec
Concurrency:                    14.82
Successful transactions:        9285
Failed transactions:            0
Longest transaction:            0.24
Shortest transaction:           0.00

RAM Usage: 11.05MB, CPU usage: 25-35%

all benchmarks are bad

  • Favorable for Aerys
    • Only one core
    • Relatively low concurrency
    • Didn't turn opcache revalidation completely off
  • Favorable for Slim + nginx
    • Minimal blocking I/O (couple of database calls)
    • Database was on-instance

Thanks! Questions?

I'm @iansltx everywhere

Yielding Higher-Performance PHP - NomadPHP

By Ian Littman

Yielding Higher-Performance PHP - NomadPHP

Need to wring more performance our of your app? Have high-quality code? You may want to run it as a long-running web service. Under normal circumstances, you would end up in a callback pyramid of doom, but through the clever use of generator systems, such as AMPHP and Icicle, you can make building asynchronous code (the backbone of a long-running PHP web server) reasonably sane. We will learn how these systems use generators and how to build logic on top of these packages to build blindingly fast, maintainable apps.

  • 2,938