The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on Linode for PHP
Nginx Configuration for High-Traffic PHP Applications
Optimizing Nginx is paramount for serving PHP applications efficiently. We’ll focus on key directives that impact connection handling, caching, and static file serving. This configuration assumes a Linode environment with a typical LAMP/LEMP stack.
Worker Processes and Connections
The `worker_processes` directive dictates how many worker processes Nginx will spawn. Setting this to `auto` is generally recommended, allowing Nginx to determine the optimal number based on available CPU cores. The `worker_connections` directive sets the maximum number of simultaneous connections that each worker process can handle. A common starting point is 1024, but this can be increased based on your server’s RAM and expected load.
Example Nginx Configuration Snippet
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Increased from 1024 for higher concurrency
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hides Nginx version for security
# ... other http configurations ...
}
Gzip Compression
Enabling Gzip compression significantly reduces the bandwidth required to transfer HTML, CSS, and JavaScript files, leading to faster page load times. Configure it to compress responses for text-based content.
Example Gzip Configuration
# /etc/nginx/nginx.conf or included conf file gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; # Compression level (1-9, 6 is a good balance) gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_disable "msie6"; # Disable for older IE versions
Caching Strategies
Leveraging browser caching and Nginx’s proxy caching can offload significant load from your application servers. For static assets, set appropriate `Cache-Control` headers. For dynamic content, consider implementing Nginx’s FastCGI cache if your PHP application is suitable.
Browser Caching for Static Assets
# In your server block or a separate conf file for static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
Nginx FastCGI Cache (for PHP-FPM)
This requires enabling the FastCGI cache module. Define a cache zone and then apply it to your PHP location. Be mindful of cache invalidation strategies.
# In http block
fastcgi_cache_path /var/cache/nginx/php_cache levels=1:2 keys_zone=php_cache:100m inactive=60m;
fastcgi_temp_path /var/tmp/nginx/fastcgi_temp; # Ensure this directory exists and is writable by nginx user
# In your server block, within the PHP location
location ~ \.php$ {
# ... other fastcgi_pass and fastcgi_param directives ...
fastcgi_cache php_cache;
fastcgi_cache_valid 200 302 10m; # Cache successful responses for 10 minutes
fastcgi_cache_valid 404 1m; # Cache 404s for 1 minute
fastcgi_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status; # Useful for debugging
# Optional: Bypass cache for logged-in users or specific requests
# fastcgi_cache_bypass $http_cookie;
# fastcgi_no_cache $http_cookie;
}
Gunicorn/PHP-FPM Tuning for Performance
The application server (Gunicorn for Python, PHP-FPM for PHP) is the next critical layer. Tuning its worker processes and memory usage directly impacts how many requests your application can handle concurrently and its responsiveness.
PHP-FPM Configuration
PHP-FPM (FastCGI Process Manager) is the standard for running PHP applications with Nginx. The primary configuration file is typically located at /etc/php/[version]/fpm/php.ini and /etc/php/[version]/fpm/pool.d/www.conf.
`php.ini` Directives
; /etc/php/[version]/fpm/php.ini memory_limit = 256M ; Increase if your app consumes more memory upload_max_filesize = 64M ; Adjust based on expected file uploads post_max_size = 64M ; Should be >= upload_max_filesize max_execution_time = 60 ; Max time in seconds for script execution
`www.conf` (Process Management)
The pm (process manager) setting is crucial. dynamic is often a good balance, but ondemand can save resources if traffic is sporadic. static provides the most consistent performance but consumes more memory.
; /etc/php/[version]/fpm/pool.d/www.conf ; Choose one of: static, dynamic, ondemand pm = dynamic ; If pm = dynamic pm.max_children = 100 ; Max number of children at any one time pm.start_servers = 5 ; Number of children when PM starts pm.min_spare_servers = 2 ; Min number of idle children pm.max_spare_servers = 10 ; Max number of idle children pm.process_idle_timeout = 10s ; How long idle processes are kept alive ; If pm = ondemand pm.max_children = 100 pm.process_idle_timeout = 10s ; Timeout for idle processes to be killed ; If pm = static pm.max_children = 100 ; Fixed number of children
Tuning Tip: Start with pm.max_children set to a value that your server’s RAM can comfortably support when all processes are active. A common formula is (Total RAM - RAM for OS/Nginx/MySQL) / Average PHP Process Memory Usage. Monitor your server’s memory usage under load and adjust accordingly. Restart PHP-FPM after making changes: sudo systemctl restart php[version]-fpm.
Gunicorn Configuration (for Python WSGI Apps)
Gunicorn is a popular WSGI HTTP Server for Python. Its configuration heavily influences concurrency and resource utilization. We’ll focus on worker types and counts.
Worker Types
sync: The default, synchronous worker. Simple but can block under heavy load.eventlet,gevent: Asynchronous workers that use green threads. Excellent for I/O-bound applications.tornado: Uses Tornado’s asynchronous I/O loop.
Worker Count
A common recommendation for Gunicorn workers is (2 * number_of_cores) + 1. However, this is a starting point. For I/O-bound applications using async workers, you might need significantly more workers.
Example Gunicorn Command Line/Configuration
# Example using command line arguments gunicorn --workers 4 --worker-class gevent --bind 0.0.0.0:8000 myapp.wsgi:application # Example using a Gunicorn configuration file (e.g., gunicorn_config.py) # /etc/gunicorn.d/myapp.py import multiprocessing bind = "0.0.0.0:8000" workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "gevent" # Or "sync", "eventlet" threads = 2 # If using sync worker class, threads can help with I/O # timeout = 30 # Adjust if your requests take longer # keepalive = 2 # Keep-alive connections # accesslog = "/var/log/gunicorn/access.log" # errorlog = "/var/log/gunicorn/error.log"
Tuning Tip: For Python applications, especially those with significant I/O (database queries, external API calls), using gevent or eventlet with a higher worker count than the CPU-bound formula suggests can yield better throughput. Monitor CPU and memory usage. If CPU is maxed out, reduce workers. If memory is the bottleneck, reduce workers or optimize application memory usage.
MySQL Performance Tuning on Linode
Database performance is often the bottleneck. Tuning MySQL involves adjusting buffer sizes, query cache settings, and connection handling. We’ll focus on key parameters in my.cnf.
Key `my.cnf` Parameters
Locate your MySQL configuration file, typically /etc/mysql/my.cnf, /etc/mysql/mysql.conf.d/mysqld.cnf, or similar. Always back up your configuration before making changes.
[mysqld] # General Settings user = mysql pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock port = 3306 basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp lc_messages_dir = /usr/share/mysql lc_messages = en skip-external-locking # InnoDB Settings (Crucial for performance) innodb_buffer_pool_size = 1G ; **Most important setting.** Set to 50-75% of available RAM if MySQL is the primary service. innodb_log_file_size = 256M ; Larger files can improve write performance but increase recovery time. innodb_log_buffer_size = 16M ; Buffer for transaction logs. innodb_flush_log_at_trx_commit = 1 ; Default (ACID compliant). Set to 2 for better performance at slight risk. innodb_flush_method = O_DIRECT ; Recommended for Linux to avoid double buffering. innodb_file_per_table = 1 ; Recommended for better space management. # MyISAM Settings (If you still use MyISAM) key_buffer_size = 128M ; For MyISAM index caching. # Connection Settings max_connections = 200 ; Adjust based on application needs and server resources. thread_cache_size = 16 ; Cache threads for reuse. table_open_cache = 2000 ; Cache open table file descriptors. table_definition_cache = 1000 ; Cache table definitions. # Query Cache (Deprecated in MySQL 8.0, removed in 8.0.34. Use with caution or disable.) # query_cache_type = 1 # query_cache_size = 64M # query_cache_limit = 2M # Other important settings sort_buffer_size = 4M read_buffer_size = 2M read_rnd_buffer_size = 4M join_buffer_size = 4M tmp_table_size = 64M max_heap_table_size = 64M
Tuning `innodb_buffer_pool_size`
This is the memory area where InnoDB caches table data and indexes. A larger buffer pool significantly reduces disk I/O. On a dedicated database server, setting this to 70-80% of the total system RAM is common. On a mixed-use server (e.g., web + DB), be more conservative, perhaps 50%.
`innodb_flush_log_at_trx_commit`
Setting this to 1 (default) ensures that each transaction commit flushes the log buffer to disk, providing full ACID compliance. Setting it to 2 means the log buffer is written to the OS buffer on commit, and the OS flushes it to disk approximately once per second. This offers a significant performance boost for writes but carries a small risk of losing the last second of transactions if the server crashes.
Query Optimization and Indexing
While not strictly a configuration parameter, effective query optimization is critical. Use EXPLAIN to analyze slow queries and ensure appropriate indexes are in place. Regularly review slow query logs.
-- Example of enabling and analyzing slow query log SET GLOBAL slow_query_log = 'ON'; SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log'; SET GLOBAL long_query_time = 2; -- Log queries taking longer than 2 seconds SET GLOBAL log_queries_not_using_indexes = 'ON'; -- Optional: Log queries not using indexes -- To analyze a query: EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
Tuning Tip: After modifying my.cnf, always restart the MySQL service: sudo systemctl restart mysql. Monitor server performance using tools like htop, iotop, and MySQL’s own performance schema or status variables (e.g., SHOW GLOBAL STATUS LIKE 'Innodb%';) to validate the impact of your changes.