The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on OVH for Ruby
OVH Infrastructure Deep Dive: Nginx, Gunicorn/FPM, and MySQL Tuning for Ruby Applications
This playbook focuses on optimizing a typical Ruby web application stack deployed on OVH infrastructure. We’ll cover granular tuning of Nginx, the chosen application server (Gunicorn for Python-based apps or PHP-FPM for PHP), and MySQL, emphasizing production-ready configurations and diagnostic techniques.
Nginx Configuration for High Throughput and Low Latency
Nginx acts as the front-line web server and reverse proxy. Optimizing its worker processes, connection handling, and caching is crucial. We’ll assume a typical OVH VPS or dedicated server with multiple CPU cores.
Worker Processes and Connections
The `worker_processes` directive should generally be set to the number of CPU cores available. `worker_connections` dictates the maximum number of simultaneous connections a worker process can handle. A common starting point is 1024, but this can be increased based on observed load and system limits.
Tuning `ulimit`
Before adjusting Nginx, ensure the operating system’s file descriptor limits are sufficient. Edit `/etc/security/limits.conf` or create a file in `/etc/security/limits.d/`.
Example `limits.conf` Snippet
# /etc/security/limits.d/99-nginx.conf * soft nofile 65536 * hard nofile 65536 root soft nofile 65536 root hard nofile 65536 nginx soft nofile 65536 nginx hard nofile 65536
After modifying limits, you’ll need to restart the Nginx service or, for systemd, reload the daemon and restart Nginx.
Nginx `nginx.conf` Snippet
# /etc/nginx/nginx.conf
user www-data; # Or the user Nginx runs as
worker_processes auto; # Or set to the number of CPU cores, e.g., 4
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Increased from default 1024
multi_accept on;
use epoll; # Linux specific, highly recommended
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hide Nginx version for security
# Gzip compression for static assets and API responses
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Buffers and timeouts
client_body_buffer_size 10K;
client_header_buffer_size 1K;
large_client_header_buffers 2 4K;
client_max_body_size 100M; # Adjust as needed for uploads
send_timeout 300s;
read_timeout 300s;
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffer_size 16k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
# Include virtual host configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Optimizing for Ruby Applications (via Gunicorn/Puma or PHP-FPM)
When proxying to application servers like Gunicorn (for Python/Ruby frameworks like Flask/Rails if using WSGI) or Puma (common for Ruby), tune the upstream connection settings. For PHP applications using PHP-FPM, Nginx configuration is more about passing requests efficiently.
Nginx Proxy Configuration Snippet
# Inside your server block for the Ruby app
location / {
proxy_pass http://your_app_backend; # e.g., http://127.0.0.1:8000 for Gunicorn
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;
# For long-running requests or WebSocket support
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
# For WebSocket support (if applicable)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# For static files served directly by Nginx
location ~ ^/(assets|images|javascripts|css|fonts)/ {
root /path/to/your/public/directory; # e.g., /var/www/your_app/public
expires 30d;
add_header Cache-Control "public";
access_log off;
}
Caching Strategies
Leverage Nginx’s caching capabilities for static assets and even dynamic content if appropriate. For dynamic content, consider using `proxy_cache` with appropriate keys and expiration times.
Example `proxy_cache` Configuration
# In http block of nginx.conf
proxy_cache_path /var/cache/nginx/my_app levels=1:2 keys_zone=my_app_cache:10m inactive=60m max_size=1g;
# Inside your server block
location / {
proxy_pass http://your_app_backend;
# ... other proxy settings ...
proxy_cache my_app_cache;
proxy_cache_valid 200 302 10m; # Cache 200 and 302 responses for 10 minutes
proxy_cache_valid 404 1m; # Cache 404 responses for 1 minute
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status; # Useful for debugging
}
Application Server Tuning: Gunicorn (for Python/Ruby) or PHP-FPM
The application server is where your Ruby code executes. Tuning its worker processes, threads, and memory usage is critical for handling concurrent requests efficiently.
Gunicorn Tuning (Common for WSGI/ASGI Apps)
Gunicorn is often used for Python applications but can also serve Ruby applications via WSGI interfaces. The key is balancing worker processes and threads.
Gunicorn Command Line/Configuration
# Example command line for Gunicorn # Assuming a Ruby app served via a WSGI interface (e.g., Rack) # For a typical Rails app, you'd use Puma or Unicorn directly, not Gunicorn. # This example is more illustrative for Python/WSGI apps. # For a Python app (e.g., Flask, Django) gunicorn --workers 3 --threads 2 --bind 127.0.0.1:8000 myapp.wsgi:application # --workers: Number of worker processes. A common starting point is (2 * num_cores) + 1. # --threads: Number of threads per worker. Useful for I/O bound tasks. # --bind: Address and port to bind to. # --timeout: Request timeout in seconds. # --worker-connections: For event-driven workers (like gevent/asyncio). # Using a Gunicorn configuration file (e.g., gunicorn_config.py) # workers = 3 # threads = 2 # bind = "127.0.0.1:8000" # timeout = 120 # worker_class = "sync" # or "gevent", "eventlet", "asyncio"
Note: For Ruby on Rails, Puma or Unicorn are the standard application servers. The tuning principles (workers, threads, memory) remain similar, but the specific directives will differ.
PHP-FPM Tuning (for PHP Applications)
PHP-FPM (FastCGI Process Manager) is the de facto standard for serving PHP. Tuning its process management is crucial.
PHP-FPM Configuration (`php-fpm.conf` or pool configuration files)
; /etc/php/X.Y/fpm/pool.d/www.conf (Replace X.Y with your PHP version) ; Process Manager Settings ; pm = dynamic | static | ondemand pm = dynamic ; If pm = dynamic: pm.max_children = 50 ; Max number of children at any one time. pm.start_servers = 5 ; Number of servers started when FPM starts. pm.min_spare_servers = 2 ; Number of servers to maintain in low-speed environment. pm.max_spare_servers = 10 ; Number of servers to maintain in high-speed environment. pm.max_requests = 500 ; Max number of requests each child process should serve. ; If pm = static: ; pm.static.max_children = 50 ; Fixed number of children. ; If pm = ondemand: ; pm.max_children = 50 ; pm.start_time = '00:00:00' ; Time to start workers ; pm.min_spare_starts = 1 ; pm.max_spare_starts = 3 ; Other important settings request_terminate_timeout = 120s ; Timeout for script execution ; listen.owner = www-data ; listen.group = www-data ; listen.mode = 0660 ; listen = /run/php/phpX.Y-fpm.sock ; For Unix socket (recommended for performance) ; listen = 127.0.0.1:9000 ; For TCP socket
Tuning `pm.max_children`: This is the most critical setting. It should be calculated based on available RAM. A rough estimate: `max_children = (Total RAM – RAM for OS/Nginx/MySQL) / Average RAM per PHP process`. Monitor memory usage and adjust.
MySQL Performance Tuning on OVH
Database performance is often the bottleneck. Tuning MySQL’s configuration file (`my.cnf` or `mysqld.cnf`) is essential. OVH often provides managed MySQL instances; if so, some parameters might be managed by OVH, but understanding them is still vital.
Key `my.cnf` Parameters
# /etc/mysql/my.cnf or /etc/mysql/mysql.conf.d/mysqld.cnf [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 # Performance Tuning # InnoDB is the default and recommended engine default_storage_engine = InnoDB # Buffer Pool Size: Crucial for InnoDB performance. Aim for 50-75% of available RAM on a dedicated DB server. innodb_buffer_pool_size = 4G # Example: For a server with 8GB RAM # Log File Size: Affects recovery time and performance. innodb_log_file_size = 512M # Adjust based on write load. Larger can improve write performance but increase recovery time. innodb_log_buffer_size = 16M # I/O and Threading innodb_flush_log_at_trx_commit = 1 # ACID compliant, but can be slower. 2 offers better performance with minimal risk. 0 is fastest but risky. innodb_flush_method = O_DIRECT # Recommended for Linux with hardware RAID or SSDs. innodb_io_capacity = 2000 # For SSDs, can be higher. Adjust based on disk speed. innodb_thread_concurrency = 0 # 0 means unlimited, let MySQL decide. Can be set to number of cores. # Query Cache (Deprecated in MySQL 5.7, removed in 8.0. Use application-level caching instead.) # query_cache_type = 0 # query_cache_size = 0 # Connection Handling max_connections = 200 # Adjust based on application needs and server resources. max_user_connections = 150 # Limit per user if needed. wait_timeout = 600 # Close idle connections after 10 minutes. interactive_timeout = 600 # Temporary Tables tmp_table_size = 64M max_heap_table_size = 64M # Sort Buffers sort_buffer_size = 2M read_rnd_buffer_size = 2M read_buffer_size = 1M join_buffer_size = 2M # Table Cache table_open_cache = 2000 table_definition_cache = 1000 # Other thread_cache_size = 16 key_buffer_size = 16M # Primarily for MyISAM, less critical if using InnoDB exclusively.
Important Considerations for OVH:
- RAM Allocation: The `innodb_buffer_pool_size` is paramount. Ensure you leave enough RAM for the OS, Nginx, and application servers. Monitor RAM usage closely using `htop` or `free -m`.
- Disk I/O: OVH offers various storage options (SATA, SSD, NVMe). Tune `innodb_io_capacity` and `innodb_flush_method` accordingly. SSDs generally benefit from `O_DIRECT` and higher `innodb_io_capacity`.
- Managed Databases: If using OVH’s managed MySQL, consult their documentation. Some parameters may be pre-configured or inaccessible. Focus on application-level query optimization and connection pooling.
- Replication/Clustering: For high availability, ensure replication is configured correctly and monitor replication lag.
Monitoring and Diagnostics
Regular monitoring is key to identifying performance bottlenecks. Use a combination of system tools and database-specific commands.
System Monitoring
- `htop` / `top`: Monitor CPU, memory, and process usage. Identify runaway processes.
- `iotop`: Monitor disk I/O per process.
- `vmstat`: Provides system-wide statistics on processes, memory, paging, block I/O, traps, and CPU activity.
- `netstat -tulnp`: Check listening ports and active connections.
- Nginx `access.log` and `error.log`: Analyze request times, status codes, and errors.
- PHP-FPM logs: Check for errors or slow script execution.
MySQL Monitoring
Connect to your MySQL server and run these commands:
-- Show current status variables SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Slow_queries'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_wait_free'; -- Indicates buffer pool contention SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_waits'; -- Indicates row lock contention -- Show running queries SHOW FULL PROCESSLIST; -- Check for table locks (especially if not using InnoDB or for specific operations) SHOW OPEN TABLES WHERE In_use > 0; -- Analyze slow query log (if enabled in my.cnf) -- The slow query log file is specified by 'slow_query_log_file' in my.cnf -- Use tools like pt-query-digest (from Percona Toolkit) for detailed analysis.
Enabling the Slow Query Log:
# In my.cnf [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 # Optional: Log queries that don't use indexes
Conclusion
Tuning Nginx, your application server (Gunicorn/PHP-FPM), and MySQL is an iterative process. Start with these baseline configurations, monitor your application’s performance under load, and adjust parameters based on observed bottlenecks. Always test changes in a staging environment before deploying to production. For Ruby applications specifically, ensure you’re using the appropriate Ruby-native servers like Puma or Unicorn and tune them according to their documentation, applying similar principles of worker/thread management and memory allocation.