Nginx Load Balancer
A load balancer distributes traffic across multiple backend servers for high availability and scalability. Nginx can be a very effective load balancer in a MicroVM.
Architecture
Client
|
[MicroVM Nginx LB]
/ | \
[VM1] [VM2] [VM3]
App1 App2 App3
Load Balancing Algorithms
Round Robin (Default)
upstream app_backend {
server 10.0.0.2:3000;
server 10.0.0.3:3000;
server 10.0.0.4:3000;
}
server {
listen 80;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Weighted Round Robin
upstream app_backend {
server 10.0.0.2:3000 weight=3; # 60%
server 10.0.0.3:3000 weight=2; # 40%
}
IP Hash (Sticky Session)
upstream app_backend {
ip_hash;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
server 10.0.0.4:3000;
}
Least Connections
upstream app_backend {
least_conn;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
server 10.0.0.4:3000;
}
Load Balancer Configuration
cat > /etc/nginx/nginx.conf << 'LBCONF'
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
multi_accept on;
}
http {
# Upstream definition
upstream app_backend {
least_conn;
server 10.0.0.2:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.3:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.4:3000 max_fails=3 fail_timeout=30s;
# Backup server (only when all primary down)
server 10.0.0.5:3000 backup;
# Keepalive connections to the upstream
keepalive 32;
}
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=30r/s;
# Logging
log_format lb '$remote_addr - [$time_local] "$request" '
'$status $body_bytes_sent '
'upstream: $upstream_addr '
'response_time: $upstream_response_time';
access_log /var/log/nginx/access.log lb;
server {
listen 80;
server_name app.example.com;
# Rate limit
limit_req zone=ratelimit burst=50 nodelay;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
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_set_header Connection "";
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Retry on failure
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
}
# Health check endpoint
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
# Status page for monitoring
server {
listen 8080;
server_name _;
location /status {
stub_status on;
allow 10.0.0.0/24;
deny all;
}
}
}
LBCONF
nginx -t && nginx -s reload
Setting Up Multiple MicroVMs
Cluster Setup Script
#!/bin/bash
# Script: setup-lb-cluster.sh
set -e
NUM_BACKENDS=3
SUBNET="10.0.0"
BRIDGE="br0"
# Setup bridge
sudo ip link add $BRIDGE type bridge 2>/dev/null || true
sudo ip addr add ${SUBNET}.1/24 dev $BRIDGE 2>/dev/null || true
sudo ip link set $BRIDGE up
# Enable NAT
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -s ${SUBNET}.0/24 -o $(ip route | grep default | awk '{print $5}') -j MASQUERADE
# Create the TAP interfaces
for i in $(seq 0 $NUM_BACKENDS); do
sudo ip tuntap add dev tap${i} mode tap
sudo ip link set tap${i} up
sudo ip link set tap${i} master $BRIDGE
done
echo "Bridge and TAP interfaces ready"
echo "Backend IPs: ${SUBNET}.2 - ${SUBNET}.$((NUM_BACKENDS + 1))"
echo "Load Balancer IP: ${SUBNET}.1"
Run the Backend VMs
for i in 2 3 4; do
qemu-system-x86_64 \
-M microvm -enable-kvm -cpu host -m 512m -smp 2 \
-kernel vmlinuz \
-append "console=ttyS0 root=/dev/vda rw ip=${SUBNET}.${i}::${SUBNET}.1:255.255.255.0::eth0" \
-drive id=rootfs,file=app-rootfs.img,format=raw,if=none \
-device virtio-blk-device,drive=rootfs \
-netdev tap,id=net0,ifname=tap$((i-2)),script=no,downscript=no \
-device virtio-net-device,netdev=net0 \
-nographic -serial stdio &
done
Run the Load Balancer
qemu-system-x86_64 \
-M microvm -enable-kvm -cpu host -m 256m -smp 2 \
-kernel vmlinuz \
-append "console=ttyS0 root=/dev/vda rw ip=${SUBNET}.2::${SUBNET}.1:255.255.255.0::eth0" \
-drive id=rootfs,file=lb-rootfs.img,format=raw,if=none \
-device virtio-blk-device,drive=rootfs \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-device virtio-net-device,netdev=net0 \
-nographic -serial stdio
Session Persistence
# Cookie-based sticky session
upstream app_backend {
hash $cookie_sessionid consistent;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
}
# or with an upstream cookie
upstream app_backend {
server 10.0.0.2:3000;
server 10.0.0.3:3000;
sticky cookie srv_id expires=1h domain=.example.com path=/;
}
SSL Termination
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/ssl/certs/app.crt;
ssl_certificate_key /etc/ssl/private/app.key;
location / {
proxy_pass http://app_backend;
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 https;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
Monitoring
# Check the upstream connections
curl http://localhost:8080/status
# Check the response time per upstream
tail -f /var/log/nginx/access.log | grep upstream_response_time
# Script health check
#!/bin/bash
for i in 2 3 4; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://${SUBNET}.${i}:3000/health)
echo "VM ${SUBNET}.${i}: HTTP $STATUS"
done
Troubleshooting
A backend is down but traffic still goes to it
# Make sure max_fails is set
server 10.0.0.2:3000 max_fails=3 fail_timeout=30s;
# Use proxy_next_upstream
proxy_next_upstream error timeout http_502 http_503;
Uneven load distribution
# Use least_conn
least_conn;
# or set weights according to server capacity
server 10.0.0.2:3000 weight=5; # Stronger server
server 10.0.0.3:3000 weight=3; # Weaker server