The problem
There are multiple ways to optimize. For example, if there is a slow mysql query, one can use
asynchronous queries
offered by the mysqli module and
mysql_nd. This assumes of course that there
is something else which can run during that time.
If you are calling an external webservice which can be a bit slow, you can use
guzzlehttp/guzzle which supports asynchronous
requests via promises, which can be seen as "block as late as possible".
For other tasks, one solution can be to queue jobs and have the user collect the result later. This can keep the
main process quite short and has the added benefit of doing the heavy loading on maybe even a different server and
if the concept of the software allows it, even during idle times - e.g. at night.
But sometimes you need the result within a few seconds or faster - and even within the main process - but need that
process to not take too long.
Solutions
In this blog post, we're looking solely into how to handle background jobs which we call directly from the parent job and that run in parallel to the parent job. No queues involved - not that queues are bad - they are just another topic and we want to focus. PHP has multiple ways which allow us to spawn a child process and use it's output. The manual describes them but lacks a bit of hinting when and how to use which of them. And there are even more options which I will describe briefly first.
AJAX
Obviously, loading results asynchronously with one or more subsequent requests which contain only the data, can and most likely will speed up the initial request because it will not need to do the processing itself. If your application is focused on the front end, this is a good way to go, there are many frameworks supporting you, you can even build SPAs (Single Page Applications) which only load one page and from then on just do requests for the data. However, this uses the frontend to trigger parallel running processes on one or more servers, so this is out of scope of this article.
file_get_contents, fopen or curl of "http://localhost/..."
One could think that what we can do in the frontend, we can also do in the backend, right? So, building together a
website by calling a few data endpoints and then combining them doesn't seem too bad, right?
Well, don't do it. Just don't. First of all, you don't win anything here - the operations are blocking, that means,
if you do this e.g. three times in a request, the user will wait as if they have done 4 HTTP requests, one after the
other.
But more importantly, you will exhaust the webserver's resources. The webserver and/or the php-fpm process are
configured with a maximum number of children to run in parallel or requests to process in parallel. You will not
only reach this limit twice as fast with that approach, you will most likely cause a situation close to a deadlock.
Imagine, we have said limit set to 10. Now, 9 processes are already busy and your new request gets the 10th slot.
It now tries to open another request. The webserver will queue that request until one of the workers is free to
accept the next request. More precisely one of the other nine, as your request will wait (almost) indefinitely for
that moment and can't be finished before itself of course.
The more such child-requests your application is sending and the higher the load of the webserver is, the more
processes will hang around waiting for a slot to execute until almost all are busy with doing nothing.
Also spawning such requests as curl background processes with the methods described below, does not help here since
you connect to localhost. I've seen this taking whole websites offline more than once over the years. Please don't
go there. Not even just a bit.
exec / shell_exec / backtick-operator / passthru / system
All of them take a string as a parameter which is basically the command one would enter into the command line. The differences between the PHP functions are mostly about how they handle output and the return status of the command. What they have in common is that they are blocking operations by default. Let me explain with a bit of code:
// getting a large file listing $output = []; exec("ls -la /var/tmp", $output); // Program code // [...] // template code $template->assign("filelist", $output);
This will sadly not have the "ls" run in the background while the rest of the application is doing it's business and
then magically provide the result when we use it first and assign it to a template variable.
The script will actually wait until the full listing is computed and handed over to the php process. It will be a
list of output lines in the $output variable.
exec in combination with &
I didn't know this "fire-and-forget" solution until a few years back. If you use exec(), have the & sign at the end of the command, redirect the output to a file or /dev/null and don't pass an array to receive the output of the command, it will be run in the background and the php script will continue.
exec("pngcrush --brute background.png > /tmp/out.txt &");
This comes with two drawbacks:
You can't control anything and you can't use the output. Of course you can script a bit around it and look for the
process ID and look into the content of the file the ouput it redirected into, but that's a bit cumbersome and prone
to faults.
But even worse is, that if the main process is terminated (through an error or just by finishing regularily), the
child process is terminated immediately as well, so you need to either sleep() and periodically check for the child
process to be around - which artificially prolongs script execution - or to choose offloading only so many and small
scripts that the main process is guaranteed to be finished.
You can cheat a bit here by returning the response to the user early and then secretly wait during script shutdown
but it is still a bit risky. For these cases, a queue and decoupled worker processes might be a better solution.
proc_open
This set of functions allows for a bit more complex management of child processes. You can actually have the
processes run in the background and at times of your choice communicate with them by writing data to them or read
from them. This is achieved in a way similar to files. The
manual for proc_open gives a good example on
how to use it. Generally it is a good approach to wrap all this into a class so that the actual application around
it does not need to know about the internals.
When using this to retrieve data from a slow source, be aware that any output the child process generates will be
written into a buffer of usually only a few kilobytes in size. As soon as this buffer is full, the child process
will halt. That means, if your child process creates output, you have to periodically read from it to keep it
running. If you have multiple such processes, it is also advisable to create some logic that looks after these
processes and lets the main process run until they are all finished.
At my current employer, we had these use cases multiple times, so I created a tiny library called
"parallel-process-dispatcher" to achieve them in a clean and reusable way.
I was thankfully allowed to open source it recently, so you can actually find it on
github.com and
packagist.org and use it yourself.
fastbill/parallel-process-dispatcher
With this library you can easily cover different use-cases, a few of them I want to describe here:
per user cronjobs
You can split the execution of cronjobs by e.g. user ID and run them in parallel and not go over the payload one by one. With this you can put all the CPUs on the server to work and finish the job approx. $numbersOfCpus times faster. This has the added benefit of reducing memory leaks because after every processed user, the child process exits and will free the memory.
single-threaded linear approach
foreach (getAllUserIds() as $userId) { doCronjobWorkload($userId); }
Parallel approach
if ($argc === 1) { $dispatcher = new Dispatcher(4); foreach (getAllUserIds() as $userId) { $dispatcher->addProcess(new Process('php cronjob.php ' . $userId)); } $dispatcher->dispatch(); die(); } doCronjobWorkload($argv[1]);
background workers listening on a queue
When using a queue - for example Redis - you can have one master process spawn an exact number of workers which will either listen on the queue or do work supplied by the queue. Depending on the work it would also usually be advisable to set the number of max. running workers to the number of CPUs. Start the following script via cron every minute and you will have 4 workers active - or listening on an empty queue with a short pause of up to a minute every 1000 spawned workers.
if ($argc === 1) { // no cli parameter = master process invocation if (file_exists('master.lock')) { die(); } touch('master.lock'); $dispatcher = new Dispatcher(4); for ($i = 0; $i < 1000; $i++) { $dispatcher->addProcess(new Process('php worker.php ' . $i)); } $dispatcher->dispatch(); unlink('master.lock'); die(); } $work = $redis->brpoplpush('queue', 'stack-' . $argv[1], 60); if (false === $work) { // empty queue - after 1 minute of waiting die(); } doWork($work);
heavy load background tasks of web applications or web services
The list of possible sub tasks for this use case is probably quite long. At FastBill for example we mostly put the generation of PDF files and the execution of webhooks to the background so that the rest of the script can continue. Although the results of the jobs do matter, there is more to do for the main script, so we wrap them into processes and only block the main process the moment we need the result.
// early in the program we start the job: $pdfProcess = new Process('php generatePdf.php ' . $id); $pdfProcess->start(); // business logic skipped // later we wait for and collect the result while (!$pdfProcess->isFinished()) { usleep(100000); } $result = $pdfProcess->getOutput();
In case of multiple such processes it might be advisable to use a Dispatcher instance to make sure that the server resources are not exhausted.
OpenSource
So this was the first project I published as open source and it felt good. Should have done it earlier. My thanks go
out to my employer
FastBill, especially to
@roritharr for supporting OpenSource software and granting my colleagues
and me to publish our work products and also to my colleague @arnebahlo
for walking me through and showing a few shortcuts.
And I can say already that there will be another project out soon, it uses the fastbill/parallel-process-dispatcher
so we had to opensource that one first. It will be a reverse code coverage tool which can help greatly when
refactoring a huge legacy application. Stay tuned...