Optimizing High CPU Usage in PHP-FPM Processes on Nginx: Performance Tuning Strategies


3 views

Here's what's happening in our production environment:

Server specs:
- Linode VPS: 8 cores, 8GB RAM, 2.6GHz
- Nginx + PHP-FPM stack
- Custom PHP framework (unknown, potentially inefficient)
- 6 PHP-FPM processes consuming 70-100% CPU each
- Current pool config:
pm = dynamic
pm.max_children = 10
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6

First, let's implement some quick fixes while we investigate the root cause:

# Updated www.conf with CPU limiting
pm = dynamic
pm.max_children = 8  # Don't max out all cores
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
pm.process_idle_timeout = 10s
pm.max_requests = 500  # Prevent memory leaks

We've also implemented Memcached for session storage:

session.save_handler = memcached
session.save_path = "127.0.0.1:11211"

To identify problematic scripts:

# Install and run PHP-FPM process inspector
sudo apt-get install php-xhprof
sudo service php-fpm restart

# Sample profiling code at application entry point
if (extension_loaded('xhprof')) {
    xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);
    register_shutdown_function(function() {
        $data = xhprof_disable();
        file_put_contents('/tmp/xhprof.log', json_encode($data));
    });
}

Implementing process prioritization with cgroups:

# Create cgroup for PHP-FPM
sudo cgcreate -g cpu:/phpfpm
echo 50000 > /sys/fs/cgroup/cpu/phpfpm/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/phpfpm/cpu.cfs_period_us

# Apply to PHP-FPM
sudo cgclassify -g cpu:phpfpm $(pgrep php-fpm)

For custom frameworks, consider these patterns:

// Replace heavy reflection with direct calls
// Before:
$method = new ReflectionMethod($object, 'methodName');
$method->invoke($object, $args);

// After:
$object->methodName($args);

// Optimize session usage
session_write_close(); // Release lock early
// Continue processing without session blocking

Prevent PHP-FPM from being overloaded:

location ~ \.php$ {
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
    fastcgi_connect_timeout 60;
    fastcgi_send_timeout 300;
    fastcgi_read_timeout 300;
    fastcgi_busy_buffers_size 64k;
    fastcgi_temp_file_write_size 64k;
    fastcgi_max_temp_file_size 0;
    fastcgi_intercept_errors off;
}

Set up real-time monitoring:

# Install and configure php-fpm-exporter
wget https://github.com/hipages/php-fpm_exporter/releases/download/v1.0.0/php-fpm_exporter_1.0.0_linux_amd64
./php-fpm_exporter --phpfpm.scrape-uri tcp://127.0.0.1:9000/status

# Sample Prometheus alert rule
- alert: HighPHPCPU
  expr: rate(phpfpm_process_cpu_seconds_total[1m]) > 0.7
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "High CPU usage in PHP-FPM process"
    description: "PHP-FPM process {{ $labels.pid }} is using {{ $value }} CPU seconds per second"

When dealing with custom PHP frameworks on Nginx/PHP-FPM setups, we often encounter situations where individual PHP-FPM processes consume 70-100% CPU. This creates resource contention and prevents proper scaling, even on relatively powerful VPS instances (8-core, 8GB RAM in this case).

First, let's examine the current pool configuration:

pm = dynamic
pm.max_children = 10
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6

While these settings appear reasonable for the available memory (only 472MB used out of 8GB), the CPU becomes the bottleneck. Here's how to identify the culprits:

Real-time Process Monitoring

# Install required tools
sudo apt-get install htop strace

# Monitor PHP-FPM processes
sudo strace -p $(pgrep -f "php-fpm: pool www") -c

1. Process Isolation

Separate frontend and backend operations into different PHP-FPM pools:

; /etc/php/7.4/fpm/pool.d/frontend.conf
[frontend]
listen = /run/php/php7.4-fpm-frontend.sock
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4

; /etc/php/7.4/fpm/pool.d/backend.conf
[backend]
listen = /run/php/php7.4-fpm-backend.sock
pm = dynamic
pm.max_children = 4
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 2

2. CPU Throttling with cgroups

Instead of cpulimit, use cgroups for more precise control:

# Create cgroup
sudo cgcreate -g cpu:/phpfpm

# Set CPU limit (50% in this example)
echo 50000 > /sys/fs/cgroup/cpu/phpfpm/cpu.cfs_quota_us

# Apply to PHP-FPM
sudo cgclassify -g cpu:phpfpm $(pgrep php-fpm)

3. Session Handling Optimization

Implementing Memcached for sessions was a good move. Here's how to configure it properly:

; php.ini configuration
session.save_handler = memcached
session.save_path = "SERVER_IP:11211"

; Additional protection
memcached.sess_lock_wait = 150000
memcached.sess_prefix = "sess_"

OPcache Configuration

Even with custom frameworks, OPcache can dramatically reduce CPU usage:

opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1

Query Optimization

Implement basic query logging to identify bottlenecks:

# MySQL slow query log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

For CPU-intensive operations, implement a queue system:

// Example using Redis queue
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// Instead of processing immediately
$redis->rPush('process_queue', json_encode($data));

// Worker script (run via supervisor)
while ($job = $redis->blPop('process_queue', 0)) {
    processJob(json_decode($job[1], true));
}

Implement proper monitoring to validate changes:

# Install Prometheus exporter
wget https://github.com/hipages/php-fpm_exporter/releases/download/v2.0.0/php-fpm_exporter_2.0.0_linux_amd64

# Configure to monitor both pools
./php-fpm_exporter --phpfpm.scrape-uri=unix:///run/php/php7.4-fpm-frontend.sock;/status \
                  --phpfpm.scrape-uri=unix:///run/php/php7.4-fpm-backend.sock;/status

The key is gradual implementation of these measures while monitoring performance. Start with pool separation and OPcache, then move to more advanced solutions like queueing if needed.