Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Async PHP Requests & Reactive Responses with PHP-FPM

Async PHP Requests & Reactive Responses with PHP-FPM

There are many approaches to execute PHP sub-tasks asynchronously or to parallelise PHP execution. While some solutions require extra extensions, individual PHP builds or a lot of process control management, this talk will show you how to configure and use the built-in PHP FastCGI Process Manager (php-fpm) to execute requests asynchronously in an isolated, tunable process pool and eventually handle their responses in a reactive way.

Uncon talk at PHP North West 2017

Holger Woltersdorf

September 30, 2017
Tweet

More Decks by Holger Woltersdorf

Other Decks in Programming

Transcript

  1. September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    Async PHP Requests
    & Reactive Responses with PHP-FPM

    View Slide

  2. + async

    View Slide

  3. USE CASE
    PDF CREATION SERVICE

    View Slide

  4. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE TRIED…
    4
    exec('php "/create-pdf.php" > /dev/null 2>&1 &');
    # OR
    shell_exec('php "/create-pdf.php" > /dev/null 2>&1 &');
    PHP 4 - 2001

    View Slide

  5. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WORKS, BUT…
    5
    ๏ PHP SCRIPT IS CALLED IN CLI MODE (DIFFERENT ENVIRONMENT)
    ๏ COMMAND GETS QUIET MESSY WHEN A LOT OF DATA SHOULD BE PASSED
    ๏ DATA HANDLING IN CALLED SCRIPT BASED ON $ARGV ARRAY
    ๏ RESPONSE ENDS UP IN NIRVANA
    ๏ DEBUGGING IS A NIGHTMARE (ESPECIALLY IN PRODUCTION)

    View Slide

  6. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE TRIED…
    6
    $scriptUrl = 'http://www.yoursite.com/create-pdf.php';
    $ch = curl_init();
    curl_setopt( $ch, CURLOPT_URL, $scriptUrl );
    curl_setopt( $ch, CURLOPT_FRESH_CONNECT, true );
    curl_setopt( $ch, CURLOPT_TIMEOUT_MS, 1 );
    curl_exec( $ch );
    curl_close( $ch );
    PHP 5.2.3 - 2009
    $scriptUrl = 'http://www.yoursite.com/create-pdf.php';
    $ch = curl_init();
    curl_setopt( $ch, CURLOPT_URL, $scriptUrl );
    curl_setopt( $ch, CURLOPT_FRESH_CONNECT, true );
    curl_setopt( $ch, CURLOPT_TIMEOUT_MS, 1 );
    curl_exec( $ch );
    curl_close( $ch );

    View Slide

  7. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WORKS, BUT…
    7
    ๏ WEBSERVER ALWAYS INVOLVED (= OVERHEAD + ERROR SOURCE)
    ๏ MAYBE A LOAD BALANCER INVOLVED, TOO
    ๏ AT LEAST 2 ENVIRONMENTS TO MAINTAIN
    ๏ CURL EXTENSION NEEDED
    ๏ RESPONSE ENDS UP IN NIRVANA
    ๏ CALLED SCRIPT MUST BE EXPOSED (UNDER DOMAIN’S DOCUMENT ROOT)

    View Slide

  8. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE TRIED VERY HARD…
    8
    $pdo->query(
    "INSERT INTO queue (id, script, data)
    VALUES ('123', '/create-pdf.php', '{json}')"
    );
    #/>$ crontab -e
    */1 * * * * php "/path/to/queue-processor.php"
    +

    View Slide

  9. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WORKS, BUT…
    9
    ๏ NO ON-DEMAND EXECUTION
    ๏ NEEDS A LOT OF LOCKING AND LOGGING
    ๏ RAISE CONDITIONS FOR THE WIN!
    ๏ ERRORS CAN PILE UP UNTIL SERVER IS DEAD
    ๏ HEAVY DATABASE LOAD FOR TECHNICALLY ELUSIVE DATA (HINT: BAD IDEA)
    ๏ MAINTENANCE OUTSIDE PHP PROJECT NEEDED (CRONTAB)
    ๏ HARD TO TEST

    View Slide

  10. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE TRIED TO BE CLEVER…
    10
    function runInBackground()
    {
    include '/create-pdf.php';
    }
    register_shutdown_function( 'runInBackground' );
    header( 'Location: /show/user/a/page.php', true, 301 );
    flush();

    View Slide

  11. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WORKS, BUT…
    11
    ๏ WEBSERVER INVOLVED
    ๏ RESPONSE ENDS UP IN NIRVANA
    ๏ MEMORY LEAKS FOR THE WIN!
    ๏ PRETTY HARD ERROR HANDLING
    ๏ NO EXECUTION TIME LIMIT
    ๏ WTF? - DON’T TRY TO BE CLEVER!

    View Slide

  12. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE COULD USE…
    12
    pthreads

    View Slide

  13. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE COULD USE…
    13
    pthreads
    ๏ NEEDS CUSTOM PHP BUILD
    ๏ NOT ALL EXTENSIONS ARE THREAD-SAFE
    ๏ NOT WORKING IN WEB ENVIRONMENT
    ๏ BASIC KNOWLEDGE ABOUT MULTI THREADING NEEDED
    ๏ FEATURE AND CONFIG OVERHEAD FOR SIMPLE ASYNC TASKS
    ๏ PROCESS / THREAD MANAGEMENT IS UP TO YOU

    View Slide

  14. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE COULD USE…
    14
    pcntl

    View Slide

  15. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE COULD USE…
    15
    pcntl
    ๏ NEEDS CUSTOM PHP BUILD
    ๏ NOT WORKING ON WINDOWS
    ๏ BASIC KNOWLEDGE ABOUT UNIX PROCESSES NEEDED
    ๏ PROCESS MANAGEMENT IS UP TO YOU

    View Slide

  16. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WE COULD USE…
    16
    ๏ NEEDS ANOTHER PIECE OF INFRASTRUCTURE + PHP EXTENSION
    ๏ FEATURE RICH, BUT OVERLOADED FOR SIMPLE ASYNC TASKS
    ๏ A LOT OF SETUP FOR LOCAL DEVELOPMENT

    View Slide

  17. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WHAT DO WE WANT?
    17
    ๏ MAKE ASYNC CALLS TO PHP
    ๏ EVENTUALLY GET THE RESPONSES
    ๏ NO ADDITIONAL INFRASTRUCTURE
    ๏ NO ADDITIONAL EXTENSIONS
    ๏ WEB-REQUEST LIKE DATA HANDLING (BECAUSE WE’RE USED TO IT)
    ๏ TAKE ADVANTAGE OF OPCACHE
    ๏ BACKGROUND WORKERS NOT EXPOSED TO "PUBLIC"
    ๏ TUNEABLE PROCESS MANAGEMENT

    View Slide

  18. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WHAT IF I TOLD YOU…
    18
    ๏THERE IS A BULLET-PROOF PROCESS MANAGER SHIPPED WITH PHP
    ๏…AND YOU’RE PROBABLY USING IT ALREADY.

    View Slide

  19. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    19
    PHP-FPM
    (PHP FastCGI Process Manager)

    View Slide

  20. hollodotme/fast-cgi-client

    View Slide

  21. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    WHAT I DID…
    21
    ๏ UPDATED PHP LANGUAGE LEVEL TO 7.X
    ๏ Releases v1.x for PHP 7.0.x
    ๏ Releases v2.x for PHP 7.1.x
    ๏ SPLIT CODE INTO MULTIPLE CLASSES
    ๏ ADDED UNIT AND INTEGRATION TESTS
    ๏ POLISHED THE CONNECTION, CLIENT, REQUEST & RESPONSE INTERFACES
    ๏ ADDED FEATURES FOR MULTI-REQUESTS AND LOOP INTEGRATION
    ๏ ADDED USAGE DOCUMENTATION

    View Slide

  22. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    CONNECT TO UNIX DOMAIN SOCKET
    22
    namespace YourVendor\YourProject;
    use hollodotme\FastCGI\Client;
    use hollodotme\FastCGI\SocketConnections\UnixDomainSocket;
    $connection = new UnixDomainSocket(
    # Socket path to php-fpm
    'unix:///var/run/php/php7.1-fpm.sock',
    # Connect timeout in milliseconds (default: 5000)
    5000,
    # Read/write timeout in milliseconds (default: 5000)
    5000
    );
    $client = new Client( $connection );

    View Slide

  23. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    PHP-FPM POOL CONFIG UNIX DOMAIN SOCKET
    23
    ; Pool name
    [background]
    ; Process ownership
    user = www-data
    group = www-data
    ; Socket path
    listen = /var/run/php/php7.1-fpm-background.sock
    ; Socket ownership
    listen.owner = www-data
    listen.group = www-data
    ; Process management
    pm = ondemand
    ; Maximum of children that can be alive at the same time
    pm.max_children = 100
    ; Number of seconds after which an idle children will be killed
    pm.process_idle_timeout = 10s

    View Slide

  24. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    CONNECT TO NETWORK SOCKET
    24
    namespace YourVendor\YourProject;
    use hollodotme\FastCGI\Client;
    use hollodotme\FastCGI\SocketConnections\NetworkSocket;
    $connection = new NetworkSocket(
    # Hostname or IP
    '127.0.0.1',
    # Port
    9000,
    # Connect timeout in milliseconds (default: 5000)
    5000,
    # Read/write timeout in milliseconds (default: 5000)
    5000
    );
    $client = new Client( $connection );

    View Slide

  25. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    PHP-FPM POOL CONFIG NETWORK SOCKET
    25
    ; Pool name
    [background]
    ; Process ownership
    user = www-data
    group = www-data
    ; Socket IP and Port
    listen = 127.0.0.1:9001
    ; Socket ownership
    listen.owner = www-data
    listen.group = www-data
    ; Process management
    pm = ondemand
    ; Maximum of children that can be alive at the same time
    pm.max_children = 100
    ; Number of seconds after which an idle children will be killed
    pm.process_idle_timeout = 10s

    View Slide

  26. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    SEND A SYNCHRONOUS REQUEST
    26
    # Content-type: ’application/x-www-form-urlencoded’ (default)
    $content = http_build_query( ['key' => 'value'] );
    # Simulate HTTP Verbs: GET, POST, PUT, PATCH, DELETE
    $request = new PostRequest( '/create-pdf.php', $content );
    $response = $client->sendRequest( $request );
    echo $response->getBody();

    View Slide

  27. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    RESPONSE INTERFACE
    27
    namespace hollodotme\FastCGI\Interfaces;
    interface ProvidesResponseData
    {
    public function getRequestId() : int;
    public function getHeaders() : array;
    public function getHeader( string $headerKey ) : string;
    public function getBody() : string;
    public function getRawResponse() : string;
    public function getDuration() : float;
    }

    View Slide

  28. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    ASYNC FIRE & FORGET
    28
    $client = new Client( new NetworkSocket( '127.0.0.1', 9000 ) );
    $content = http_build_query( ['key' => 'value'] );
    $request = new PostRequest( '/create-pdf.php', $content );
    $requestId = $client->sendAsyncRequest( $request );
    echo "Request sent, got ID: {$requestId}";

    View Slide

  29. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    ASYNC REQUEST + WAIT FOR RESPONSE
    29
    # ...
    $requestId = $client->sendAsyncRequest( $request );
    echo "Request sent, got ID: {$requestId}";
    # Do something in the meanwhile here ...
    $response = $client->readResponse( $requestId );
    # Blocking call until response is received or read timed out
    echo $response->getBody();

    View Slide

  30. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    USING RESPONSE CALLBACKS
    30
    # Response callbacks expect a ProvidesResponseData instance
    # ...variadic function
    $request->addResponseCallbacks(
    function( ProvidesResponseData $response ) {
    echo $response->getBody();
    }
    );
    # Failure callbacks expect a \Throwable instance
    # ...variadic function
    $request->addFailureCallbacks(
    function ( \Throwable $throwable ) {
    echo $throwable->getMessage();
    }
    );
    $requestId = $client->sendAsyncRequest( $request );

    View Slide

  31. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    USING PASS THROUG CALLBACKS
    31
    # Pass through callbacks expect a string (output buffer)
    # ...variadic function
    $request->addPassThroughCallbacks(
    function( string $buffer ) {
    echo $buffer;
    }
    );
    $requestId = $client->sendAsyncRequest( $request );

    View Slide

  32. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    USING RESPONSE CALLBACKS
    32
    # ...
    $client->waitForResponse( $requestId ); # Inner loop
    # ... is the same as
    while(true) # Outer loop
    {
    if ( $client->hasResponse( $requestId ) )
    {
    $client->handleResponse( $requestId );
    break;
    }
    }

    View Slide

  33. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    MULTIPLE REQUESTS - ORDERED RESPONSES
    33
    $requestIds = [];
    $requestIds[] = $client->sendAsyncRequest( $request1 );
    $requestIds[] = $client->sendAsyncRequest( $request2 );
    $requestIds[] = $client->sendAsyncRequest( $request3 );
    echo 'Sent requests: ' . implode( ', ', $requestIds ) . "\n";
    # Do something else here in the meanwhile ...
    # Blocking call until all responses are received or read timed out
    foreach ($client->readResponses(5000, ...$requestIds) as $response)
    {
    echo $response->getBody() . "\n";
    }

    View Slide

  34. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    MULTIPLE REQUESTS - ORDERED RESPONSES
    34
    # PRINTS
    Response #1
    Response #2
    Response #3
    ๏ NO MATTER HOW LONG EACH
    WORKER TOOK TO RESPOND

    View Slide

  35. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    MULTIPLE REQUESTS - REACTIVE RESPONSES
    35
    $requestIds = [];
    $requestIds[] = $client->sendAsyncRequest( $request1 );
    $requestIds[] = $client->sendAsyncRequest( $request2 );
    $requestIds[] = $client->sendAsyncRequest( $request3 );
    echo 'Sent requests: ' . implode( ', ', $requestIds ) . "\n";
    # Do something else here in the meanwhile ...
    while ( $client->hasUnhandledResponses() )
    {
    # read all ready responses
    foreach ( $client->readReadyResponses() as $response )
    {
    echo $response->getBody() . "\n";
    }

    echo '.';
    }

    View Slide

  36. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    MULTIPLE REQUESTS - REACTIVE RESPONSES
    36
    # ... is the same as
    while ( $client->hasUnhandledResponses() )
    {
    $readyRequestIds = $client->getRequestIdsHavingResponse();
    # read all ready responses
    foreach ( $readyRequestIds as $requestId )
    {
    $response = $client->readResponse( $requestId );
    echo $response->getBody() . "\n";
    }

    echo '.';
    }

    View Slide

  37. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    MULTIPLE REQUESTS - REACTIVE RESPONSES
    37
    # PRINTS
    .....Response #2
    .......Response #3
    ..........Response #1
    ๏ ORDERED BY RESPONSE TIME

    View Slide

  38. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    MULTIPLE REQUESTS - REACTIVE CALLBACKS
    38
    $client->waitForResponses(); # Inner loop
    # ... is the same as
    while ( $client->hasUnhandledResponses() ) # Outer loop
    {
    $client->handleReadyResponses();
    }
    # ... is the same as
    while ( $client->hasUnhandledResponses() ) # Outer loop
    {
    $readyRequestIds = $client->getRequestIdsHavingResponse();
    foreach ( $readyRequestIds as $requestId )
    {
    $client->handleResponse( $requestId );
    }
    }

    View Slide

  39. QUESTIONS?

    View Slide

  40. THANK YOU!
    github.com/hollodotme
    @hollodotme / @F9T3ch
    fortuneglobe.com
    phpug-dresden.org
    @phpugdd
    HOLGER WOLTERSDORF
    icehawk.github.io
    speakerdeck.com/hollodotme
    Slides available at:

    View Slide

  41. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • September 30th 2017 • PHPNW17 • Manchester HOLGER WOLTERSDORF
    LINKS / REFERENCES
    41
    ๏ PHP USERGROUP DRESDEN e.V. http://phpug-dresden.org
    ๏ PHP Developer Day 2017: https://bit.ly/PHPDD17EN
    ๏ Asynchronous cURL Requests: http://www.paul-norman.co.uk/2009/06/asynchronous-curl-requests/
    ๏ Arne Blankerts’ talk about nodejs + PHP: https://thephp.cc/dates/2016/02/confoo/just-married-node-js-and-php
    ๏ Original PHP FastCGI Client by Pierrick Charron: https://github.com/adoy/PHP-FastCGI-Client/
    ๏ PHP FastCGI Client: https://github.com/hollodotme/fast-cgi-client
    ๏ PHP FastCGI Client Demo: https://github.com/hollodotme/fast-cgi-client-demo
    ๏ FastCGI Specification: http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html
    ๏ Experimental use-case with rabbitMQ: https://hollo.me/php/experimental-async-php-volume-2.html

    View Slide