The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and PostgreSQL on OVH for Perl
Nginx as a High-Performance Frontend for Perl Applications
When deploying Perl applications, particularly those leveraging frameworks like Mojolicious or Dancer, Nginx serves as an excellent, high-performance frontend. Its strengths lie in efficient static file serving, SSL termination, request buffering, and load balancing. For Perl applications, we’ll typically proxy requests to a backend application server like Gunicorn (for WSGI-compatible frameworks) or directly to a FastCGI process manager (like FCGIwrap or Starman) if the Perl application supports it.
A common setup involves Nginx handling incoming HTTP requests, serving static assets directly, and forwarding dynamic requests to the Perl application server. This offloads the heavy lifting of I/O and connection management from the application itself, allowing it to focus on business logic.
Optimizing Nginx Configuration for OVH Instances
OVH instances, especially bare-metal servers, offer significant raw performance. To leverage this, we need to tune Nginx’s worker processes and connection handling. The key directives are worker_processes, worker_connections, and various buffer settings.
Tuning Worker Processes
The optimal number of worker processes often correlates with the number of CPU cores available. For I/O-bound applications, a higher number might be beneficial, but for CPU-bound Perl applications, matching the core count is a good starting point. We can set this dynamically using auto.
Tuning Worker Connections
worker_connections defines the maximum number of simultaneous connections that each worker process can handle. This, combined with worker_processes, determines the total maximum connections Nginx can manage. The system’s file descriptor limit (ulimit -n) is the ultimate ceiling.
Buffer Settings
Buffer sizes are crucial for efficient request handling, especially when proxying. client_body_buffer_size, client_header_buffer_size, and large_client_header_buffers impact how Nginx buffers incoming requests. For proxying, proxy_buffer_size and proxy_buffers are critical.
Example Nginx Configuration Snippet
This configuration snippet is designed for a typical OVH instance with multiple CPU cores. Adjust worker_processes if you have specific insights into your application’s CPU vs. I/O bound nature.
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto; # Set to the number of CPU cores, or 'auto'
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Max connections per worker. Adjust based on ulimit -n
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;
# Buffering for proxying
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# SSL Settings (if applicable)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
# Gzip compression
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;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Perl Application Proxy Configuration
This configuration assumes your Perl application is running via Gunicorn (for WSGI) or a similar process manager listening on a local socket or port. Adjust the proxy_pass directive accordingly.
# /etc/nginx/sites-available/your_perl_app
server {
listen 80;
server_name your_domain.com www.your_domain.com;
# Serve static files directly
location /static/ {
alias /path/to/your/app/static/;
expires 30d;
add_header Cache-Control "public";
}
# Proxy dynamic requests to the application server
location / {
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;
# If using Gunicorn with a Unix socket:
# proxy_pass http://unix:/path/to/your/app.sock;
# If using Gunicorn with a TCP socket:
proxy_pass http://127.0.0.1:8000;
proxy_read_timeout 300s; # Increase timeout for long-running requests
proxy_connect_timeout 75s;
}
# Optional: Handle specific error pages
error_page 500 502 503 504 /500.html;
location = /500.html {
root /usr/share/nginx/html; # Or your custom error page location
internal;
}
}
Gunicorn/FPM Tuning for Perl Applications
When deploying Perl applications, especially those built with WSGI-compatible frameworks, Gunicorn is a popular choice. If your Perl application exposes a FastCGI interface, you’ll use a FastCGI process manager. The tuning principles are similar: manage worker processes and their concurrency.
Gunicorn Worker Tuning
Gunicorn’s worker class and count are critical. For CPU-bound Perl applications, the sync worker class is often sufficient and simpler. For I/O-bound tasks, gevent or event (if supported and beneficial for your specific Perl libraries) can offer better concurrency. The number of workers should generally be (2 * number_of_cores) + 1 as a starting point, but this needs empirical testing.
Example Gunicorn Command Line
This command assumes your Perl WSGI application is in a file named app.py (or equivalent for Perl WSGI wrappers) and the WSGI application object is named application.
# Example for a hypothetical Perl WSGI app (e.g., using Dancer2 with PSGI/WSGI adapter)
# Assuming your PSGI app is in 'app.psgi' and the entry point is 'handler'
# You might need a wrapper script if your framework doesn't directly expose a WSGI app.
# If using a Unix socket (recommended for single-instance deployments):
gunicorn --workers 3 --worker-class sync \
--bind unix:/path/to/your/app.sock \
--timeout 120 \
--log-level info \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log \
app:application # Replace 'app:application' with your actual WSGI entry point
# If using a TCP socket:
gunicorn --workers 3 --worker-class sync \
--bind 127.0.0.1:8000 \
--timeout 120 \
--log-level info \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log \
app:application # Replace 'app:application' with your actual WSGI entry point
Note: For Perl, you’d typically use a PSGI application and a WSGI adapter. The app:application part would reference your PSGI entry point. For example, if your main PSGI file is app.psgi and it exports a handler, it might look like app.psgi:handler.
FastCGI Process Manager (e.g., FCGIwrap) Tuning
If your Perl application uses the FastCGI protocol (common with PSGI applications via modules like FCGI::ProcManager or Starman), you’ll configure the process manager. FCGIwrap is a common choice for integrating with Nginx.
# Example systemd service for FCGIwrap # /etc/systemd/system/fcgiwrap.service [Unit] Description=FastCGI wrapper After=network.target [Service] User=www-data Group=www-data ExecStart=/usr/sbin/fcgiwrap -f -s unix:/var/run/fcgiwrap.socket -c 10 StandardInput=socket Socket=fcgiwrap.socket [Install] WantedBy=multi-user.target
In this FCGIwrap example, -c 10 sets the maximum number of child processes. This should be tuned based on your application’s resource usage and expected load. Nginx would then be configured to proxy to unix:/var/run/fcgiwrap.socket.
PostgreSQL Performance Tuning on OVH
PostgreSQL is a robust relational database. Optimizing it on OVH instances involves tuning memory parameters, connection pooling, and query performance.
Key PostgreSQL Configuration Parameters
The primary configuration file is postgresql.conf. The most impactful parameters relate to memory allocation and I/O behavior.
shared_buffers: The amount of memory dedicated to PostgreSQL’s shared memory buffer cache. A common starting point is 25% of system RAM, but this can be increased up to 40% on dedicated database servers with ample RAM.work_mem: The amount of memory used for internal sort operations and hash tables before spilling to disk. Crucial for complex queries. Set per-operation, so be cautious not to over-allocate.maintenance_work_mem: Memory used for maintenance operations likeVACUUM,CREATE INDEX, andALTER TABLE. Higher values speed up these operations.effective_cache_size: An estimate of how much memory is available for disk caching by the operating system and PostgreSQL’s shared buffers. Helps the query planner make better decisions.max_connections: Maximum number of concurrent connections. Ensure this is high enough for your application but not so high it exhausts system resources.wal_buffers: Memory for WAL (Write-Ahead Logging) data.checkpoint_completion_target: Spreads checkpoint I/O over time, reducing I/O spikes.
Example postgresql.conf Snippet
This example assumes a server with 64GB of RAM. Adjust values based on your specific instance size and workload.
# /etc/postgresql/X.Y/main/postgresql.conf (Replace X.Y with your PostgreSQL version) # Memory shared_buffers = 16GB # ~25% of 64GB RAM work_mem = 64MB # Adjust based on query complexity maintenance_work_mem = 2GB # For VACUUM, CREATE INDEX etc. effective_cache_size = 48GB # ~75% of RAM (shared_buffers + OS cache) # Connections max_connections = 200 # Adjust based on application needs and system limits # listen_addresses = '*' # Or specific IPs if needed # WAL & Checkpoints wal_buffers = 16MB checkpoint_completion_target = 0.9 # Consider tuning wal_writer_delay and checkpoint_timeout for high-write loads # Logging log_destination = 'stderr' logging_collector = on log_directory = 'pg_log' log_filename = 'postgresql-%Y-%m-%d_%H-%M-%S.log' log_statement = 'ddl' # Log DDL statements, or 'all' for debugging log_min_duration_statement = 250ms # Log slow queries # Other potentially useful settings random_page_cost = 1.1 # Lower if using SSDs seq_page_cost = 1.0 effective_io_concurrency = 200 # For SSDs, tune based on IOPS max_worker_processes = 8 # Match CPU cores for parallel query execution max_parallel_workers_per_gather = 4 # Tune for parallel queries
Connection Pooling
Opening and closing database connections is expensive. For applications with frequent, short-lived connections, a connection pooler like PgBouncer is essential. It sits between your application and PostgreSQL, managing a pool of persistent connections.
PgBouncer Configuration Example
# /etc/pgbouncer/pgbouncer.ini [databases] mydb = host=127.0.0.1 port=5432 dbname=your_database_name user=your_db_user password=your_db_password [pgbouncer] listen_addr = 127.0.0.1 listen_port = 6432 auth_type = md5 auth_file = /etc/pgbouncer/userlist.txt pool_mode = session # 'session' or 'transaction'. 'session' is generally better for Perl apps. max_client_conn = 1000 # Max connections from application to PgBouncer default_pool_size = 20 # Pool size per database. Tune based on max_client_conn and DB load. reserve_pool_size = 5 # Pool size for reserved connections reserve_pool_timeout = 10 # Seconds to wait for a reserved connection logfile = /var/log/pgbouncer/pgbouncer.log pidfile = /var/run/pgbouncer/pgbouncer.pid admin_users = pgbouncer_admin stats_users = pgbouncer_stats
Your Perl application would then connect to 127.0.0.1:6432 instead of directly to PostgreSQL. Ensure the userlist.txt file contains the necessary credentials for PgBouncer to connect to PostgreSQL.
Query Optimization and Indexing
This is application-specific but fundamental. Use EXPLAIN ANALYZE to understand query execution plans. Ensure appropriate indexes exist for frequently queried columns, especially those used in WHERE clauses, JOIN conditions, and ORDER BY clauses. Regularly review slow query logs generated by PostgreSQL.
Monitoring and Iterative Tuning
Performance tuning is not a one-time task. Continuous monitoring is key. Utilize tools like Prometheus with exporters for Nginx, Gunicorn, and PostgreSQL. Monitor CPU usage, memory consumption, disk I/O, network traffic, and application-specific metrics (e.g., request latency, database query times, connection counts).
On OVH, tools like htop, iotop, vmstat, and PostgreSQL’s built-in statistics views (pg_stat_activity, pg_stat_statements) are invaluable for real-time diagnostics. Make incremental changes to configurations, test their impact, and iterate.