Skillversitas

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