The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and DynamoDB on OVH for Shopify
Optimizing Nginx for High-Traffic Shopify Stores on OVH
This section details critical Nginx configurations for serving a high-traffic Shopify store hosted on OVH infrastructure. We’ll focus on caching, connection management, and static file serving to minimize latency and server load.
Nginx Caching Strategies
Effective caching is paramount. We’ll implement both browser caching for static assets and server-side caching for dynamic responses where appropriate. For Shopify, direct page caching at the Nginx level is often discouraged due to the dynamic nature of product pages, cart contents, and user-specific data. However, caching static assets like images, CSS, and JavaScript is highly beneficial.
Browser Caching for Static Assets
Configure Nginx to set appropriate `Cache-Control` and `Expires` headers for static files. This instructs the client’s browser to cache these resources, reducing subsequent requests to the server.
Example Nginx Configuration Snippet
# /etc/nginx/conf.d/shopify_performance.conf
# Cache static assets for a long duration
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2|ttf|eot)$ {
expires 365d;
add_header Cache-Control "public, immutable";
access_log off; # Optionally disable access logs for static files
try_files $uri =404;
}
# Cache fonts for a long duration
location ~* \.(woff|woff2|ttf|eot|svg)$ {
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}
Optimizing Connection Handling
Tuning worker processes, connections, and keep-alive settings is crucial for handling concurrent user requests efficiently. OVH instances typically have multiple CPU cores, so we can leverage them.
Worker Processes and Connections
Set worker_processes to the number of CPU cores available. worker_connections defines the maximum number of simultaneous connections a worker process can handle. The total maximum connections will be worker_processes * worker_connections.
Keep-Alive Timeout
A reasonable keepalive_timeout allows clients to reuse existing connections, reducing the overhead of establishing new TCP connections. However, excessively long timeouts can tie up worker connections.
Gzip Compression
Enabling Gzip compression significantly reduces the size of text-based assets (HTML, CSS, JS), leading to faster load times. Ensure it’s configured to compress responses before sending them to the client.
Example Nginx Configuration Snippet (Global)
# /etc/nginx/nginx.conf
user www-data; # Or your Nginx user
worker_processes auto; # Set to number of CPU cores or 'auto'
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # Adjust based on expected load and server memory
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65; # Seconds
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Compression level (1-9)
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Other configurations...
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Tuning Gunicorn/PHP-FPM for Application Performance
The application server (Gunicorn for Python/Django/Flask, or PHP-FPM for PHP) is the next critical layer. Incorrect tuning here can lead to request queuing, high CPU usage, and memory exhaustion.
Gunicorn Configuration (Python Applications)
For Python-based applications (e.g., custom backends, headless Shopify apps), Gunicorn is a common choice. The key parameters are workers and threads.
Worker Processes and Threads
A common starting point for workers is (2 * CPU_CORES) + 1. This formula aims to keep CPU cores busy while accounting for I/O waits. If your application is heavily I/O-bound (e.g., many external API calls), you might benefit from more workers. If it’s CPU-bound, fewer might be better. Threads can be used to handle concurrent requests within a worker process, especially for I/O-bound tasks, but they add complexity and potential for race conditions.
Example Gunicorn Command Line / Configuration
# Assuming 4 CPU cores on your OVH instance
# (2 * 4) + 1 = 9 workers
gunicorn --workers 9 \
--threads 2 \
--bind 0.0.0.0:8000 \
your_project.wsgi:application
Alternatively, use a Gunicorn configuration file (e.g., gunicorn_config.py):
# gunicorn_config.py import multiprocessing bind = "0.0.0.0:8000" workers = multiprocessing.cpu_count() * 2 + 1 threads = 2 worker_class = "gthread" # Use 'gthread' for threaded workers # worker_connections = 1000 # If using gevent/eventlet worker_class # timeout = 30 # Adjust based on your longest expected request # graceful_timeout = 30 # accesslog = "/var/log/gunicorn/access.log" # errorlog = "/var/log/gunicorn/error.log" # loglevel = "info"
PHP-FPM Configuration (PHP Applications)
For PHP applications, PHP-FPM (FastCGI Process Manager) is the standard. Tuning its process management settings is crucial.
Process Manager Settings
PHP-FPM offers three primary process management strategies: static, dynamic, and ondemand. For high-traffic sites, dynamic or static are generally preferred.
dynamic: Starts with a few processes and spawns more as needed, up to a definedpm.max_children. It also kills idle processes to save resources. This is a good balance for fluctuating traffic.static: Keeps a fixed number of child processes running at all times (pm.max_children). This offers the most predictable performance but can be wasteful if traffic is low.ondemand: Starts no processes initially and spawns them only when requests arrive. This saves memory but can introduce latency for the first few requests.
Tuning Parameters
Key parameters within the PHP-FPM pool configuration (e.g., /etc/php/8.1/fpm/pool.d/www.conf):
pm.max_children: The maximum number of child processes that will be created. This is the most critical setting. Set it based on available RAM. A rough guideline:(Total RAM - RAM for OS/Nginx) / Average RAM per PHP-FPM process.pm.start_servers: Number of child processes to start when the FPM master process is started.pm.min_spare_servers: Minimum number of idle/spare child processes.pm.max_spare_servers: Maximum number of idle/spare child processes.request_terminate_timeout: Maximum time a script can run before being terminated. Essential for preventing runaway scripts.pm.process_idle_timeout: How long a child process can be idle before being killed (only fordynamicandondemand).
Example PHP-FPM Configuration (Dynamic Mode)
; /etc/php/8.1/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /run/php/php8.1-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 150 ; Adjust based on server RAM (e.g., 150 * ~20MB/process = 3GB) pm.start_servers = 20 pm.min_spare_servers = 10 pm.max_spare_servers = 30 pm.process_idle_timeout = 10s pm.max_requests = 500 ; Restart processes after this many requests to prevent memory leaks request_terminate_timeout = 60 ; seconds, adjust based on longest expected script execution ; Other settings... ; php_admin_value[memory_limit] = 256M ; php_admin_value[max_execution_time] = 120
Example PHP-FPM Configuration (Static Mode)
; /etc/php/8.1/fpm/pool.d/www.conf [www] user = www-data group = www-data listen = /run/php/php8.1-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = static pm.max_children = 100 ; Fixed number of processes, adjust based on RAM request_terminate_timeout = 60 ; seconds ; Other settings...
DynamoDB Performance Tuning for Shopify Integrations
When building custom Shopify integrations or headless commerce solutions that interact with AWS DynamoDB, performance tuning is critical. OVH infrastructure doesn’t directly host DynamoDB, but your application servers running on OVH will be making requests to it. Latency and throughput are key concerns.
Understanding Provisioned Throughput
DynamoDB operates on a provisioned throughput model (or on-demand). For predictable performance and cost management, provisioned throughput is often preferred for known workloads. You define Read Capacity Units (RCUs) and Write Capacity Units (WCUs).
Read Capacity Units (RCUs)
One RCU can perform one strongly consistent read per second for an item up to 4 KB in size, or two eventually consistent reads per second for an item up to 4 KB. Reads larger than 4 KB consume more RCUs.
Write Capacity Units (WCUs)
One WCU can perform one write per second for an item up to 1 KB in size. Writes larger than 1 KB consume more WCUs.
Strategies for Optimization
1. Efficient Data Modeling
DynamoDB is a NoSQL database, and its performance is heavily influenced by data modeling. Design your tables with access patterns in mind. Use single-table design where appropriate to minimize the need for multiple queries.
2. Indexing (GSI & LSI)
Global Secondary Indexes (GSIs) and Local Secondary Indexes (LSIs) are crucial for supporting query patterns that differ from the primary key. Be mindful that GSIs consume their own provisioned throughput and add cost.
3. Query Optimization
Use Query operations (which require a partition key) over Scan operations whenever possible. Scans read every item in the table and are very inefficient and costly for large tables.
Example Python SDK (Boto3) for Efficient Queries
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('your_shopify_integration_table')
# Efficient query using partition key
# Assuming 'order_id' is the partition key
try:
response = table.query(
KeyConditionExpression=Key('order_id').eq('shopify_order_12345')
)
items = response['Items']
print(f"Found {len(items)} items for order_id shopify_order_12345")
except Exception as e:
print(f"Error querying DynamoDB: {e}")
# Avoid scans if possible. If a scan is absolutely necessary,
# consider using pagination and filtering at the application level.
# Example of a scan (use with extreme caution on large tables):
# try:
# response = table.scan(
# FilterExpression=Attr('status').eq('processing')
# )
# items = response['Items']
# print(f"Found {len(items)} items with status 'processing'")
# except Exception as e:
# print(f"Error scanning DynamoDB: {e}")
4. Auto Scaling
Configure DynamoDB Auto Scaling to automatically adjust provisioned throughput based on actual traffic. This helps manage costs and ensures performance during traffic spikes without manual intervention. Set appropriate minimum and maximum values for RCUs and WCUs.
5. Client-Side SDK Configuration
Ensure your AWS SDK (e.g., Boto3 for Python) is configured correctly. Consider:
- Region: Ensure your application servers on OVH are configured to use the DynamoDB region closest to them (or the region where your DynamoDB table resides) to minimize network latency.
- Retries and Backoff: The SDK handles retries for throttled requests, but understanding and potentially tuning the retry strategy can be beneficial.
- Connection Pooling: For applications making frequent calls, ensure efficient connection management.
Example Boto3 Configuration (Region)
import boto3
# Explicitly set the region to minimize latency
# Replace 'us-east-1' with your DynamoDB table's region
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Or configure via environment variables or AWS config files
# export AWS_DEFAULT_REGION='us-east-1'
6. Monitoring and Alarms
Set up CloudWatch alarms for key DynamoDB metrics such as ThrottledRequests, ConsumedReadCapacityUnits, and ConsumedWriteCapacityUnits. This allows you to proactively identify bottlenecks and adjust provisioned throughput or optimize queries.