Lab: Load Balancer Algorithms¶
Pairs with: Load Balancing
Three real backends (two fast, one deliberately slow) behind a real nginx upstream block. Swap round robin for weighted or least-connections by editing config and reloading — the same way you would in production — kill a backend and watch nginx route around it with zero client-visible errors.
docker-compose.yml¶
name: labs-load-balancer
services:
backend1:
image: python:3.12-slim
command: ["python", "/app/backend.py"]
environment:
BACKEND_ID: backend1
volumes:
- ./backend.py:/app/backend.py:ro
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 3s
timeout: 3s
retries: 20
backend2:
image: python:3.12-slim
command: ["python", "/app/backend.py"]
environment:
BACKEND_ID: backend2
volumes:
- ./backend.py:/app/backend.py:ro
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 3s
timeout: 3s
retries: 20
backend3:
image: python:3.12-slim
command: ["python", "/app/backend.py"]
environment:
BACKEND_ID: backend3
# This backend is deliberately slow — pairs with the least_conn exercise.
SLEEP_MS: "300"
volumes:
- ./backend.py:/app/backend.py:ro
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 3s
timeout: 3s
retries: 20
lb:
image: nginx:1.27
depends_on:
backend1:
condition: service_healthy
backend2:
condition: service_healthy
backend3:
condition: service_healthy
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "8080:80"
nginx.conf¶
worker_processes 1;
events { worker_connections 1024; }
http {
# Exercise 1-2: default round robin. Comment this block and uncomment
# one of the alternatives below, then `docker exec labs-load-balancer-lb-1 nginx -s reload`.
upstream backend_pool {
server backend1:8000 max_fails=2 fail_timeout=5s;
server backend2:8000 max_fails=2 fail_timeout=5s;
server backend3:8000 max_fails=2 fail_timeout=5s;
}
# Exercise 4: weighted round robin — backend1 gets 5x the traffic.
# upstream backend_pool {
# server backend1:8000 weight=5 max_fails=2 fail_timeout=5s;
# server backend2:8000 weight=1 max_fails=2 fail_timeout=5s;
# server backend3:8000 weight=1 max_fails=2 fail_timeout=5s;
# }
# Exercise 5: least connections — routes to whichever backend has the
# fewest active connections, instead of blindly rotating.
# upstream backend_pool {
# least_conn;
# server backend1:8000 max_fails=2 fail_timeout=5s;
# server backend2:8000 max_fails=2 fail_timeout=5s;
# server backend3:8000 max_fails=2 fail_timeout=5s;
# }
server {
listen 80;
location / {
proxy_pass http://backend_pool;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_connect_timeout 1s;
}
}
}
Exercises¶
The full walkthrough (verify round robin, kill a backend, swap to least-connections and weighted, measure the distribution difference) lives in the lab's README — including a real bind-mount caching gotcha on macOS/Colima worth knowing about: