Setting Up PHP-FPM
To scale your PHP application, run it with PHP-FPM (FastCGI Process Manager). It is now the preferred way to run a PHP application. This post aims to clarify the concepts and settings of PHP-FPM in general. Actual installation commands are only a google search away.
Why FastCGI? What is FastCGI?
Originally, web servers provided the Common Gateway Interface (CGI) as a method to run programs to create dymanic content for a web request. Each CGI request caused the web server to fork a new process, initialize the runtime (ususally Perl), load the source code and run the program to produce content. This was great for simple scripts, but was too much overhead for more complicated applications.
The Apache web server introduced the mod_php module which ran PHP inside apache. With mod_php, the overhead of the per-process request was solved, but it could only scale vertically (larger servers), and likely had other runtime and security implications.
FastCGI pushed the application into a separate process outside the web server, even to one or more servers, allowing the application to scale horizontally (more servers). It uses a control process and worker or child processes to perform the reqeuests concurrently.
What is PHP-FPM?
FPM manages a pool of PHP processes which incoming web requests without the startup penalty. There is the manager process which controls the PHP processes, and multiple PHP processes to run the request. Since PHP is single-threaded for web requests, each process can only run a single request at a time. After it finishes, it is available for the next request.
The major difference between PHP-FPM and a Rails or Java application is that PHP-FPM offers “PHP as as service” whereas a Rails application runs the “Application as a service”. Therefore, PHP-FPM could run multiple applications at a time, while a Rails process runs a single application.
PHP-FPM can run multiple pools. Each pool is used to separate traffic between applications, hosting accounts, or request type (User-facing, API, Backend Requests, etc). This allows you to allocate resources appropriately and prevents one source of traffic from impacting another.
PHP-FPM does not accept HTTP requests. The FastCGI Protocol is a binary protocol that requires an adapter from a web server to send the request to the pool. Unfortunately, utilities like curl will not work. There are some FastCGI clients available on Github, but there is no standard command line utility that comes with FPM.
Installation
First, install additional PHP and any additional modules as needed and configure it in the php.ini
file. Some things to consider in your configuration:
- max_execution_time: in seconds
- upload_max_filesize: Maximum file size your server will allowed to be uploaded, in megabytes
- post_max_size: Maximum data size allowed to uploaded, in megabytes
- max_input_vars: The maximum number of variable form elements that can be submitted to a single page.
Next, install PHP-FPM if necessary using your package manager. Sometimes, these are bundled together.
Install PHP Opcache. This component is installed separately from PHP, and it is specific to the installed version of PHP you are running. Look for packages named something like “php-opcache” or “php80-opcache”.
Once installed, each pool needs to be configured (see below) and the PHP-FPM service started. Once running, any web server like NGINX can send requests to the service.
Pool Configuration
The default pool is “www” and if you are using only a simple configuration, go with this one. Otherwise, you may want a different pools for each application, account, or request type. The location of your pool configuration is system-dependent, but usually something like:
/etc/php-fpm.d/www.conf [www pool configuration]
Each pool needs to “listen” to a different Unix file socket or port. The web server will proxy incoming http requests to the port or socket on the application server. Choose where to host your pool in either one of these formats:
listen = 127.0.0.1:9000
listen = /path/to/unix/socket
You also may need a custom Unix user to run the pool as.
user = myapp
group = myapp
Worker Configuration
Now we need to configure how many children or worker processes we will need. Here are the relevant settings:
- pm.max_children = Maximum number of children to run in this pool
- pm.start_servers = At launch, immediate start up this number of children
- pm.min_spare_servers = Keep at least this number idle children ready for requests
- pm.max_spare_servers = Keep at most this number idle children ready for requests
- pm.max_requests = Lifetime of a process. Recycle after this many requests
You need enough workers to process in number of concurrent incoming requests. Configure how many workers to start, and how many idle workers to be ready and the maximum workers your system can handle (load, database resources, etc.) Setting the lifetime of a worker is a balance between any memory bloat or extra database connections to reclaim. Your configuration may look something like this:
pm.max_children = 15
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 8
pm.max_requests = 1000
When you are ready, start the PHP-FPM service on your server, and ensure it will come up after a restart of the server.
Opcache
A Rails or Java “Application Service” load the code or byte-code on startup. Since PHP-FPM functions differently, use the PHP opcache
to cache files and compiled byte-code in shared memory across all the workers. This improves performance and lowers the memory footprint.
In your php.ini file, configure the Opcache settings. They are commented out by default and are well documented with inline comments. Sometimes though, they are found in the etc/php/10-opcache.ini
or similar file.
Add or Uncomment and modify the following settings, modified for your needs:
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.interned_strings_buffer=8
opcache.revalidate_freq=200
- memory_consumption = Shared Memory size (MB)
- max_accelerated_files = Maximum number of files (keys) in the cache
- interned_strings_buffer = size (MB) for de-duplicated string (hashed). Error messages, templates, etc.
- revalidate_freq = number of seconds between file timestamp validations. It will reload the file if it detects a change after this frequency.
Restart PHP-FPM after your change your configutions for PHP or PHP-FPM.
Install your application
Since PHP-FPM is a “PHP runtime as a service” process, it does not know about your application. It needs the PHP source code of your application to be installed on the same server as FPM. To configure NGINX, we need to know the application path to tell FastCGI what application to run.
PHP-FPM with NGINX
NGINX provides a FastCGI client out of the box to easily proxy web requests to the FPM process. NGINX is mostly a file server (serving HTML or other files) and proxy server, sending requests to other processes. It does not usually run processes itself as Apache does.
The /etc/nginx/
directory ships with a fastcgi_params
file that you can use for default FastCGI settings. Include that in your location block. Because each use case is unique, your configuration will vary.
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_send_timeout 360;
proxy_read_timeout 360;
fastcgi_param DOCUMENT_ROOT /apps/myapp/index.php;
fastcgi_param SCRIPT_FILENAME /apps/myapp/index.php;
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
}
Before this, you could configure a “location /api/” block that forwards to the FPM API pool listening on 127.0.0.1:9001.
HAProxy with PHP-FPM
This post on the HAProxy blog shows you how to setup HTProxy to forwaard requests to PHP-FPM.
Database Connection Pooling
PHP-FPM does not use a database connection pool since it runs across processes and even applications.
Instead, it relies on PHP’s persistent connection functions like pg_pconnect
, mysql_pconnect
, and the Redis module’s pconnect
to cache the connected resource between requests on each process. These connection functions are likely what your framework of choice already uses.
- When the worker process starts up, no connections are created.
- The first pconnect command from a request will make the connection.
- After the request ends, the connection is retained, keyed by the connection string.
- Subsequent requests calling pconnect will be returned the prior connection as long as the connection string is identical. If not, a new connection is made.
- When the worker process ends, alls connections close.
You need to ensure your database has enough connections available for your “max_children” settings and the number of connections your application requires per request.
Also, in php.ini
, set the maximum persistent connections. This will be per process, so consider if you will have many different databases being accessed and retained in the persistence cache.
pgsql.max_persistent = 15
Summary
After install, you need to determine how many resources you need to allocate on your server or determine your Virtual Server size. Considerations in configuring your application service discussed earlier include:
- Configure PHP from the php.ini-production
- Configure request memory and execution time
- How many DB connections are persisted in each worker process and total?
- How many pools do you need?
- A pool per hosting account?
- A Pool per application?
- A pool per connection class (User http, API http, Message Queue, Backend)?
- How many workers do you need for each pool?
- Each concurrent connection requires a child worker
- Have just enough standby idle workers for traffic surges
- Don’t allow to many to overwhelm your server or or database
- How many request should a worker process before it is replaced?
- Do you have enough server resources to handle the max configured workers?
- Do you need Opcache?
- What is the number of application files and size of code to cache?
- What should be the size of the cache?
- How frequently should it check for a file change?
- Can your database handle the maximum configured connections?
- Are you using read-replicas to balance DB requests? Does this affect your persistent connection count?
- Configure your web server (NGINX, HAProxy, etc) to handle traffic
- Split out locations or routes across different PHP-FPM application servers and pools as necessary
- Does your setup allow all requests to be handled evenly?