Fixing the three issues previously mentioned will certainly improve your image handling performance, but there’s a less apparent problem hidden in the LHNMPRR stack.
Let’s assume a 10MB image is uploaded by one of your users. This is how it would flow through your stack if you designed it exactly as described in this book.
Deconstructing a file upload
Moving a 10MB image through the LNHMPRR stack
1. The user kicks the process off by uploading their file, via web browser or mobile app, by sending an HTTP POST with the file (+10MB Network Bandwidth). The HAProxy load balancer receives this file upload and immediately starts streaming it to one of the nginx
Case Study: Optimizing image handling in PHP 184 App Servers (+10MB Network Bandwidth). HAProxy does not buffer any of the data from the client in memory or onto disk, it passes straight through.
2. Nginx begins receiving the file upload from HAProxy and begins buffering it into memory. Depending on the size of client_body_buffer_size(8KBout of the box), nginx will spill over to disk and stop buffering the file in memory until the entire file has been transferred. (+10MB Disk Write I/O)
3. Once the file upload has been completely transferred from HAProxy, nginx will begin reading the file back (+10MB Disk Read I/O) as it transfers it to PHP-FPM using FastCGI (+10MB Local Network Bandwidth). Since PHP-FPM is running locally and (hopefully) using Unix Sockets, this is a quick operation.
4. PHP-FPM notices the file upload and begins streaming it, you guessed it, back onto disk (+10MB Disk Write I/O). This PHP-FPM worker will be stuck in a “busy” state and your PHP code will not begin executing until the entire file is received from nginx.
5. Once the file upload finally makes it into PHP and your code starts running, the ImageMagick resizing runs, reading the file into memory twice to process it. (+20MB Disk Read I/O, +20MB Memory).
It turns out, moving that image around actually consumes far more than just 10MB of resources! In fact, at the end of the day, it takes 50MB of Disk I/O, 30MB of Network Bandwidth, and 20MB of Memory. Crazy! Imagine if you were working with videos!
Resource Amount
Network Bandwidth 30MB
Disk I/O 50MB
Memory 20MB
What an inefficient process that just seems to juggle the exact same data around from system to system. We can optimize this by avoiding Disk I/O and trying to keep the data in memory for as long as possible.
There are two places the file goes in and out of memory— the first is during the nginx buffering process and the second is when it’s received by PHP. We’ll tackle each one individually.
On the nginx side, we can change theclient_body_buffer_size setting to something larger than8KB, which would mean larger amounts of data staying being buffered in memory before hitting disk.
Instead of doing it this way, I prefer to keepclient_body_buffer_sizeat the default and setup tmpfs as covered in Chapter 5. Withtmpfs setup, the client_body_temp_pathsetting can be changed to the /tmp directory, optimizing out the Disk I/O overhead while still keeping the nginx memory footprint low.
On the php side, it’s easy since the hard work has been done for us with thetmpfschange. If we just modify php.iniand changeupload_tmp_dirto somewhere in/tmp, the file data just gets written back out into memory when it’s moved from nginx to PHP. Not ideal, but much better than getting pushed back-and-forth from disk 3 times.
Case Study: Optimizing image handling in PHP 185
Unlike HAProxy, when using nginx with the HTTP Proxy Module or FastCGI, the entire client request (from a load balancer or even directly from a web user) will be buffered in nginx before moving upstream— the data will never be streamed in real- time. On the plus side, this prevents Slowloris attacks¹³⁷, but will break PHP’s file upload progress meter. Nginx offers aUpload Progress Module¹³⁸if upload progress is an essential feature.
The Nginx HTTP Upload Module
Even with both of thetmpfschanges described above, it still feels gross to me because no matter which way you slice it, the data gets copied around more times than it needs to be. And,tmpfs won’t work for everyone! If you’re handling very large file uploads (i.e, videos), you don’t really want those large files buffered into memory anyways.
The more efficient, albeit more complicated and involved, solution is to use theHTTP Upload Module¹³⁹ that comes with nginx. Instead of sending the raw file upload to PHP, the HTTP Upload Module will just send the file location to PHP, completely avoiding the double copy. If you’re using the Dotdeb Apt Repository as mentioned in Chapter 5, great news— you just need to install nginx-extrasto get the HTTP Upload Module. Otherwise, you’ll have to compile it from source, which I’ll leave as an exercise for the reader.
You can quickly determine if you have the HTTP Upload Module withnginx -V and grep. It will show1if you have the module,0if you don’t.
1 > nginx -V 2>&1 | grep -c "nginx-upload-module" If you don’t have it, install it withapt-get.
1 > apt-get install nginx-extras
I had some trouble installing nginx-extras on Ubuntu 12.04 (because of perlapi
errors), but was able to install it fine on Debian Squeeze. If you’re on Ubuntu, you may need to compile nginx from source to get the HTTP Upload Module.
Hold up, before you get going, the module needs to be configured to intercept the file uploads. Pop openmy_app.conffrom Chapter 5 and add the followinglocationblock.
¹³⁷http://en.wikipedia.org/wiki/Slowloris
¹³⁸http://wiki.nginx.org/HttpUploadProgressModule
Case Study: Optimizing image handling in PHP 186 1 > vi /etc/nginx/sites-available/my_app.conf 2 3 location /upload { 4 upload_pass /index.php; 5 upload_store /tmp/nginx/upload 1;
6 upload_set_form_field $upload_field_name.name "$upload_file_name";
7 upload_set_form_field $upload_field_name.content_type "$upload_content_type"; 8 upload_set_form_field $upload_field_name.path "$upload_tmp_path";
9
10 # Delete the file if PHP returns any of these errors 11 upload_cleanup 400 404 499 500-505;
12 }
Alright, all configured, just have to reload nginx and create the temporary directories needed forupload_store.
1 > service nginx reload
2 > mkdir -p /tmp/nginx/upload/{1,2,3,4,5,6,7,8,9,0}
Lastly, the only change we have to make to the code is to use the$_POST["file_path"]variable instead of $_FILES["upload"]["tmp_file"].
1 <?php 2
3 class Image_Controller extends Controller { 4
5 // Image upload is posted to this method
6 public function create() { 7
8 if (isset($_POST["file_path"])) { 9
10 $tmp = $_POST["file_path"]; 11
12 // Create the Scaled 600x400 Size
13 exec("convert -resize 600x400 {$tmp} /u/scaled.jpg"); 14
15 // Create the 150x150 Thumbnail
16 exec("convert -thumbnail x300 -resize 150x< -resize 50%
17 -gravity center -crop 150x150+0+0 +repage
18 {$tmp} /u/thumbnail.jpg"); 19
20 // Delete the temporary file
21 @unlink($tmp);
22 }
23 } 24 }
Case Study: Optimizing image handling in PHP 187 We also need to change our form to POST the file to/uploadinstead of the normalindex.php endpoint (specified by thelocationblock that we added tomy_app.conf).