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.

Talk at PHP Usergroup Münster

8ad631306f5ab343446a967b98e64c0e?s=128

Holger Woltersdorf

November 21, 2017
Tweet

Transcript

  1. November 21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF Async

    PHP Requests & Reactive Responses with PHP-FPM
  2. HOLGER WOLTERSDORF CIO • FATHER • HUSBAND • PHP DEV

    WITH ♥ github.com/hollodotme @hollodotme
  3. None
  4. 2018 SEPTEMBER 21st & 22nd

  5. + async

  6. USE CASE A PDF CREATION SERVICE

  7. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF USE CASE (PDF CREATION) 7 WEB-SERVICE
  8. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF USE CASE (PDF CREATION) 8 WEB-SERVICE SEQUENTIAL PROCESSING IS OK(-ish)
  9. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF USE CASE (PDF CREATION) 9 WEB-SERVICE BUT WHAT IF…
  10. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF USE CASE (PDF CREATION) 10 PARALLELISM TO THE RESCUE
  11. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE TRIED… 11 exec('php "/create-pdf.php" > /dev/null 2>&1 &'); # OR shell_exec('php "/create-pdf.php" > /dev/null 2>&1 &'); # OR proc_open('php "/create-pdf.php"', $descriptorSpec ); PHP 4 - 2001
  12. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WORKS, BUT… 12 ๏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)
  13. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE TRIED… 13 $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
  14. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WORKS, BUT… 14 ๏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)
  15. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE TRIED VERY HARD… 15 $pdo#->query( "INSERT INTO queue (id, script, data) VALUES ('123', '/create-pdf.php', '{json}')" ); $ crontab -e #*/1 * * * * php "/path/to/queue-processor.php" +
  16. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WORKS, BUT… 16 ๏ 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
  17. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE TRIED TO BE CLEVER… 17 function runInBackground() { include '/create-pdf.php'; } register_shutdown_function( 'runInBackground' ); header( 'Location: /show/user/a/page', true, 301 ); flush();
  18. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WORKS, BUT… 18 ๏WEBSERVER INVOLVED ๏RESPONSE ENDS UP IN NIRVANA ๏MEMORY LEAKS FOR THE WIN! ๏PRETTY HARD ERROR HANDLING ๏NO EXECUTION TIME LIMIT
  19. DON’T TRY TO BE CLEVER!

  20. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE COULD USE… 20 pthreads
  21. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE COULD USE… 21 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
  22. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE COULD USE… 22 pcntl
  23. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE COULD USE… 23 pcntl ๏NEEDS CUSTOM PHP BUILD ๏NOT WORKING ON WINDOWS ๏BASIC KNOWLEDGE ABOUT UNIX PROCESSES NEEDED ๏PROCESS MANAGEMENT IS UP TO YOU
  24. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WE COULD USE… 24 ๏NEEDS ANOTHER PIECE OF INFRASTRUCTURE + PHP EXTENSION ๏FEATURE RICH, BUT OVERLOADED FOR SIMPLE ASYNC TASKS ๏A LOT OF SETUP FOR LOCAL DEVELOPMENT
  25. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WHAT DO WE WANT? 25 ๏ 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
  26. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF WHAT IF I TOLD YOU… 26 THERE IS A BULLET-PROOF PROCESS MANAGER SHIPPED WITH PHP … AND YOU’RE PROBABLY USING IT ALREADY.
  27. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF 27 PHP-FPM (PHP FastCGI Process Manager)
  28. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF 28 WEBSERVER
 (NGINX, APACHE) PHP-FPM HOW IT USUALLY WORKS POOL (WWW) 1..N PROCESSES PHP WEB CONTEXT
  29. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF HOW IT USUALLY WORKS 29 PID TIME COMMAND 1365 00:00:01 php-fpm: master process (/etc/php/7.1/fpm/php-fpm.conf) 1719 00:00:00 php-fpm: pool ##www 1720 00:00:00 php-fpm: pool ##www 4529 00:00:01 php-fpm: pool ##www LOOKS FAMILIAR?
  30. hollodotme/fast-cgi-client

  31. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF 31 WEBSERVER
 (NGINX, APACHE) PHP-FPM HOW IT WORKS POOL (WWW) 1..N PROCESSES POOL (BACKGROUND)
 0..N PROCESSES PHP WEB CONTEXT
  32. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF 32 WEBSERVER
 (NGINX, APACHE) PHP-FPM HOW IT WORKS POOL (WWW) 1..N PROCESSES POOL (BACKGROUND)
 0..N PROCESSES QUEUE PHP WEB CONTEXT
  33. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF PHP-FPM POOL CONFIG UNIX DOMAIN SOCKET 33 ; 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
  34. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF CONNECT TO UNIX DOMAIN SOCKET 34 <?php declare(strict_types=1); namespace YourVendor\YourProject; use hollodotme\FastCGI\Client; use hollodotme\FastCGI\SocketConnections\UnixDomainSocket; $connection = new UnixDomainSocket( # Socket path to php-fpm '/var/run/php/php7.1-fpm-background.sock', # Connect timeout in milliseconds (default: 5000) 5000, # Read/write timeout in milliseconds (default: 5000) 5000 ); $client = new Client( $connection );
  35. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF CONNECT TO NETWORK SOCKET 35 <?php declare(strict_types=1); namespace YourVendor\YourProject; use hollodotme\FastCGI\Client; use hollodotme\FastCGI\SocketConnections\NetworkSocket; $connection = new NetworkSocket( # Hostname or IP '127.0.0.1', # Port 9001, # Connect timeout in milliseconds (default: 5000) 5000, # Read/write timeout in milliseconds (default: 5000) 5000 ); $client = new Client( $connection );
  36. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF PHP-FPM POOL CONFIG NETWORK SOCKET 36 ; 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
  37. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF SEND A SYNCHRONOUS REQUEST 37 <?php declare(strict_types=1); # 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();
  38. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF RESPONSE INTERFACE 38 <?php declare(strict_types=1); 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; }
  39. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF ASYNC REQUEST (FIRE & FORGET) 39 <?php declare(strict_types=1); $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}";
  40. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF ASYNC REQUEST + WAIT FOR RESPONSE 40 <?php declare(strict_types=1); # ##... $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();
  41. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF USING RESPONSE CALLBACKS 41 <?php declare(strict_types=1); # 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 );
  42. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF USING PASS THROUG CALLBACKS 42 <?php declare(strict_types=1); # Pass through callbacks expect a string (output buffer) # ##...variadic function $request#->addPassThroughCallbacks( function( string $buffer ) { echo $buffer; } ); $requestId = $client#->sendAsyncRequest( $request );
  43. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF USING RESPONSE CALLBACKS 43 <?php declare(strict_types=1); # ##... $client#->waitForResponse( $requestId ); # Inner loop # ##... is the same as while(true) # Outer loop { if ( $client#->hasResponse( $requestId ) ) { $client#->handleResponse( $requestId ); break; } }
  44. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF MULTIPLE REQUESTS - ORDERED RESPONSES 44 <?php declare(strict_types=1); $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"; }
  45. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF MULTIPLE REQUESTS - ORDERED RESPONSES 45 # PRINTS Response #1 Response #2 Response #3 ๏ NO MATTER HOW LONG EACH WORKER TOOK TO RESPOND
  46. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF MULTIPLE REQUESTS - REACTIVE RESPONSES 46 <?php declare(strict_types=1); $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 '.'; }
  47. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF MULTIPLE REQUESTS - REACTIVE RESPONSES 47 <?php declare(strict_types=1); # ##... 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 '.'; }
  48. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF MULTIPLE REQUESTS - REACTIVE RESPONSES 48 # PRINTS .....Response #2 .......Response #3 ..........Response #1 ๏ ORDERED BY RESPONSE TIME
  49. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF MULTIPLE REQUESTS - REACTIVE CALLBACKS 49 <?php declare(strict_types=1); $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 ); } }
  50. DEMO!

  51. QUESTIONS?

  52. THANK YOU! github.com/hollodotme @hollodotme / @F9T3ch fortuneglobe.com phpug-dresden.org @phpugdd HOLGER

    WOLTERSDORF icehawk.github.io speakerdeck.com/hollodotme Slides available at:
  53. ASYNC PHP REQUESTS AND REACTIVE RESPONSES WITH PHP-FPM • NOVEMBER

    21st 2017 • PHP USERGROUP MÜNSTER HOLGER WOLTERSDORF LINKS / REFERENCES 53 ๏ PHP USERGROUP DRESDEN e.V. http://phpug-dresden.org ๏ 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