Modern PHP (2015)

Part III. Deployment, Testing, and Tuning

Chapter 8. Tuning

By this point, your PHP application should be running alongside nginx with its own PHP-FPM process pool. We’re not done yet, though. We should tune PHP’s configuration with settings appropriate for your application and production server. Default PHP installations are like an average suit you find at your local department store; they fit, but they don’t fit well. A tuned PHP installation is a custom tailored suit prepared with your exact measurements.

Don’t get too excited. PHP tuning is not a universal cure for application performance. Bad code is still bad code. For example, PHP tuning cannot solve poorly written SQL queries or unresponsive API calls. However, PHP tuning is a low-hanging fruit that can improve PHP efficiency and application performance.

The php.ini File

The PHP interpreter is configured and tuned with a file named php.ini. This file can live in one of several directories on your operating system. If you run PHP with PHP-FPM, as I demonstrated earlier, you can find the php.ini configuration file at /etc/php5/fpm/php.ini. Oddly enough, thisphp.ini file does not control the PHP interpreter used when you invoke php on the command line. PHP on the command line uses its own php.ini file often located at /etc/php5/cli/php.ini. If you built PHP from source, the php.ini location is likely beneath the $PREFIX directory specified when you configured the PHP source files. I’ll assume you’re running PHP with PHP-FPM as described, but all of these optimizations are applicable to any php.ini file.

TIP

Scan your php.ini file for best security practices with the PHP Iniscan tool, written by Chris Cornutt.

The php.ini file uses the INI format. You can learn about the INI format on Wikipedia.

Memory

My first concern when running PHP is how much memory each PHP process consumes. The memory_limit setting in the php.ini file determines the maximum amount of system memory that can be used by a single PHP process.

The default value is 128M, and this is probably fine for most small to medium-sized PHP applications. However, if you are running a tiny PHP application, you can save system resouces by lowering this value to something like 64M. If you are running a memory-intensive PHP application (e.g., a Drupal website), you may see improved performance with a higher value like 512M. The value you choose is dictated by the amount of available system memory. Figuring out how much memory to allocate to PHP is more an art than a science. These are the questions I ask myself to determine my PHP memory limit and the number of PHP-FPM processes I can afford:

What is the total amount of memory I can allocate for PHP?

First, I determine how much system memory I can allocate for PHP. For example, I may be working with a Linode virtual machine with 2 GB of total memory. However, other processes (e.g., nginx, MySQL, or memcache) might run on the same machine and consume memory of their own. I think I can safely set aside 512 MB of memory for PHP.

How much memory, on average, is consumed by a single PHP process?

Next, I determine how much memory, on average, is consumed by a single PHP process. This requires me to monitor process memory usage. If you live in the command line, then you can run top to see realtime stats for running processes. You can also invoke thememory_get_peak_usage() PHP function at the tail end of a PHP script to output the maximum amount of memory consumed by the current script. Either way, run the same PHP script several times (to warm caches) and take the average memory consumption. I often find PHP processes consume between 5–20 MB of memory (your mileage may vary). If you are working with file uploads, image data, or a memory-intensive application, this value will obviously be higher.

How many PHP-FPM processes can I afford?

I have 512 MB of total memory allocated for PHP. I determine that each PHP process, on average, consumes about 15 MB of memory. I divide the total memory by the amount of memory consumed by each PHP process, and I determine I can afford 34 PHP-FPM processes. This value is an estimate and should be refined with experimentation.

Do I have enough system resources?

Finally, I ask myself if I believe I have sufficient system resources to run my PHP application and handle the expected web traffic. If yes, awesome. If no, I need to upgrade my server with more memory and return to the first question.

NOTE

Use Apache Bench or Seige to stress-test your PHP applications under production-like conditions. If your PHP application does not have sufficient resources, it’s wise to figure this out before you take your application into production.

Zend OPcache

After I figure out my memory allocation, I configure the PHP Zend OPcache extension. This is an opcode cache. What’s an opcode cache? Let’s first examine how a typical PHP script is processed for every HTTP request. First, nginx forwards an HTTP request to PHP-FPM, and PHP-FPM assigns the request to a child PHP process. The PHP process finds the appropriate PHP scripts, it reads the PHP scripts, it compiles the PHP scripts into an opcode (or bytecode) format, and it executes the compiled PHP opcode to generate an HTTP response. The HTTP response is returned to nginx, and nginx returns the HTTP response to the HTTP client. This is a lot of overhead for every HTTP request.

We can speed this up by caching the compiled opcode for each PHP script. Then we can read and execute precompiled opcode from cache instead of finding, reading, and compiling PHP scripts for each HTTP request. The Zend OPcache extension is built into PHP 5.5.0+. Here are myphp.ini settings to configure and optimize the Zend OPcache extension:

opcache.memory_consumption = 64

opcache.interned_strings_buffer = 16

opcache.max_accelerated_files = 4000

opcache.validate_timestamps = 1

opcache.revalidate_freq = 0

opcache.fast_shutdown = 1

opcache.memory_consumption = 64

The amount of memory (in megabytes) allocated for the opcode cache. This should be large enough to store the compiled opcode for all of your application’s PHP scripts. If you have a small PHP application with few scripts, this can be a lower value like 16 MB. If your PHP application is large with many scripts, use a larger value like 64 MB.

opcache.interned_strings_buffer = 16

The amount of memory (in megabytes) used to store interned strings. What the heck is an interned string? That was my first question, too. The PHP interpreter, behind the scenes, detects multiple instances of identical strings and stores the string in memory once and uses pointers whenever the string is used again. This saves memory. By default, PHP’s string interning is isolated in each PHP process. This setting lets all PHP-FPM pool processes store their interned strings in a shared buffer so that interned strings can be referenced across multiple PHP-FPM pool processes. This saves even more memory. The default value is 4 MB, but I prefer to bump this to 16 MB.

opcache.max_accelerated_files = 4000

The maximum number of PHP scripts that can be stored in the opcode cache. You can use any number between 200 and 100000. I use 4000. Make sure this number is larger than the number of files in your PHP application.

opcache.validate_timestamps = 1

When this setting is enabled, PHP checks PHP scripts for changes on the interval of time specified by the opcache.revalidate_freq setting. If this setting is disabled, PHP does not check PHP scripts for changes, and you must clear the opcode cache manually. I recommend you enable this setting during development and disable this setting during production.

opcache.revalidate_freq = 0

How often (in seconds) PHP checks compiled PHP files for changes. The benefit of a cache is to avoid recompiling PHP scripts on each request. This setting determines how long the opcode cache is considered fresh. After this time interval, PHP checks PHP scripts for changes. If PHP detects a change, PHP recompiles and recaches the script. I use a value of 0 seconds. This value requires PHP to revalidate PHP files on every request if and only if you enable the opcache.validate_timestamps setting. This means PHP revalidates files on every request during development (a good thing). This setting is moot during production because the opcache.validate_timestamps setting is disabled anyway.

opcache.fast_shutdown = 1

This prompts the opcache to use a faster shutdown sequence by delegating object deconstruction and memory release to the Zend Engine memory manager. Documentation is lacking for this setting. All you need to know is turn this on.

File Uploads

Does your PHP application accept file uploads? If not, turn off file uploads to improve application security. If your application does accept file uploads, it’s best to set a maximum upload filesize that your application accepts. It’s also best to set a maximum number of uploads that your application accepts at one time. These are the php.ini settings I use for my own applications:

file_uploads = 1

upload_max_filesize = 10M

max_file_uploads = 3

By default, PHP allows up to 20 uploads in a single request. Each uploaded file can be up to 2 MB in size. You probably don’t need to allow 20 uploads at once; I only allow three uploads in a single request, but change this setting to a value that makes sense for your application.

If my PHP applications accept file uploads, they often need to accept files much larger than 2 MB. I bump the upload_max_filesize setting to 10M or higher based on each application’s requirements. Don’t set this to something too large, otherwise your web server (e.g., nginx) may complain about the HTTP request having too large a body or timing out.

NOTE

If you accept very large file uploads, be sure your web server is configured accordingly. You may need to adjust the client_max_body_size setting in your nginx virtual host configuration in addition to your php.ini file.

Max Execution Time

The max_execution_time setting in your php.ini file determines the maximum length of time that a single PHP process can run before terminating. By default, this is set to 30 seconds. You don’t want PHP processes running for 30 seconds. We want our applications to be super-fast (measured in milliseconds). I recommend you change this to 5 seconds:

max_execution_time = 5

NOTE

You can override this setting on a per-script basis with the set_time_limit() PHP function.

What if my PHP script needs to run a long time? you ask. It shouldn’t. The longer PHP runs, the longer your web application visitors must wait for a response. If you have long-running tasks (e.g., resizing images or generating reports), offload those tasks to a separate worker process.

TIP

I use the exec() PHP function to invoke the at bash command. This lets me fork separate nonblocking processes that do not delay the current PHP process. If you use the exec() PHP function, it is your responsibility to escape shell arguments with the escapeshellarg PHP function.

Assume we need to run a report and generate a PDF file with the results. This task may take 10 minutes to complete. Surely we don’t want the PHP request to sit around for 10 minutes. Instead, we create a separate PHP file called create-report.php that will chug along for 10 minutes and eventually generate our report. However, our web application will take only milliseconds to spin off a separate background process and return an HTTP response, like this:

<?php

exec('echo "create-report.php" | at now');

echo 'Report pending...';

The standalone create-report.php script runs in a separate background process; it can update a database or email the report recipient upon completion. There is absolutely no reason why the primary PHP script should hold up the user experience for long-running tasks.

TIP

If you find yourself spawning a lot of background processes, you may be better served with a dedicated job queue. PHP Resque is a great job queue manager based on the original Resque job queue manager from GitHub.

Session Handling

PHP’s default session handler can slow down larger applications because it stores session data on disk. This creates unnecessary file I/O that takes time. Instead, offload session handling to a faster in-memory data store like Memcached or Redis. This has the added benefit of future scalability. If your session data is stored on disk, this prevents you from scaling PHP across additional servers. If your session data is, instead, stored on a central Memcached or Redis data store, it can be accessed from any number of distributed PHP-FPM servers.

Install the the PECL Memcached extension to access a Memcached datastore from PHP. You can now change PHP’s default session store to Memcached by adding these lines to your php.ini file:

session.save_handler = 'memcached'

session.save_path = '127.0.0.2:11211'

Output Buffering

Networks are more efficient when sending more data in fewer chunks, rather than less data in more chunks. In other words, deliver content to your visitor’s web browser in fewer pieces to reduce the total number of HTTP requests.

This is why you enable PHP output buffering. By default, PHP’s output buffer is enabled (except on the command line). PHP’s output buffer collects up to 4,096 bytes before flushing its contents back to the web server. Here are my recommended php.ini settings:

output_buffering = 4096

implicit_flush = false

TIP

If you change the output buffer size, make sure its value is a multiple of 4 (for 32-bit systems) or 8 (for 64-bit systems).

Realpath Cache

PHP maintains a cache of file paths that are used by your PHP application so it does not have to continually search the include path each time it includes or requires a file. This cache is called the realpath cache. If you are running a large PHP application that uses a lot of separate files (Drupal, Composer components, etc.), you can realize better performance by increasing the size of PHP’s realpath cache.

The default realpath cache size is 16k. It’s not obvious how to figure out the exact size you need, but here’s a trick you can use. First, bump the realpath cache size to something obnoxiously large, like 256k. Then output the actual realpath cache size at the tail end of a PHP script withprint_r(realpath_cache_size());. Change your realpath cache size to this actual value. You can set the realpath cache size in your php.ini file:

realpath_cache_size = 64k

Up Next

We’ve got a server firing on all cylinders, and we’re ready to deploy our PHP application into production. In the next chapter we’ll discuss several strategies to automate PHP application deployment.