Cambiar tema

Balanceo de carga con Nginx en la práctica: configuración upstream y comprobaciones de salud

Easton editorial illustration: large Nginx upstream distributor steering weighted requests toward three servers with health indicators

El móvil vibra sin parar. Abres el panel de monitorización y la barra de estado de backend1 está en rojo: un servidor de aplicaciones se cayó.

Era el Doble 11 (el Black Friday chino) y solo teníamos dos servidores backend. En la configuración de Nginx figuraba upstream backend { server backend1; server backend2; }, simétrico en apariencia. Pero backend1 absorbía el 70 % del tráfico porque iba primero en el archivo y usábamos round-robin por defecto — sin pesos ni comprobación de salud.

Cuando backend1 cayó, las peticiones de pedidos seguían yendo al servidor muerto. Nginx no sabía que había caído y seguía reenviando. ¿Qué veían los usuarios? Páginas de error 500. Pasaron 15 minutos hasta que operaciones retiró backend1 del upstream manualmente.

Después de eso repasamos: el upstream de Nginx no es solo listar direcciones. Pesos, comprobaciones de salud y conmutación por error son lo que hace falta en producción. Este artículo recoge los errores que cometimos y lo que aprendimos, incluido cómo implementar comprobación activa con Nginx open source.

1. Configuración básica de upstream: de un solo servidor al clúster

El bloque upstream tiene un papel sencillo: agrupar varios servidores en un grupo lógico para que Nginx sepa a dónde enviar las peticiones. Pero sus parámetros son más ricos de lo que muchos imaginan.

upstream backend {
  zone backend 64k;
  server backend1.example.com weight=3 max_fails=2 fail_timeout=30s;
  server backend2.example.com;
  server backup1.example.com backup;
}

Línea por línea:

zone backend 64k: región de memoria compartida. Los procesos worker de Nginx deben compartir el estado de los backends (quién está vivo, quién caído). 64k es un punto de partida; con muchos servidores puedes subirlo. Sin esta línea, cada worker gestiona su propio estado y surgen problemas.

weight=3: peso. backend1 tiene peso 3 y backend2 el valor por defecto 1. De cada 4 peticiones, 3 van a backend1 y 1 a backend2. Sirve cuando los servidores no son homogéneos — por ejemplo backend1 con 8 núcleos y 16 GB y backend2 con 4 núcleos y 8 GB.

max_fails=2: umbral de fallos. En la ventana de fail_timeout, si fallan 2 peticiones a ese servidor, Nginx lo marca como no disponible. El valor por defecto es 1, demasiado sensible: un simple jitter de red lo dispara. En producción conviene 2 o 3.

fail_timeout=30s: doble significado. Primero, la ventana de conteo de fallos es de 30 segundos; segundo, tras marcarse como no disponible, Nginx reintenta la conexión pasados 30 segundos. El valor por defecto es 10 s y puede quedarse corto para servicios de arranque lento.

backup: servidor de respaldo. Solo recibe tráfico cuando todos los principales están caídos. Útil para un servidor más modesto como red de seguridad.

Otro parámetro es down, para marcar un servidor fuera de servicio manualmente, habitual en mantenimiento:

server backend3.example.com down;  # Baja temporal para mantenimiento

En la práctica, muchos omiten zone. Cada worker mantiene su estado por separado: uno detecta un fallo y los demás siguen enviando tráfico al servidor caído. Con zone, el estado se sincroniza.

2. Cinco estrategias de balanceo de carga: ¿cuándo usar cada una?

¿Basta el round-robin por defecto? Depende.

He visto proyectos que llevan años con round-robin por defecto sin problemas. Pero con WebSocket, sesiones de carrito o escenarios de cache stampede, la estrategia por defecto empieza a fallar. Esta tabla resume mi criterio de elección:

EscenarioEstrategia recomendadaMotivo
API sin estadoround-robinReparto uniforme, sin requisitos especiales
Servicio WebSocketleast_connMonitoriza conexiones en tiempo real, evita sobrecarga
Carrito de e-commerceip_hashLas peticiones del mismo usuario van al mismo servidor
Proxy de cachéhash key=$uriReduce invalidación y cache stampede
Entorno de pruebasrandomValidación rápida, configuración simple

round-robin (por defecto)

Sin configurar nada, es round-robin. Las peticiones se reparten en orden:

upstream backend {
  server backend1.example.com;
  server backend2.example.com;
  server backend3.example.com;
}

Ideal para servicios sin estado. Cada petición es independiente. La mayoría de APIs REST encajan aquí.

least_conn (menor número de conexiones)

Prioriza el servidor con menos conexiones activas:

upstream websocket_app {
  least_conn;
  server ws1.example.com:8080;
  server ws2.example.com:8080;
}

Escenario típico de WebSocket. Un usuario abre una conexión larga y el número de conexiones varía mucho. Con round-robin, un servidor puede acumular muchas conexiones largas y seguir recibiendo peticiones nuevas. least_conn monitoriza en tiempo real y envía al servidor con menor carga.

ip_hash (hash por IP)

Calcula un hash a partir de la IP del cliente; la misma IP siempre va al mismo servidor:

upstream shopping_cart {
  ip_hash;
  server cart1.example.com;
  server cart2.example.com;
}

Para escenarios que exigen consistencia de sesión. En un carrito de compra, si el usuario añade productos en cart1 y la siguiente petición cae en cart2 por round-robin, pierde el carrito (salvo que uses almacenamiento de sesión distribuido). ip_hash lo evita.

Pero ip_hash tiene un límite: si un servidor cae, los usuarios que iban a él se reasignan y pierden la sesión. Conviene cuando la sesión no es crítica o va con almacenamiento compartido.

hash (hash consistente)

Clave de hash personalizable, con algoritmo de hash consistente:

upstream cache_proxy {
  hash $uri consistent;
  server cache1.example.com;
  server cache2.example.com;
}

Preferido en proxies de caché. $uri es la ruta de la petición. El parámetro consistent activa hash consistente: al añadir o quitar servidores, solo parte de las claves se remapean, no todo el conjunto. La tasa de aciertos de caché no cae en picado.

random

Reparto aleatorio simple:

upstream test_backend {
  random;
  server test1.example.com;
  server test2.example.com;
}

Basta para entornos de prueba. En producción no lo recomiendo: poco control.

En mi experiencia, la mayoría de aplicaciones web bastan con round-robin o least_conn. ip_hash y hash son para casos concretos; no los uses solo para «parecer más avanzado».

3. Comprobación de salud pasiva: max_fails y fail_timeout

«Pasiva» significa que Nginx no sondea activamente a los backends, sino que infiere el estado por el éxito o fracaso de las peticiones reales. Como no llamas a la puerta del vecino para preguntar «¿estás bien?», sino que observas si sale a pasear al perro o recoge el correo.

max_fails y fail_timeout configuran ese mecanismo de observación:

upstream backend {
  server backend1.example.com max_fails=3 fail_timeout=30s;
  server backend2.example.com max_fails=3 fail_timeout=30s;
}

location / {
  proxy_pass http://backend;
  proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
}

proxy_next_upstream define qué cuenta como fallo. error es error de conexión, timeout es tiempo agotado, http_500 a http_504 son códigos HTTP de error. En esos casos, Nginx reenvía a otro servidor y suma un fallo al actual.

Tres fallos en 30 segundos y el servidor queda marcado como no disponible. Durante los siguientes 30 segundos Nginx no le envía tráfico. Pasado ese tiempo, intenta una vez: si responde, vuelve; si no, espera otros 30 segundos.

El problema: la respuesta al fallo es lenta. Hacen falta al menos tres peticiones reales fallidas para retirar el servidor. Esos tres usuarios ya recibieron error.

Peor aún: un servidor recién arrancado puede estar inestable. Con max_fails=1, un fallo durante el arranque puede dejarlo excluido indefinidamente.

Mis recomendaciones:

  • max_fails en 2 o 3, para tolerar jitter puntual
  • fail_timeout de 30 segundos o más, para dar margen de recuperación
  • proxy_next_upstream con la lista completa de tipos de error

La ventaja de la comprobación pasiva es la simplicidad: Nginx open source la soporta de serie. La desventaja es depender del tráfico de usuarios, que sufre antes que el sistema reaccione. Si necesitas detectar fallos antes y sondear activamente, toca comprobación activa.

4. Comprobación de salud activa: NGINX Plus y alternativas open source

La lógica activa: Nginx envía peticiones de sondeo periódicas al backend (por ejemplo GET /health) y juzga la salud por la respuesta. No hace falta esperar a que falle un usuario; Nginx retira el servidor antes.

La opción oficial es NGINX Plus (comercial), $3,675/año por instancia. Diez instancias son $36,750 al año. Para muchas empresas, el precio pesa.

La alternativa open source es nginx_upstream_check_module, desarrollado por el equipo de Taobao. Requiere recompilar Nginx con el módulo, pero la funcionalidad es amplia:

CaracterísticaNGINX Plusnginx_upstream_check_module
Precio$3,675/añoGratuito, open source
Comprobación HTTP
Comprobación TCP
Comprobación MySQLNo
Comprobación FastCGINo
Página de estadoSí (check_status)

El módulo open source soporta MySQL y FastCGI, algo que NGINX Plus no ofrece. Si tu backend es PHP-FPM o MySQL, encaja mejor.

Ejemplo de configuración de nginx_upstream_check_module

upstream backend {
  server backend1.example.com:8080;
  server backend2.example.com:8080;

  check interval=3000 rise=2 fall=5 timeout=1000 type=http;
  check_http_send "GET /health HTTP/1.0\r\n\r\n";
  check_http_expect_alive http_2xx http_3xx;
}

server {
  location / {
    proxy_pass http://backend;
  }

  location /upstream_status {
    check_status json;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
  }
}

Parámetros:

  • interval=3000: sondeo cada 3 segundos
  • rise=2: tras 2 éxitos seguidos, el servidor se marca como sano (un arranque reciente puede ser inestable)
  • fall=5: tras 5 fallos seguidos, se marca como no disponible (tolera timeouts puntuales)
  • timeout=1000: timeout del sondeo, 1 segundo
  • type=http: protocolo HTTP (también tcp, ssl_hello, mysql, ajp, fastcgi)

check_http_send define el contenido del sondeo. Aquí un GET /health simple. El backend debe implementar /health y devolver 200 o un 3xx.

check_http_expect_alive indica qué códigos cuentan como saludables. http_2xx y http_3xx cubren 200-299 y 300-399.

check_status expone una página de monitorización. El formato json encaja con Prometheus o Zabbix. El bloque allow/deny es control de acceso — no expongas esta página a cualquiera.

Instalación del módulo

nginx_upstream_check_module requiere compilación. Pasos aproximados:

# Descargar el código del módulo
git clone https://github.com/yaoweibin/nginx_upstream_check_module.git

# Descargar el código fuente de Nginx
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar -zxvf nginx-1.24.0.tar.gz

# Aplicar parche (según versión de Nginx)
cd nginx-1.24.0
patch -p1 < ../nginx_upstream_check_module/check_1.20.1+.patch

# Compilar
./configure --add-module=../nginx_upstream_check_module
make && make install

Con Docker puedes construir una imagen propia con el módulo o usar imágenes de la comunidad.

5. Producción: seguridad y monitorización

En producción, la configuración de comprobaciones de salud tiene detalles donde es fácil tropezar. Tres principios:

Seguridad: tres puntos clave

1. Restringir el acceso a la página check_status

La página de estado expone la lista de backends y su salud. Si es accesible desde fuera, un atacante ve la topología interna y sabe qué servidor está caído — el momento ideal para atacar.

location /upstream_status {
  check_status json;
  allow 127.0.0.1;       # Acceso local
  allow 10.0.0.0/8;      # IP de red interna
  deny all;              # Denegar el resto
}

O más estricto: solo IPs de servidores de monitorización concretos.

2. Usar un puerto dedicado para health checks

Los sondeos son frecuentes (cada 3-5 segundos). Si apuntas al puerto de negocio, los logs del backend se llenan de /health y el rendimiento sufre.

Conviene dos puertos: negocio (p. ej. 8080) y salud (p. ej. 8888). El puerto de salud solo devuelve un código de estado, sin lógica de negocio ni logs.

check interval=5000 rise=2 fall=3 timeout=2000 type=http port=8888;

port=8888 fija el puerto de sondeo.

3. Que /health no devuelva información sensible

/health solo necesita un código de estado; no versiones, configuración ni uso de memoria. Esa información ayuda a localizar vulnerabilidades.

# Ejemplo de implementación en el backend
@app.route('/health')
def health():
    return '', 200   # Solo código de estado

Integración con monitorización: salida JSON

check_status admite varios formatos. json encaja con sistemas de monitorización:

curl http://127.0.0.1/upstream_status

Ejemplo de salida:

{
  "servers": {
    "total": 3,
    "generation": 12,
    "server": [
      {"index": 0, "name": "10.0.0.1:8080", "status": "up", "rise": 5, "fall": 0, "type": "http"},
      {"index": 1, "name": "10.0.0.2:8080", "status": "up", "rise": 3, "fall": 0, "type": "http"},
      {"index": 2, "name": "10.0.0.3:8080", "status": "down", "rise": 0, "fall": 5, "type": "http"}
    ]
  }
}

generation es un contador de cambios de configuración. Cada vez que modificas el upstream y haces reload, sube. Los scripts de monitorización pueden compararlo para confirmar que la configuración está activa.

Ajuste de parámetros

interval no por debajo de 3000 ms

Una frecuencia muy alta presiona al backend. 3-5 segundos bastan; el retraso de detección sigue siendo de segundos y no afecta la experiencia.

Equilibrio entre rise y fall

  • rise muy bajo (p. ej. 1): un servidor recién arrancado puede oscilar entre sano y no sano
  • fall muy bajo (p. ej. 1): un jitter de red retira el servidor de inmediato

Valores que uso: rise=2, fall=3 o fall=5. Tolera fallos breves y confirma fallos sostenidos antes de retirar.

Configuración completa de producción

upstream web_app {
  zone web_app 64k;
  server 10.0.0.1:8080 weight=3;
  server 10.0.0.2:8080;
  server 10.0.0.3:8080 backup;

  check interval=5000 rise=2 fall=3 timeout=2000 type=http port=8888;
  check_http_send "GET /health HTTP/1.1\r\nHost: app.example.com\r\n\r\n";
  check_http_expect_alive http_2xx;
}

server {
  listen 80;
  server_name app.example.com;

  location / {
    proxy_pass http://web_app;
    proxy_set_header Host $host;
    proxy_next_upstream error timeout http_502 http_503 http_504;
  }

  location /upstream_status {
    check_status json;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
  }
}

Esta configuración:

  • zone en memoria compartida sincroniza el estado entre workers
  • comprobación activa cada 5 segundos en puerto dedicado
  • página de estado solo accesible desde la red interna
  • proxy_next_upstream reenvía peticiones de backends caídos a servidores sanos

Conclusión

Volviendo al incidente del Doble 11. Después añadimos zone en memoria compartida, max_fails y fail_timeout, y más tarde compilamos nginx_upstream_check_module para comprobación activa. Cuando un servidor cae, Nginx lo detecta y lo retira en unos 5 segundos; los usuarios casi no ven errores.

La elección de estrategia de balanceo, en una frase: round-robin o least_conn para servicios sin estado; ip_hash o hash para servicios con estado. En comprobaciones de salud, en producción son obligatorias; con Nginx open source usa nginx_upstream_check_module y no olvides restringir check_status.

Si tu Nginx sigue con round-robin por defecto y sin comprobaciones, empieza por la pasiva (max_fails + fail_timeout). Es el cambio más pequeño y el efecto se nota al momento. Cuando esté estable, valora la comprobación activa. Prueba primero en entorno de test y luego sube a producción.

FAQ

¿Para qué sirve la configuración zone en el upstream de Nginx?
zone crea una región de memoria compartida para que varios procesos worker compartan el estado de los servidores backend. Sin zone, cada worker mantiene su propio estado y puede ocurrir que uno detecte un fallo mientras otros siguen enviando tráfico al servidor caído.
¿Cuál es la diferencia entre comprobación de salud pasiva y activa?
La pasiva observa el éxito o fracaso de las peticiones reales y depende del tráfico de usuarios; la detección es lenta. La activa envía peticiones de sondeo periódicas sin depender de usuarios, detecta fallos antes y puede retirar servidores antes de que el usuario se vea afectado.
¿Qué conviene más, nginx_upstream_check_module o NGINX Plus?
Cada uno tiene ventajas: NGINX Plus ofrece soporte oficial y documentación completa; nginx_upstream_check_module es gratuito y open source, y soporta comprobaciones MySQL/FastCGI (que NGINX Plus no). Con presupuesto ajustado, el módulo open source; si priorizas estabilidad y soporte oficial, NGINX Plus.
¿Cómo elegir entre round-robin y least_conn?
Para APIs sin estado, round-robin reparte de forma uniforme. Para WebSocket y conexiones largas, least_conn monitoriza conexiones en tiempo real y envía nuevas peticiones al servidor con menor carga, evitando acumular demasiadas conexiones largas en una sola máquina.
¿Cómo configurar los parámetros rise y fall en las comprobaciones de salud?
rise define cuántos éxitos seguidos marcan el servidor como sano; conviene 2 para evitar oscilaciones al arrancar. fall define cuántos fallos seguidos lo marcan como no disponible; 3 o 5 toleran fallos transitorios. Valores demasiado sensibles provocan falsos positivos.
¿Por qué hay que restringir el acceso a la página check_status?
La página de estado expone la lista de backends y su salud. Un atacante puede ver la topología interna y saber qué servidor está caído — el momento ideal para atacar. Limita el acceso a IPs internas o a servidores de monitorización específicos.

11 min de lectura · Publicado el: 27 abr 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog