EQProxy Load balancer y reverse proxy en Rust

Ejemplos

Recetario de fragmentos válidos contra el esquema, con la referencia de claves en Configuración. Los que incluyen [listener.web] y sus backends se pueden validar tal cual; los demás se añaden a una configuración existente.

Proxy TLS a dos backends con health check

schema_version = 1

[listener.web]
listen = "0.0.0.0:443"
mode = "http"
backend = "api"

[listener.web.tls]
cert = "certs/fullchain.pem"
key = "certs/privkey.pem"
alpn = ["h2", "http/1.1"]
min_version = "1.2"

[backend.api]
servers = [
  { addr = "10.0.0.11:9000", weight = 3 },
  { addr = "10.0.0.12:9000", weight = 1 },
]

[backend.api.health_check]
type = "http"
interval = "2s"
timeout = "1s"
fall = 3
rise = 2
http_path = "/healthz"
expect_status = "200-299"

Tres sondas fallidas sacan al servidor del reparto; dos éxitos lo devuelven. Los pesos 3:1 reparten tres de cada cuatro peticiones al primero.

Balanceo least-conn

[backend.api]
balance = "leastconn"
servers = [
  { addr = "10.0.0.11:9000", weight = 2 },
  { addr = "10.0.0.12:9000", weight = 1 },
  { addr = "10.0.0.13:9000", weight = 1, backup = true },
]
slow_start = "30s"

leastconn elige por conexiones activas divididas por peso; slow_start sube el peso de un servidor recuperado durante 30 s, y el backup solo entra cuando no queda ningún primario elegible.

Límites de conexiones

[listener.web]
listen = "0.0.0.0:8080"
mode = "http"
backend = "api"
maxconn = 2048

[listener.web.limits]
max_head_bytes = 16384
max_headers = 100
max_body_bytes = 1048576

[backend.api]
servers = [
  { addr = "10.0.0.11:9000", maxconn = 256 },
  { addr = "10.0.0.12:9000", maxconn = 256 },
]
maxqueue = 128
timeout_queue = "500ms"

Al tope del listener se rechaza al recién llegado; al tope de un servidor, el balanceador deja de elegirlo. Si todos lo alcanzan, se espera en la cola y, agotado timeout_queue, se responde 503.

Caché de respuestas

[listener.web.cache]
default_ttl = "60s"
max_memory_bytes = 67108864
max_entry_bytes = 1048576
disk_path = "cache/web"

Se guardan GET con estados 200, 301, 308, 404 y 410 y sin Set-Cookie; las peticiones con Cookie, Authorization, Range o no-store ni se consultan ni se guardan. La respuesta lleva X-Cache: HIT, MISS o REVALIDATED.

Rate limit por regla

[[listener.web.rules]]
name = "api"
host = "api.example.com"
path_prefix = "/api/"
use_backend = "api"
rate_limit = { rate = 10, per = "1s", burst = 20 }

rate = 10 con per = "1s" son 10 peticiones por segundo; con per = "60s", 10 por minuto. Sin burst, el cubo tiene capacidad rate. Con varias instancias, el límite es por instancia.

Acceso por ruta a distintos backends

[listener.web]
listen = "0.0.0.0:8080"
mode = "http"
backend = "web"

[[listener.web.rules]]
name = "api-v1"
path_prefix = "/api/v1/"
use_backend = "api"

[listener.web.rules.rewrite]
path_prefix_rewrite = "/api/"

[[listener.web.rules]]
name = "admin-writes"
path_prefix = "/admin/"
methods = ["POST", "PUT", "DELETE"]
use_backend = "admin"

[[listener.web.rules]]
name = "healthz"
path = "/healthz"
[listener.web.rules.direct_response]
status = 200
content_type = "text/plain"
body = "ok"

[backend.web]
server = "10.0.0.21:8080"

[backend.api]
server = "10.0.0.22:9000"

[backend.admin]
server = "10.0.0.23:9000"

/api/v1/users llega a api como /api/users; /healthz se contesta localmente; lo demás cae en web. El orden importa, y path_prefix no normaliza: /api/ casa también con rutas más largas.

Despliegue Docker con Compose

services:
  eqproxy:
    image: ghcr.io/exyonq/eqproxy:0.9.1
    command: ["run", "--config", "/etc/eqproxy/eqproxy.toml"]
    ports: ["8080:8080", "127.0.0.1:9001:9001"]
    volumes:
      - ./eqproxy.toml:/etc/eqproxy/eqproxy.toml:ro
      - ./admin.token:/etc/eqproxy/admin.token:ro
      - ./certs:/certs:ro
      - eqproxy-state:/var/lib/eqproxy
    depends_on: [app1, app2]
    restart: unless-stopped

  app1:
    image: ghcr.io/acme/storefront:1.0
    expose: ["9000"]

  app2:
    image: ghcr.io/acme/storefront:1.0
    expose: ["9000"]

volumes:
  eqproxy-state:
schema_version = 1

[admin]
listen = "127.0.0.1:9001"
token_file = "/etc/eqproxy/admin.token"

[listener.web]
listen = "0.0.0.0:8080"
mode = "http"
backend = "app"

[backend.app]
servers = [
  { addr = "app1:9000", weight = 1 },
  { addr = "app2:9000", weight = 1 },
]

[backend.app.health_check]
type = "tcp"
interval = "2s"
fall = 3
rise = 2

Los nombres de servicio se resuelven por DNS en segundo plano, sin bloquear los listeners. El fichero montado tiene que ser legible por el uid 65532, y el volumen de estado conserva el historial de config apply. Para recargar en caliente, edita el TOML y manda la señal: docker kill --signal=HUP eqproxy.