Reactive PHP

ARKADIUSZ KONDAS

Lead Software Architect
@ Proget Sp. z o.o.

Zend Certified Engineer

Code Craftsman

Blogger

Ultra Runner

@ ArkadiuszKondas

itcraftsman.pl

Zend Certified Architect

Agenda:

  1. The genesis ...
  2. The rise ...
  3. The pleasure ...

To block or not to block (I/O)

PHP page scraper


$urls = ['amazon.com', 'ebay.com', 'allegro.pl'];

foreach ($urls as $url) {
    $content = file_get_contents(
        sprintf('http://%s', $url)
    );
}
amazon.com:    1.3739
ebay.com:      1.4950
allegro.pl:    0.4184

Total time:    3.2877

Blocking I/O

I/O is slow

Latency numbers every programmer should know

L1 cache reference ......................... 0.5 ns
Branch mispredict ............................ 5 ns
L2 cache reference ........................... 7 ns
Mutex lock/unlock ........................... 25 ns
Main memory reference ...................... 100 ns             
Compress 1K bytes with Zippy ............. 3,000 ns  =   3 µs
Send 2K bytes over 1 Gbps network ....... 20,000 ns  =  20 µs
SSD random read ........................ 150,000 ns  = 150 µs
Read 1 MB sequentially from memory ..... 250,000 ns  = 250 µs
Round trip within same datacenter ...... 500,000 ns  = 0.5 ms
Read 1 MB sequentially from SSD* ..... 1,000,000 ns  =   1 ms
Disk seek ........................... 10,000,000 ns  =  10 ms
Read 1 MB sequentially from disk .... 20,000,000 ns  =  20 ms
Send packet CA->Netherlands->CA .... 150,000,000 ns  = 150 ms

Latency numbers every programmer should know

L1 cache reference                  0.5 s         One heart beat (0.5 s)
Branch mispredict                   5 s           Yawn
L2 cache reference                  7 s           Long yawn
Mutex lock/unlock                   25 s          Making a coffee

Main memory reference               100 s         Brushing your teeth
Compress 1K bytes with Zippy        50 min        One episode of a TV show 
                                                  (including ad breaks)

Send 2K bytes over 1 Gbps network   5.5 hr        From lunch to end of work day

SSD random read                     1.7 days      A normal weekend
Read 1 MB sequentially from memory  2.9 days      A long weekend
Round trip within same datacenter   5.8 days      A medium vacation
Read 1 MB sequentially from SSD    11.6 days      Waiting for almost 2 weeks 
                                                  for a delivery

Disk seek                           16.5 weeks    A semester in university
Read 1 MB sequentially from disk    7.8 months    Almost producing a new 
                                                  human being
The above 2 together                1 year

Send packet CA->Netherlands->CA     4.8 years     Average time it takes to 
                                                  complete a bachelor's degree

C10K Problem

You can buy a 1000MHz machine with 2 gigabytes of RAM and an 1000Mbit/sec Ethernet card for $1200 or so. Let's see - at 20000 clients, that's 50KHz, 100Kbytes, and 50Kbits/sec per client.

It shouldn't take any more horsepower than that to take four kilobytes from the disk and send them to the network once a second for each of twenty thousand clients.

 So hardware is no longer the bottleneck. (Dan Kegel)

10–12 million connections (MigratoryData, 12 cores, using Java on Linux

https://mrotaru.wordpress.com/2013/10/10/scaling-to-12-million-concurrent-connections-how-migratorydata-did-it/

Non-blocking I/O

Threads

Apache

Multiplexing

kevent/select/pool

Apache vs Nginx

NodeJS - page scraper

var http = require('http');
var urls = ['amazon.com', 'ebay.com', 'allegro.pl'];

for (let i in urls) {
    http.get('http://' + urls[i], function(response){
        var str = '';
        response.on('data', function(chunk) {
            str += chunk;
        });
        response.on('end', function(){});
    });
}
allegro.pl:   0.3901
amazon.com:   0.4755
ebay.com:     0.6653

Total time:   0.6756
amazon.com:    1.3739
ebay.com:      1.4950
allegro.pl:    0.4184

Total time:    3.2877

Non-blocking I/O

select/poll

SELECT(2)                                    Linux Programmer's Manual                          

NAME
       select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

SYNOPSIS
       /* According to POSIX.1-2001, POSIX.1-2008 */
       #include <sys/select.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

       int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *timeout,
                   const sigset_t *sigmask);

DESCRIPTION
       select()  and  pselect()  allow a program to monitor multiple file descriptors, 
       waiting until one or more of the file descriptors become "ready" for some class
       of I/O operation (e.g., input possible).  A file descriptor  is  considered  
       ready if it is possible to perform a corresponding I/O operation (e.g., read(2) 
       without blocking, or a sufficiently small write(2)).

Tools in PHP

  • pthreads
  • popen()
  • proc_open()
  • pcntl_fork()
     
  • socket_select()
  • stream_select()
  • stream_set_blocking()
  • curl_multi_select()

Non-blocking I/O in PHP

<?php

$readable = [stream_socket_client(), stream_socket_client()];
$writable = $writeStreams;
$except = [];

while(true) {
    if (stream_select($readable, $writable, $except, 1)) {
        foreach ($readable as $stream) {
    
        }
        foreach ($writable as $stream) {
    
        }
    }
}

[] == ![] 

true

Welcome to JS

NodeJS

No  JS!

Event-driven, non-blocking I/O with PHP.

Architecture

EventEmmiter

EventLoop

Stream

Socket

ChildProcess

DNS

Http

HttpClient

Stomp

ØMQ

Whois

Redis

WebSockets

AR.drone

Promise

Filesystem

EventEmitter

$emitter = new Evenement\EventEmitter();

$emitter->on('data', function ($data) {
    echo $data;
});

$emitter->emit('data', [$data]);

EventLoop

  • core low level component
  • Reactor pattern :)
  • wait for streams to become
    • writeable
    • readable
  • use Factory to create instance, implementations:
    • StreamSelectLoop
    • LibEventLoop, LibEvLoop, ExtEventLoop
  • usually just past the loop around
  • run forever (unless stopped or done)
  • timers
    • once
    • perodic

EventLoop

$loop = React\EventLoop\Factory::create();

$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, 0);

$loop->addReadStream($server, function ($server) use ($loop) {
    $conn = stream_socket_accept($server);

    $loop->addWriteStream($conn, function ($conn)  {

    });
});

$loop->addPeriodicTimer(5, function () {
    $memory = memory_get_usage() / 1024;
    $formatted = number_format($memory, 3).'K';
    echo "Current memory usage: {$formatted}\n";
});

$loop->run();

Stream

  • read file content in chunks
  • types
    • readable (e.g. STDIN)
    • writeable (e.g. STDOUT)
    • duplex (e.g. TCP/IP connection)
  •  very similar to the streams found in PHP itself, but have an interface more suited for async, non-blocking I/O

Readable Stream

interface ReadableStreamInterface
{
    public function isReadable();
    public function pause();
    public function resume();
    public function close();
    public function pipe(WritableStreamInterface $dest, array $options = array());
}
Events: data, error, end, close

Writable Stream

 

interface WritableStreamInterface
{
    public function isWritable();
    public function write($data);
    public function end($data = null);
    public function close();
}
Events: drain, error, close, pipe

Stream pipe FTW

use React\EventLoop\Factory;
use React\Stream\ReadableResourceStream;
use React\Stream\WritableResourceStream;

$loop = Factory::create();

$source = new ReadableResourceStream(
    fopen('omg.txt', 'r'), $loop
);
$dest = new WritableResourceStream(
    fopen('wtf.txt', 'w'), $loop
);

$source->pipe($dest);

$loop->run();

Socket

  • abstract for stream_socket_server
  • streaming plaintext TCP/IP and secure TLS socket serve
  • more usable interface for a socket-layer server based on the EventLoop and Stream components.

Socket

$loop = React\EventLoop\Factory::create();

$socket = new React\Socket\Server(8080, $loop);
$socket->on('connection', function (ConnectionInterface $conn) {
    $conn->write("Hello " . $conn->getRemoteAddress() . "!\n");
    $conn->write("Welcome to this amazing server!\n");
    $conn->write("Here's a tip: don't say anything.\n");

    $conn->on('data', function ($data) use ($conn) {
        $conn->close();
    });
});

$loop->run();

Promise

  • placeholder for a single future result
  • states
    • pending
    • fulfilled (success)
    • rejected (failure)

Deferred

  • computation or unit of work that may not have completed yet
  • resolve($value)
  • reject($reason)
  • progress($data)

Promise

$urls = ['...', '...'];
$promises = [];
foreach($urls as $url) {
    $deferred = new Promise\Deferred();
    $request = $client->request('GET', $url);
    $request->on('response', 
    function ($response) use ($deferred, $url) {
        $response->on('end', function ($data) use ($deferred, $url) {
            $deferred->resolve($url);
        });            
    });
    $request->end();
    $promises[] = $deferred->promise();
}

Promise\race($promises)->then(function($value) {
	echo $value . " wins the race!" . PHP_EOL;
});

Promise example


$loop = \React\EventLoop\Factory::create();
$factory = new \React\Dns\Resolver\Factory();
$dns = $factory->create('8.8.8.8', $loop);

$dns->resolve($argv[1])
    ->then(
        function ($ip) { echo "Host: $ip\n"; },
        function ($error) { echo "Error: {$error->getMessage()}\n"; }
    );

$loop->run();
php bin/promise.php 4developers.pl
Host: 87.98.239.5

php bin/promise.php aaa           
Error: DNS Request did not return valid answer.

Avoid blocking

  • loop must not be blocked
  • most of functions assume blocking by default!
  • anything above 1ms is blocking (?)
  • alternative:
    • single result: Promise
    • events: Stream
  • need a blocking function?
    • fork off!

Fork off

$loop = React\EventLoop\Factory::create();

$process = new React\ChildProcess\Process('echo foo');
$process->start($loop);

$process->stdout->on('data', function ($chunk) {
    echo $chunk;
});

$process->on('exit', function($exitCode, $termSignal) {
    echo 'Process exited with code ' . $exitCode . PHP_EOL;
});

$loop->run();

Show me the example

Canonical: chat server

<?php

$client->on('data', function ($data) use ($client, &$clients) {

    $data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data));

    if ($data === '') {
        return;
    }

    $data = $client->getRemoteAddress() . ': ' . $data . PHP_EOL;

    $otherClients = array_filter($clients, function (ConnectionInterface $c) use ($client) {
        return $c!==$client;
    });

    array_walk($otherClients, function(ConnectionInterface $c) use ($data){
        $c->write($data);
    });
});

Canonical: chat server

reactphp/http-client

Text

VS

https://philsturgeon.uk/php/2013/11/12/benchmarking-codswallop-nodejs-v-php/

clue/php-redis-server

  • Redis - written in C
  • Implementations written in pure PHP
  • Simple to add new commands 
  • How fast could is PHP ...

php-smasher

https://github.com/itcraftsmanpl/php-smasher

ReactPHP on the battlefield

Ratchet

http://socketo.me/

Predis\Async

https://github.com/nrk/predis-async

React/ZMQ

https://github.com/reactphp/zmq

React/STOMP

https://github.com/reactphp/stomp

php-ar-drone

https://github.com/jolicode/php-ar-drone

php-ar-drone

$drone->on('landed', function() {
    // do something
});

$drone
    ->after(3, function () use ($drone) {
        $drone->up(0.6);
    })
    ->after(4, function () use ($drone) {
        $drone->stop();
    })
    ->after(1, function () use ($drone) {
        $drone->left(0.3);
    })

$drone->start();

Still not enough?

Life of a PHP request

  1. Parsing
  2. AST (Abstract Syntax Tree)
  3. bytcode
  4. execution

opcache

please enable opcache

Why so long ...

Life of a PHP request

  • Initialization
    • autoloading
    • memory allocation
      • DI
  • Useful execution

reactphp/http

PPM - PHP Process Manager

https://github.com/php-pm/php-pm

PPM

  • work as process manager on top of React PHP
  • written in pure PHP
  • much like PHP-FPM
  • works best with applications that use request-response frameworks like Symfony's HTTPKernel

PHP Process Manager

PPM - caveats!

  • debugging
  • memory leaks (restarts works nice)

Summary

Summary

  • React is real deal
  • React doesn't make your code faster 
  • React is ready for production
  • Consider using React when:
    • have to wait
    • have to access network

Alternative

amphp

Asynchronous Multitasking PHP: Hypertext Preprocessor

https://github.com/amphp

Amp\Thread

function slowAddition($x, $y) {
    sleep(1);
    return $x + $y;
}

$dispatcher = new Amp\Thread\Dispatcher;

$a = $dispatcher->call('slowAddition', 1, 5);
$b = $dispatcher->call('slowAddition', 10, 10);
$c = $dispatcher->call('slowAddition', 11, 31);

$comboPromise = Amp\all([$a, $b, $c]);

list($a, $b, $c) = $comboPromise->wait();

Swoole

https://github.com/swoole/swoole-src

Swoole is an event-driven asynchronous & concurrent networking communication framework with high performance written only in C for PHP.

More to read

http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html

Cooperative multitasking using coroutines (in PHP!)

by Nikita Popov

Q&A

Thanks for listening

@ ArkadiuszKondas

https://github.com/itcraftsmanpl/php-smasher

Reactive PHP

By Arkadiusz Kondas

Reactive PHP

Programowanie sterowane zdarzeniami (event-driven) jest znane i używane w aplikacjach klient / serwer oraz bibliotek asynchronicznych. RactPHP (zbliżony do node.js lub Python Twisted), przenosi PHP na nowy poziom prędkości ze wszystkimi niezbędnym do tego funkcjami. Podczas prezentacji wprowadzę pojęcie event loop, non-blocking IO i programowania asynchronicznego w PHP, a także przedstawię sposoby gdzie ta technika może przydać się w twoim stosie technologicznym.

  • 2,412