The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on OVH for Python
Nginx Configuration for High-Traffic Python Applications
Optimizing Nginx is paramount for serving Python web applications efficiently, especially when dealing with high concurrency. We’ll focus on key directives that impact performance and stability, assuming a standard setup with Gunicorn or PHP-FPM.
Worker Processes and Connections
The number of worker processes directly influences how many requests Nginx can handle concurrently. A common starting point is to set worker_processes to the number of CPU cores available. worker_connections dictates the maximum number of simultaneous connections a single worker process can handle.
On OVH, where you might have dedicated or VPS instances, identifying the core count is straightforward. For a Linux system, you can use nproc or check /proc/cpuinfo.
Here’s a snippet from nginx.conf:
user www-data;
worker_processes auto; # Or set to the number of CPU cores, e.g., worker_processes 4;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on expected load and system limits
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ... other http configurations
}
multi_accept on; allows workers to accept multiple connections at once, further improving concurrency.
Buffering and Timeouts
Nginx buffering can significantly impact performance. While buffering can help smooth out traffic spikes, overly aggressive buffering can increase latency. For API-heavy applications, you might want to disable or tune down client request buffering.
client_body_buffer_size, client_header_buffer_size, and large_client_header_buffers are key. For typical web requests, default values are often sufficient. However, for large file uploads, you might need to increase client_body_buffer_size.
Timeouts are crucial for preventing hung connections from consuming resources. client_header_timeout, client_body_timeout, and send_timeout should be set to reasonable values, typically between 30-60 seconds, depending on the application’s expected response times.
http {
# ... other http configurations
client_body_buffer_size 128k;
client_header_buffer_size 128k;
large_client_header_buffers 4 128k;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 10s;
# ... other http configurations
}
Gzip Compression
Enabling Gzip compression can drastically reduce the amount of data transferred, leading to faster page loads. It’s a low-CPU cost operation for modern servers.
http {
# ... other http configurations
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Compression level (1-9)
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# ... other http configurations
}
Proxying to Gunicorn/PHP-FPM
When proxying to your Python application server (Gunicorn) or PHP-FPM, ensure the proxy settings are optimized. For Gunicorn, this typically involves HTTP or FastCGI (if using a WSGI server that supports it). For PHP-FPM, it’s FastCGI.
Key directives include proxy_connect_timeout, proxy_send_timeout, and proxy_read_timeout. These should generally be longer than the client timeouts to allow the backend to process requests.
server {
listen 80;
server_name your_domain.com;
location / {
proxy_pass http://unix:/path/to/your/app.sock; # Or http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 75s;
proxy_send_timeout 75s;
proxy_read_timeout 75s;
}
# For PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust PHP version and path
fastcgi_read_timeout 300; # Longer timeout for potentially long PHP scripts
}
}
Gunicorn Tuning for Python Web Applications
Gunicorn (Green Unicorn) is a popular WSGI HTTP Server for Python. Its performance is heavily influenced by the number of worker processes and the type of worker class used.
Worker Processes and Threads
The --workers flag determines the number of worker processes. A common recommendation is (2 * number_of_cores) + 1. This formula aims to keep CPU cores busy while accounting for I/O waits.
Gunicorn also supports threads via the --threads flag. Using threads can be beneficial for I/O-bound applications, as they allow a single worker process to handle multiple requests concurrently without the overhead of creating new processes. However, due to Python’s Global Interpreter Lock (GIL), threads don’t offer true parallelism for CPU-bound tasks.
For CPU-bound applications, relying solely on multiple worker processes is generally more effective. For I/O-bound applications (e.g., those making many external API calls or database queries), a combination of workers and threads can be optimal.
# Example Gunicorn command gunicorn --workers 3 --threads 2 --bind unix:/path/to/your/app.sock myapp.wsgi:application
In this example, we have 3 worker processes, and each worker can handle up to 2 threads. This configuration would be suitable for a server with 1-2 CPU cores, balancing CPU and I/O.
Worker Classes
Gunicorn offers different worker classes:
- sync: The default worker class. It’s simple but can block under heavy load if requests are slow.
- eventlet: Uses eventlet for non-blocking I/O. Good for I/O-bound applications.
- gevent: Uses gevent for non-blocking I/O. Similar to eventlet, often preferred for its performance.
- tornado: Uses the Tornado IOLoop.
- asyncio: Supports Python’s asyncio framework.
For most modern Python applications, especially those with significant I/O, using gevent or eventlet is highly recommended. You’ll need to install them (e.g., pip install gevent).
# Example using gevent worker gunicorn --worker-class gevent --workers 3 --threads 2 --bind unix:/path/to/your/app.sock myapp.wsgi:application
Timeouts and Keepalive
--timeout specifies the number of seconds to wait for a worker to respond. If a worker takes longer, it’s killed and restarted. This prevents hung requests from blocking workers indefinitely. Set this based on your application’s longest expected request processing time.
--keep-alive controls the number of requests a worker can handle before being restarted. Setting this to a reasonable number (e.g., 1000) can reduce overhead by reusing connections.
gunicorn --workers 4 --threads 2 --timeout 120 --keep-alive 1000 --bind unix:/path/to/your/app.sock myapp.wsgi:application
PHP-FPM Tuning for PHP Applications
For PHP applications, PHP-FPM (FastCGI Process Manager) is the standard. Its configuration heavily impacts how PHP scripts are executed and how many requests can be handled.
Process Management Modes
PHP-FPM offers three process management modes, configured in php-fpm.conf or pool configuration files (e.g., www.conf):
- static: Pre-spawns a fixed number of child processes. Good for predictable loads.
- dynamic: Spawns processes as needed, up to a defined maximum. More flexible.
- ondemand: Spawns processes only when a request arrives and kills them after a period of inactivity. Saves resources but can increase latency for the first request.
For high-traffic sites, dynamic or static are generally preferred. dynamic offers a good balance.
; /etc/php/7.4/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /var/run/php/php7.4-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 50 ; Maximum number of children that can be started. pm.min_spare_servers = 5 ; Minimum number of servers to keep idle. pm.max_spare_servers = 10 ; Maximum number of servers to keep idle. pm.process_idle_timeout = 10s; The number of seconds after which an idle process will be killed. pm.max_requests = 500 ; Maximum number of requests each child process should serve.
In this example, using dynamic mode:
pm.max_children: The absolute maximum number of PHP processes that can run simultaneously. This is the most critical setting and should be tuned based on your server’s RAM. Too high, and you’ll OOM kill.pm.min_spare_serversandpm.max_spare_servers: These define the range of idle processes PHP-FPM tries to maintain.pm.process_idle_timeout: How long an idle process waits before being terminated.pm.max_requests: Limits the number of requests a child process can handle before it’s recycled. This helps prevent memory leaks from accumulating over time.
Resource Limits and Timeouts
request_terminate_timeout in the pool configuration is analogous to Gunicorn’s --timeout. It defines how long a single PHP script can run before being terminated. This is crucial for preventing runaway scripts.
max_execution_time in php.ini is also relevant, but request_terminate_timeout in FPM often takes precedence for FastCGI requests.
; /etc/php/7.4/fpm/pool.d/www.conf [www] # ... other settings request_terminate_timeout = 120s ; Timeout for a single script execution
MySQL Tuning for Performance
Database performance is often the bottleneck. Tuning MySQL involves adjusting buffer sizes, query cache settings, and connection handling.
Key Configuration Variables
These variables are set in my.cnf or mysqld.cnf. Always restart the MySQL service after changes.
[mysqld] # General Settings datadir=/var/lib/mysql socket=/var/run/mysqld/mysqld.sock pid-file=/var/run/mysqld/mysqld.pid # Performance Tuning innodb_buffer_pool_size = 512M # Crucial for InnoDB performance. Adjust based on available RAM (e.g., 50-70% of RAM if dedicated DB server) innodb_log_file_size = 256M # Size of InnoDB redo log files. Larger can improve write performance but increases recovery time. innodb_flush_log_at_trx_commit = 1 # 1=ACID compliant (safest, slowest), 0=faster, less safe, 2=balance. For high write loads, consider 2. innodb_flush_method = O_DIRECT # Bypass OS cache for InnoDB data files. Can improve performance on systems with sufficient RAM. # Connection Handling max_connections = 200 # Maximum number of simultaneous client connections. Adjust based on application needs and server resources. thread_cache_size = 16 # Number of threads the server should cache. table_open_cache = 2000 # Number of tables the server keeps open. table_definition_cache = 1000 # Number of table definitions the server keeps in cache. # Query Cache (Deprecated in MySQL 5.7, removed in 8.0. Consider application-level caching instead) # query_cache_type = 1 # query_cache_size = 64M
Important Notes:
innodb_buffer_pool_sizeis the single most important setting for InnoDB. Ensure it’s not so large that it causes swapping.innodb_flush_log_at_trx_commit: Setting to2can significantly boost write performance at the cost of potentially losing the last second of transactions during an OS crash (not a MySQL crash).- Query Cache: If you are on MySQL 8.0+, the query cache is removed. For older versions, it can sometimes cause contention. Test thoroughly before enabling.
Slow Query Log Analysis
Identifying slow queries is critical. Enable the slow query log to capture queries that exceed a defined execution time.
[mysqld] # ... other settings 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 mysqldumpslow or pt-query-digest (from Percona Toolkit) to pinpoint problematic queries. Optimize these queries with appropriate indexing.
# Example using mysqldumpslow mysqldumpslow /var/log/mysql/mysql-slow.log # Example using pt-query-digest pt-query-digest /var/log/mysql/mysql-slow.log > /tmp/slow_query_report.txt
Monitoring and Iteration
Tuning is an iterative process. Continuous monitoring is key to understanding the impact of your changes and identifying new bottlenecks.
Key Metrics to Monitor
- Nginx: Active connections, requests per second, error rates (4xx, 5xx), worker connections, buffer usage. Tools like
nginx-module-vtsare invaluable. - Gunicorn/PHP-FPM: Worker utilization, request latency, error rates, memory usage per worker.
- MySQL: Connections, query throughput, slow queries, buffer pool hit rate, disk I/O, CPU usage, memory usage.
- System: CPU load, memory usage, disk I/O, network traffic.
Utilize tools like Prometheus with Grafana, Datadog, New Relic, or even basic system tools like htop, iotop, and netstat to gather this data.
Tuning Workflow
- Baseline: Measure current performance under typical and peak loads.
- Identify Bottleneck: Use monitoring tools to find the slowest component (Nginx, App Server, DB, or System Resource).
- Tune One Component: Make small, incremental changes to the identified component.
- Measure Again: Observe the impact of the change. Did it improve performance? Did it shift the bottleneck elsewhere?
- Repeat: Continue the cycle until performance targets are met or no further significant improvements can be made.
Remember to test changes in a staging environment that closely mirrors your production setup before deploying them live.