The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on Google Cloud for Python
Nginx as a High-Performance Frontend for Python Applications
When deploying Python web applications on Google Cloud, Nginx serves as an indispensable component for handling incoming traffic, serving static assets, and acting as a reverse proxy to your application servers (Gunicorn or PHP-FPM). Proper Nginx tuning is critical for maximizing throughput and minimizing latency.
Nginx Worker Processes and Connections
The `worker_processes` directive dictates how many worker processes Nginx will spawn. A common best practice is to set this to the number of CPU cores available on your instance. The `worker_connections` directive sets the maximum number of simultaneous connections that each worker process can handle. The total number of connections is `worker_processes * worker_connections`.
For a typical Google Cloud Compute Engine instance with 4 vCPUs, a good starting point would be:
worker_processes 4;
events {
worker_connections 1024;
}
To determine the optimal number of worker processes, you can use the `nproc` command on your instance:
nproc
The `worker_connections` value should be set considering your expected peak load and the available memory. A value of 1024 is often a safe default, but this can be increased if your application servers can handle a higher concurrency per worker.
Optimizing Keep-Alive Connections
HTTP Keep-Alive allows a single TCP connection to be reused for multiple HTTP requests, significantly reducing the overhead of establishing new connections. Tuning `keepalive_timeout` and `keepalive_requests` can improve performance.
A longer `keepalive_timeout` can be beneficial for clients that make frequent requests, but it also ties up worker connections. A reasonable starting point is 65 seconds. `keepalive_requests` limits the number of requests that can be served over a single keep-alive connection.
http {
# ... other http directives ...
keepalive_timeout 65;
keepalive_requests 100;
# ... server blocks ...
}
Gzip Compression for Static and Dynamic Content
Compressing responses with Gzip can drastically reduce bandwidth usage and improve load times. Enable Gzip for both static assets and dynamic responses from your Python application.
http {
# ... other http directives ...
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
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;
# ... server blocks ...
}
gzip_comp_level (1-9) controls the compression level; 6 is a good balance between CPU usage and compression ratio. gzip_types specifies the MIME types to compress.
Caching Strategies with Nginx
Leverage Nginx’s ability to cache static assets and even dynamic responses to reduce load on your backend and improve response times. For static assets, use `expires` directives.
server {
# ... server directives ...
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# ... other locations ...
}
For more advanced caching of dynamic content, consider Nginx’s proxy_cache directives. This requires careful configuration of cache keys and zones.
http {
# ... other http directives ...
proxy_cache_path /var/cache/nginx/my_app levels=1:2 keys_zone=my_app_cache:10m max_size=10g inactive=60m use_temp_path=off;
server {
# ... server directives ...
location / {
proxy_pass http://your_gunicorn_or_php_fpm_upstream;
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;
}
}
}
Tuning Gunicorn for Python WSGI 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 worker type.
Gunicorn Worker Processes and Threads
The number of worker processes is crucial. A common recommendation is `(2 * number_of_cores) + 1`. For applications that are I/O bound (e.g., making many external API calls or database queries), using Gunicorn’s `gevent` or `eventlet` workers with threads can be more efficient than the default `sync` workers.
To start Gunicorn with 4 worker processes (assuming 2 cores) and `sync` workers:
gunicorn --workers 5 --bind 0.0.0.0:8000 myapp.wsgi:application
If your application is heavily I/O bound and you want to use `gevent` workers:
pip install gevent gunicorn --worker-class gevent --workers 5 --threads 2 --bind 0.0.0.0:8000 myapp.wsgi:application
Here, `–threads 2` means each `gevent` worker can handle up to 2 concurrent requests using green threads. The total concurrency is `workers * threads`. Experimentation is key to finding the optimal balance.
Gunicorn Timeouts and Keep-Alive
--timeout specifies the number of seconds Gunicorn will wait for a worker to respond before considering it dead. This should be set higher than your longest expected request, but not so high that it masks performance issues. `keepalive` (in seconds) is the duration Gunicorn will keep connections alive.
gunicorn --workers 5 --timeout 120 --keepalive 60 --bind 0.0.0.0:8000 myapp.wsgi:application
Note that Gunicorn’s `keepalive` is distinct from Nginx’s `keepalive_timeout`. Nginx will manage the client connection, and Gunicorn will handle the worker process connection.
Tuning PHP-FPM for PHP Applications
For PHP applications, PHP-FPM (FastCGI Process Manager) is the standard. Its configuration significantly impacts performance.
PHP-FPM Process Manager Settings
PHP-FPM offers several process management strategies: `static`, `dynamic`, and `ondemand`. `dynamic` is often a good balance, allowing FPM to scale the number of workers based on load.
; /etc/php/X.Y/fpm/pool.d/www.conf pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 2 pm.max_spare_servers = 10 pm.max_requests = 500
pm.max_children is the maximum number of child processes that will be spawned. This is a critical setting; setting it too high can exhaust server memory. pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers control how FPM dynamically manages the number of workers. pm.max_requests defines how many requests a child process should execute before respawning, helping to prevent memory leaks.
For a static configuration, suitable for predictable loads or when you want to avoid the overhead of dynamic scaling:
; /etc/php/X.Y/fpm/pool.d/www.conf pm = static pm.max_children = 20
PHP-FPM Slowlog and Request Termination
Enabling the slowlog can help identify performance bottlenecks within your PHP code. `request_slowlog_timeout` defines how long a script can run before it’s logged as slow.
; /etc/php/X.Y/fpm/pool.d/www.conf request_slowlog_timeout = 10s slowlog = /var/log/php/php-fpm.slow.log
You can also configure `request_terminate_timeout` to automatically kill scripts that run too long, preventing them from holding up worker processes indefinitely. Be cautious with this setting, as it can lead to incomplete operations.
; /etc/php/X.Y/fpm/pool.d/www.conf request_terminate_timeout = 60s
Tuning MongoDB on Google Cloud
MongoDB performance is heavily influenced by hardware, indexing, query patterns, and configuration. On Google Cloud, consider the underlying disk I/O and network latency.
MongoDB WiredTiger Cache Tuning
The WiredTiger storage engine uses a cache to hold data and index blocks. The default cache size is 50% of the system RAM. For production environments, it’s often beneficial to dedicate more RAM to the WiredTiger cache, especially if your working set fits within it.
# /etc/mongod.conf
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 0.75 # Example: 75% of RAM for a 4GB instance
To determine the optimal `cacheSizeGB`, monitor your instance’s RAM usage and MongoDB’s cache hit ratio. A high cache hit ratio (e.g., > 90%) indicates that most data is being served from memory.
MongoDB Journaling and Write Concerns
Journaling ensures data durability by writing operations to a log before applying them to data files. While it adds overhead, it’s crucial for preventing data loss in case of crashes. `writeConcern` controls the acknowledgment of write operations.
# /etc/mongod.conf
operationProfiling:
slowOpThresholdMs: 100
# Journaling is enabled by default and recommended
storage:
journal:
enabled: true
For applications requiring high durability, consider setting `writeConcern` to `majority` in your application code or connection strings. This ensures writes are acknowledged by a majority of replica set members.
from pymongo import MongoClient
client = MongoClient('mongodb://host:port/?w=majority')
MongoDB Indexing and Query Optimization
This is arguably the most critical aspect of MongoDB performance. Regularly analyze slow queries using `db.slow_command_log` or the profiler. Ensure that all fields used in query predicates, sorts, and projections are covered by indexes.
Use `explain()` to understand query execution plans:
db.collection.find({ field1: "value1" }).sort({ field2: 1 }).explain("executionStats")
Look for `totalDocsExamined` being significantly higher than `nReturned`. This indicates inefficient scanning. Aim for `totalDocsExamined` to be close to `nReturned` by using appropriate indexes.
Google Cloud Specific Considerations
When deploying on Google Cloud, leverage managed services where appropriate. For MongoDB, consider using Google Cloud’s Database Migration Service or a managed MongoDB Atlas cluster for easier scaling and management.
For Compute Engine instances running Nginx, Gunicorn/PHP-FPM, and MongoDB, choose machine types with sufficient CPU and RAM. Disk I/O is paramount for databases; use SSD persistent disks for MongoDB. Network latency between services can also be a factor; ensure your instances are in the same region and, if possible, the same zone for low-latency communication.