The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on AWS for C
Nginx as a High-Performance Frontend Proxy
When deploying web applications, especially those built with Python (using Gunicorn) or PHP (using PHP-FPM), Nginx serves as an indispensable frontend proxy. Its event-driven, asynchronous architecture makes it exceptionally efficient at handling a large number of concurrent connections, offloading static file serving, SSL termination, and load balancing. Tuning Nginx is crucial for maximizing throughput and minimizing latency.
Core Nginx Performance Directives
The primary configuration file for Nginx is typically located at /etc/nginx/nginx.conf. Within the main context (outside of http, server, or location blocks), several directives significantly impact performance:
Worker Processes and Connections
The worker_processes directive determines how many worker processes Nginx will spawn. A common recommendation is to set this to the number of CPU cores available on the server. The worker_connections directive sets the maximum number of simultaneous connections that each worker process can handle. The total maximum connections will be worker_processes * worker_connections.
Tuning Example (nginx.conf)
# In the main context (outside http block)
user www-data;
worker_processes auto; # Or set to number of CPU cores, e.g., worker_processes 4;
events {
worker_connections 1024; # Adjust based on expected load and system limits
multi_accept on; # Allows workers to accept multiple connections at once
}
multi_accept on; is particularly useful for high-concurrency scenarios, allowing workers to accept new connections more rapidly.
File Descriptors and Open Files Limit
Each connection in Nginx consumes a file descriptor. To avoid hitting system limits, especially under heavy load, it’s essential to increase the open files limit. This is typically done via ulimit settings, often configured in /etc/security/limits.conf or systemd service files.
System-wide Limits (limits.conf)
# In /etc/security/limits.conf * soft nofile 65536 * hard nofile 65536 root soft nofile 65536 root hard nofile 65536
After modifying limits.conf, you’ll need to either reboot the server or re-login for the changes to take effect for your user. For Nginx specifically, ensure the user Nginx runs as (e.g., www-data) has these limits applied. Often, this is managed by the systemd service file for Nginx.
Systemd Service Limits (e.g., /etc/systemd/system/nginx.service.d/override.conf)
[Service] LimitNOFILE=65536
After creating or modifying this override file, reload systemd and restart Nginx: sudo systemctl daemon-reload && sudo systemctl restart nginx.
Keep-Alive Connections
HTTP keep-alive (persistent connections) significantly reduces the overhead of establishing new TCP connections for each request. Nginx’s keepalive_timeout and keepalive_requests directives control this behavior.
Tuning Example (nginx.conf – http block)
http {
# ... other http directives ...
keepalive_timeout 65; # Default is 75. Adjust based on client behavior and server load.
keepalive_requests 100; # Default is 100. Maximum requests per keep-alive connection.
# ... other http directives ...
}
A longer keepalive_timeout can be beneficial if clients tend to make multiple requests in quick succession, but it also ties up worker connections longer. A value between 30-75 seconds is a common starting point.
Buffering and Timeouts
Nginx uses buffers to handle data transfer between clients and backend servers. Tuning buffer sizes and timeouts can prevent issues with slow clients or slow backends.
Tuning Example (nginx.conf – http or server block)
http {
# ...
client_body_buffer_size 128k; # Default is 16k. Increase for large POST requests.
client_header_buffer_size 1k; # Default is 1k. Usually sufficient.
large_client_header_buffers 2 128k; # Default is 2 4k. For large headers.
client_max_body_size 100M; # Maximum allowed size of the client request body.
send_timeout 60s; # Timeout for sending a response to the client.
client_body_timeout 60s; # Timeout for receiving client request body.
client_header_timeout 60s; # Timeout for receiving client request header.
proxy_connect_timeout 60s; # Timeout for establishing a connection with the upstream server.
proxy_send_timeout 60s; # Timeout for transmitting a request to the upstream server.
proxy_read_timeout 60s; # Timeout for receiving a response from the upstream server.
# ...
}
client_max_body_size is critical for file uploads. proxy_read_timeout is particularly important when dealing with long-running backend processes.
Gunicorn Tuning for Python Applications
Gunicorn (Green Unicorn) is a popular WSGI HTTP Server for Python. It’s a pre-fork worker model, meaning it spawns multiple worker processes. Tuning Gunicorn involves selecting the right worker type and determining the optimal number of workers.
Worker Types
Gunicorn offers several worker types:
- Sync Workers (
sync): The default and most basic worker type. Each worker handles one request at a time. Suitable for I/O-bound applications that don’t use asynchronous libraries. - Asynchronous Workers (
gevent,eventlet): These workers can handle multiple requests concurrently using non-blocking I/O. Ideal for applications that perform a lot of I/O operations (network requests, database queries) and can leverage asynchronous libraries. - Threaded Workers (
gthread): Uses threads within a single process to handle multiple requests. Can be useful for CPU-bound tasks if the Global Interpreter Lock (GIL) is not a bottleneck, but generally less performant than async workers for I/O-bound tasks.
For most modern Python web applications, especially those making external API calls or database queries, gevent or eventlet workers are recommended for better concurrency.
Number of Workers
The general recommendation for the number of worker processes is:
- For
syncworkers:2 * Number of CPU Cores + 1. - For
gevent/eventletworkers: This is more nuanced. A common starting point isNumber of CPU Cores * 2or even higher, as these workers can handle many concurrent connections efficiently. The key is to monitor CPU and memory usage and adjust.
Gunicorn Command Line / Configuration
You can configure Gunicorn via command-line arguments or a Python configuration file.
Example Command Line
# For sync workers gunicorn --workers 3 --worker-class sync --bind 0.0.0.0:8000 myapp.wsgi:application # For gevent workers (requires gevent installed: pip install gevent) gunicorn --workers 4 --worker-class gevent --bind 0.0.0.0:8000 myapp.wsgi:application
Example Configuration File (gunicorn_config.py)
import multiprocessing # Number of worker processes workers = multiprocessing.cpu_count() * 2 + 1 # Or for gevent/eventlet, you might start higher: # workers = multiprocessing.cpu_count() * 2 # Worker class # worker_class = "sync" worker_class = "gevent" # Requires 'pip install gevent' # Bind address and port bind = "0.0.0.0:8000" # Maximum number of requests a worker can handle before restarting max_requests = 1000 # Set to 0 to disable # max_requests = 0 # Timeout for worker requests timeout = 30 # seconds # Threads for gthread worker class (if used) # threads = 2 # Logging configuration loglevel = "info" accesslog = "-" # Log to stdout errorlog = "-" # Log to stderr
When using Gunicorn behind Nginx, ensure Gunicorn is bound to a non-public interface (e.g., 127.0.0.1:8000 or unix:/path/to/socket.sock) and Nginx is configured to proxy requests to it.
PHP-FPM Tuning for PHP Applications
PHP-FPM (FastCGI Process Manager) is the standard way to run PHP applications efficiently. It manages a pool of PHP worker processes. Tuning PHP-FPM involves configuring the process manager and the PHP settings themselves.
Process Manager Settings (php-fpm.conf / pool.d/*.conf)
PHP-FPM configuration files are typically found in /etc/php/[version]/fpm/php-fpm.conf and pool configurations in /etc/php/[version]/fpm/pool.d/www.conf (or similar). The key directives for performance are within the pool configuration.
Process Management Modes
PHP-FPM offers three primary process management modes:
- Static: A fixed number of child processes are created at startup. This offers the most predictable performance but can be less responsive to traffic fluctuations.
- Dynamic: Starts with a minimum number of processes and spawns more up to a maximum as needed. Processes are then killed if they are idle for a certain period.
- On-Demand: Starts only one process and spawns more as requests come in. Processes are killed when idle. This is the most memory-efficient but can have higher latency for initial requests.
For most production environments, Dynamic is a good balance. Static can be better if you have consistent, high traffic and want minimal overhead.
Tuning Example (pool.d/www.conf)
; /etc/php/[version]/fpm/pool.d/www.conf ; Choose one of the process management modes ; pm = static pm = dynamic ; pm = ondemand ; For pm = static: ; pm.max_children = 50 ; Number of child processes to always maintain. ; For pm = dynamic: pm.max_children = 35 ; Maximum number of children that can be started. pm.min_spare_servers = 10 ; Minimum number of servers that should be kept idle. pm.max_spare_servers = 20 ; Maximum number of servers that should be kept idle. pm.max_requests = 500 ; Maximum number of requests each child process should serve before respawning. ; For pm = ondemand: ; pm.max_children = 50 ; pm.max_requests = 500 ; Process idle timeout (for dynamic and ondemand) ; pm.process_idle_timeout = 10s ; Listen socket ; listen = /run/php/php[version]-fpm.sock listen = 127.0.0.1:9000 ; Or a TCP socket if preferred listen.owner = www-data listen.group = www-data listen.mode = 0660 ; Other important settings request_terminate_timeout = 60s ; Timeout for script execution ; request_slowlog_timeout = 10s ; Log scripts taking longer than this ; slowlog = /var/log/php/[version]/php-fpm-slow.log
The optimal values for pm.max_children, pm.min_spare_servers, and pm.max_spare_servers depend heavily on your server’s RAM and the nature of your PHP application. A common starting point for pm.max_children is (Total RAM - RAM used by OS/other services) / Average RAM per PHP process. Monitor memory usage closely.
PHP Configuration (php.ini)
Beyond PHP-FPM settings, core PHP directives in php.ini also impact performance. These are typically found in /etc/php/[version]/fpm/php.ini.
Key php.ini Directives
; In php.ini for FPM ; Memory limit for scripts memory_limit = 256M ; Adjust as needed, avoid excessively high values ; Maximum execution time for scripts max_execution_time = 60 ; Match or be less than FPM's request_terminate_timeout ; Maximum input variables max_input_vars = 3000 ; Useful for complex forms ; OPcache settings (crucial for performance) opcache.enable=1 opcache.memory_consumption=128 ; MB, adjust based on code size opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.revalidate_freq=60 ; Check for file updates every 60 seconds opcache.validate_timestamps=1 ; Set to 0 in production for max performance if deployments are managed carefully opcache.enable_cli=0 ; Usually not needed for FPM
OPcache is non-negotiable for PHP performance. Ensure it’s enabled and properly configured. Setting opcache.validate_timestamps=0 in production can significantly boost performance by eliminating file stat checks on every request, but requires a manual cache clear or application restart after deployments.
MySQL/MariaDB Tuning on AWS
Database performance is often the bottleneck. Tuning MySQL/MariaDB involves configuring its buffer pools, query cache (though often deprecated/removed in newer versions), connection handling, and logging.
Key Configuration Variables (my.cnf / my.ini)
MySQL configuration is typically found in /etc/mysql/my.cnf, /etc/mysql/mysql.conf.d/mysqld.cnf, or similar paths. On AWS, you might be using RDS, which offers parameter groups for tuning.
InnoDB Buffer Pool Size
This is arguably the most critical setting for InnoDB performance. It caches table data and indexes in memory. A common recommendation is to set it to 50-75% of available RAM on a dedicated database server.
[mysqld] innodb_buffer_pool_size = 4G ; Example for a server with 8GB RAM innodb_buffer_pool_instances = 4 ; Typically set to number of CPU cores, or 1 per GB of buffer pool size
On AWS RDS, this is controlled by the innodb_buffer_pool_size parameter.
Connection Handling
Managing database connections efficiently is vital.
[mysqld] max_connections = 200 ; Adjust based on application needs and server capacity thread_cache_size = 16 ; Cache threads for reuse wait_timeout = 600 ; Close idle connections after 10 minutes (default 8 hours) interactive_timeout = 600 ; Close idle interactive connections
wait_timeout is important to prevent too many sleeping connections from consuming resources.
Query Cache (Deprecated/Removed)
The query cache was often a source of contention and is disabled by default in MySQL 5.7 and removed in MySQL 8.0. If you are on an older version and considering it, benchmark carefully. For modern deployments, focus on other optimizations.
Log Files
Slow query logging is essential for identifying performance bottlenecks.
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 ; Log queries taking longer than 2 seconds log_queries_not_using_indexes = 1 ; Log queries that don't use indexes
Regularly analyze the mysql-slow.log file using tools like pt-query-digest to optimize problematic queries.
Temporary Tables and Sort Buffers
[mysqld] tmp_table_size = 64M max_heap_table_size = 64M sort_buffer_size = 2M join_buffer_size = 2M read_buffer_size = 1M read_rnd_buffer_size = 2M
These buffers are allocated per connection/thread, so set them conservatively. Larger values can lead to excessive memory consumption if max_connections is high.
AWS Specific Considerations
When deploying on AWS, several factors come into play:
Instance Sizing
Choose instance types that match your workload. For compute-intensive tasks (like PHP processing), CPU-optimized instances (C-series) might be suitable. For memory-intensive databases, memory-optimized instances (R-series) are preferred. Ensure sufficient network bandwidth.
EBS Volumes
For database instances (RDS or EC2-hosted), use provisioned IOPS (io1/io2) or General Purpose SSD (gp3) volumes. gp3 offers better baseline performance and cost-effectiveness than gp2, allowing independent tuning of IOPS and throughput. Avoid magnetic volumes for production databases.
RDS Parameter Groups
For RDS, create custom parameter groups to modify MySQL/MariaDB settings. Changes made here are applied dynamically or upon instance reboot, depending on the parameter.
Database Read Replicas
Offload read traffic from your primary database instance by setting up read replicas. Configure your application to direct read-heavy queries to these replicas.
Caching Layers
Implement caching at various levels: Nginx (e.g., proxy_cache), application-level caching (e.g., Redis, Memcached), and database query caching (if applicable and carefully managed).
Monitoring and Iteration
Tuning is an iterative process. Continuous monitoring is key:
- Nginx: Use
stub_statusmodule for connection metrics,access.loganalysis, and system metrics (CPU, memory, network). - Gunicorn/PHP-FPM: Monitor worker status, request latency, error rates, and system resource usage. PHP-FPM has its own status page.
- MySQL: Monitor
SHOW GLOBAL STATUS,SHOW ENGINE INNODB STATUS, slow query logs, and system resource usage (CPU, I/O, memory). AWS CloudWatch provides excellent metrics for RDS. - Application Performance Monitoring (APM): Tools like Datadog, New Relic, or Sentry provide deep insights into application-level performance, database query times, and external service calls.
Start with conservative settings, monitor the impact, and gradually adjust parameters based on observed performance and resource utilization. Always test changes in a staging environment before deploying to production.