Yielding higher-performance php
Ian Littman / @iansltx
NomadPHP US March 2017
- FastCGI vs. no-FastCGI
- 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
- Icicle (abandoned)*
- php -S
* Older versions of this talk included this info, so you can find branches using those libs on the GitHub repo I'm demoing from.
Standard nginx/FPM Request Model
Client (or Load Balancer)
Web Server (nginx)
FastCGI Daemon (php-fpm)
- Fast for static resources
- Library support
- Slim\Http Req\Res
- Don't worry (much) about blocking the thread
- No in-request parallelism
- Could be faster
- Not 12-factor
- Process manager (runit)
Demo time: ngx + fpm + Slim 3 App
Important: Don't block the loop!
- Async I/O
- Don't do too much computation at one time
Event Loop Interaction Methods
- 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+
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
$g = gen(1);
$a = $g->current();
$b = yield $arg1 + 1;
$c = $g->send($a + 1);
$d = yield $b + 2;
$e = $g->send($c + 1);
return $d + 2;
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
- Run until blocking I/O
- Yield promise representing blocking I/O
- Event loop skips coroutine until promise is resolved
- Event loop send()s promise result to coroutine
- Repeat from 1 until coroutine is complete (return)
Event Loop Extensions
Client (or Load Balancer)
Application Server (PHP 7 + Aerys)
Look ma, twelve factor app!
- No per-req bootstrap time
- Fewer moving parts (12F app)
- Async execution
- Generator based (!pyramid)
- Async database access
- A little fragile
- Requires port match
- 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!
- 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>
** 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%
** 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
I'm @iansltx everywhere
Yielding Higher-Performance PHP - NomadPHP
By Ian Littman