Tuning PHP’s FastCGI Process Manager (PHP-FPM)

The PHP FastCGI Process Manager (or PHP-FPM for short) is now the standard means of deploying the PHP interpreter. Unlike CGI, the FastCGI executable remains resident in memory even after the request is done and unlike storing an interpreter in a server module (a la mod_php) the interpreter only appears in memory once with its worker processes only being scaled up or down to meet the capacity requirements for the actual PHP work. Considering this it may be useful to understand how to configure the FPM daemon to best suit your needs.

Content

FastCGI Pools

When it comes to PHP-FPM configuration, you have the standard php.ini settings (which aren’t covered here since that’s common with both mod_php and PHP as a CGI) and then you have settings made to “pools.”

A pool is a collection of FPM worker processes that share common characteristics and application purpose. The ideal scenario for pools is the traditional model where you have a single PHP-FPM instance serving PHP requests for multiple virtual hosts where each virtual host either gets its own pool or points towards a generic “common” pool. In this situation, pools give the administrator a way to define each application’s behavior and resource constraints.

Listening Socket Configuration

Configure The Connection Protocol…

You have two choices when it comes to how you want to your application to connect to its pool: Unix Domain Sockets or TCP.

In general, you should always use Unix Domain Sockets unless you have a particular reason to use TCP. TCP is fine but contains a lot of overhead in terms of packet acknowledgement, protocol encapsulation and the number of context switches required per transaction. The only time you should use TCP is when communication with FPM must go over the network. If network accessibility isn’t a concern, you should always use Unix domain sockets.

The only scenario I’m aware of where TCP is probably preferable would be a scenario where access to PHP-FPM itself needs to be load balanced. For instance, let’s say you have an internet website accessed through a load balancer as an edge device with static files delivered via CDN. In that scenario, the backend servers need to be network accessible (since they may exist on different physical nodes). In that scenario the website can be accessed by the load balancer communicating directly to the PHP-FPM on member nodes over TCP.

Configuring a TCP socket for a given pool is as simple as changing the listen directive to read something similar to:

listen = 192.0.2.200:9000

Configuring a Unix Domain Socket is as simple as setting listen to the path to the socket prefixed with unix: for instance:

listen = /run/php/php7.0-fpm.sock

Securing The TCP Socket…

Once FPM is accessible over the network you may wish to limit the systems that can access FastCGI to eliminate unnecessary attack vectors. Usually I would suggest modifying the firewall instead of doing some sort of application level host-based access control (HBAC) since that would be a common repository of all such controls and thus be more easily reviewable/discoverable.

There is a use case for FPM though. In the age of microservices you may have several PHP-FPM instances spread out over an arbitrary number of nodes with that number and its distributions varying wildly and dynamically (such as with autoscaling). In such an environment you can’t, for example, easily tell which load balancers should be able to connect to which nodes on which ports. To work around that you can use FPM’s native HBAC since you’ll likely need to manage PHP-FPM in other areas as well.

Configuring this HBAC is pretty simple. Once you’ve configured the listen directive to serve requests, configure the listen.allowed_clients directives to include the allowed IP addresses. For example:

listen.allowed_clients = 192.0.2.1, 192.0.2.2, 192.0.2.3

Securing The Unix Domain Socket…

For domain sockets, you may still need to secure the connection, except from unauthorized users of the system. This is because the socket appears as a file on the filesystem. As such, to secure it you need to make sure the file ownership and permissions are such that it’s only accessible by the users that legitimately need it. For example:

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

The above allows the web server on a Debian to read and write to the socket but no one else.

Process Manager

This is probably the most interesting part of PHP-FPM configuration as it’s more centered on what the “FPM” part of it actually does in the first place.

Controlling Spawning Limits…

As mentioned earlier, PHP-FPM differs from CGI and server module methods of deployment in that it privately maintains a set of worker processes that remain resident in memory and get scaled up and down according to the actual PHP load for an application.

This process is obviously dynamic but it’s not beyond a certain measure of control especially as far as limits are concerned. Some of the important directives for this:

  • pm: this sets the means by which worker processes are managed. Valid values are:
    • static: The number of worker processes is constant irrespective of demand.
    • ondemand: No worker processes initially, scales up to meet demand. Ideal for near-zero use sites such as personal development machines where you may have many sites going at once and only need PHP to function when requested
    • dynamic: A minimum amount of workers is maintained at all times but the value can be scaled up to a predefined limit (pm.max_children) meet workload requirements.
  • pm.max_children: The maximum number of worker processes PHP will spawn for this pool. In the context of static management this is the constant rate always maintained, in dynamic and ondemand modes this is the upper limit the manager will scale up to. There is no default for this so it’s mandatory for all pool types.
  • pm.start_servers: The number of servers dynamic pools will start with. Not applicable for static or ondemand pools.
  • pm.min_spare_servers and pm.max_spare_servers: Sets the number of long-time idle worker processes dynamic pools will keep around just in case workload increases.
  • pm.process_idle_timeout: The amount of time that ondemand will keep a worker process around without it doing anything. The default of 10s is usually sufficient but if you notice that your local development websites take a bit to spin back up, you may consider increasing this value to 30s or something close.
  • request_terminate_timeout: The amount of time the process manager will wait until it reaps an unresponsive worker. The default is to wait forever and let max_execution_time in the main php.ini configuration kill the script but there may be cases where the worker itself gets stuck. For instance a bad extension or a bug in PHP itself.

Controlling Worker Process Priorities…

In situations where you have a single PHP-FPM instance serving multiple websites (such as with colocation) or PHP-FPM running on your laptop, you may want to control which website’s traffic takes precedence over other system processes. To do this you can manipulate the “nice” value each pool’s workers will be spawned with by setting the process.priority directive. For example:

process.priority = 15

The above will cause workers in the given pool to be very deferential to other processes on the system.

Pool-specific php.ini Settings…

The php_flag and php_value directives are used to boolean and arbitrary values respectively. The syntax for each is to use them similar to associative arrays in PHP:

php_flag[display_errors] = off
php_flag[log_errors] = on
php_value[memory_limit] = 32M 
php_value[output_buffering] = 4096

The above directives only set defaults for the given values since they can be overridden by scripts during execution. If you need to make them into mandatory hard limits, then you need to use their php_admin_* analogs:

php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 32M 
php_admin_value[output_buffering] = 4096

For containers the former method (of setting defaults) probably wouldn’t have much benefit over editing the php.ini directives themselves since there’s likely only one pool. The latter method actually provides the only native mechanism I’m aware of for making particular php.ini values mandatory.

Logging

For our purposes here we’re going to concentrate on the two logs that will likely help you diagnose errors with your application: the PHP-FPM access log and the slowlog.

We’re not going to go over the error log since that gets sent back to your web server over your chosen means of connectivity and saved according to whatever rules you have configured there.

PHP Access Log…

For performance monitoring, we’ve already mentioned the pm.status_page directive. There are two problems with that approach though:

  • It creates a web accessible page whos access you need to restrict in case sensitive information about your application ends up there somehow (via query string or something similar).
  • It only tells you what’s currently happening with no notion of history

Luckily PHP-FPM provides us with a more comprehensive solution in the form of the PHP-FPM access log. This access log functions similarly to the access logs with nginx or Apache in that it records each request that makes it back to the PHP-FPM worker threads.

In its most basic form you can enable the default access log by just settings the access.log in the pool to an valid directory. My recommended settings though would be:

access.log = /var/log/php-access.$pool.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%

The above instructs PHP-FPM to save the regular NCSA access log recrod as well as tack on some useful additional fields:

  • %q adds the query string to the HTTP request field
  • %f prints the actual script being executed. Useful if your webserver handles some of your routing and you might need to determine which script ended up getting the request
  • %{milli}d length of time it took to serve the request
  • %{kilo}M peak memory usage for the PHP process executing the script
  • %C CPU time used in executing the script.

The above is just the alternative format listed (albeit comment out) in the default pool configuration (www.conf) for both distros.

PHP Slow Log…

Ok so the access log has pointed out one of our scripts is taking an inordinate amount of time. How do we figure out which function call seems to be slowing down execution without resorting to full-blown profiling of the application. One solution is to configure PHP-FPM’s slowlog feature so that when a pre-defined time period expires a function trace gets written out for developers.

To configure the slowlog you only need to configure two options for the pool:

slowlog = /var/log/php-slowlog.$pool.log
request_slowlog_timeout = 20s

Adjusting the values as appropriate and restarting. If php-fpm is running inside a Linux container you’ll need to make sure processes inside the container have CAP_PTRACE. For Docker containers that means adding --cap-add=SYS_PTRACE to your docker run command.

One thing to bear in mind: since slowlog can be written to continuously with the traces being particularly long you probably shouldn’t have this enabled on a production server unless you’re running into intermittent issues that are difficult to reproduce. Additionally, if your use containers to service production content you’ll probably not want to give them privileges if you can avoid it.

Given the above you should only temporarily enable slowlog in production while you either try to reproduce the issue or get it to resurface.

Miscellaneous Controls

PHP Status Page…

Occasionally, you’ll run across a need to find out specifically what a PHP-FPM worker process is actually doing. For instance, let’s say you frequently get high CPU utilization alerts from your monitoring and can’t figure out what’s causing the spikes in CPU because all top shows you is that it’s a PHP worker process. To tease this information out of FPM, you can configure it to generate a web-accessible status page that displays the details of currently executing workers.

Please note that many web server configurations will test for file presence before handing of to PHP. If that’s the case then you’ll need to find a way around that, most likely by explicitly listing the status path as part of the “does this exist” check.

The default output is just for pool-wide information:

pool:                 www
process manager:      dynamic
start time:           04/Jan/2018:18:21:37 +0000
start since:          11
accepted conn:        4
listen queue:         0
max listen queue:     0
listen queue len:     0
idle processes:       1
active processes:     1
total processes:      2
max active processes: 1
max children reached: 0
slow requests:        0

If you want to see the worker processes you need to request /status?full though. An example worker description would be:

pid:                  13258
state:                Idle
start time:           04/Jan/2018:18:21:37 +0000
start since:          11
requests:             2
request duration:     319420
request method:       GET
request URI:          /index.php
content length:       0
user:                 -
script:               /var/www/html/index.php
last request cpu:     0.00
last request memory:  2097152

You’ll notice that it gives you both the pid of the worker as well as the request URI that came from the browser. Armed with this knowledge you can now try to isolate the problem and deal with it appropriately.

Manually Setting Environmental Variables…

If you use a server module to execute your PHP, then modifications to the environment to nginx or apache is usually sufficient to get it to pick up some hard coded value. With PHP-FPM now that it’s running outside of a service, you now need to set the environmental variables on the execution pools that need them.

The syntax is simple, you set them using env[] with the variable name inside the square brackets. For instance:

env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp

Further Reading

Once you get comfortable with the above, I would invite you to check the php-fpm.conf and www.conf files that come with the vanilla installs on both Ubuntu and CentOS. These files are replete with commented out values and commentary on possible values. In the above I’ve only included what I’ve thought of as the most important PHP-FPM tunables. They’re far from the only use and many of the tunables I’ve presented here are themselves capable