Lección 2 de 212 min
Proxy inverso y balanceo de carga
proxy_pass, upstreams, algoritmos de balanceo, comprobaciones de salud y cabeceras X-Forwarded.
Tu tienda online va bien hasta que llega el Black Friday: un solo servidor con tu app de Node.js no da abasto. Arrancas tres copias de la app en tres máquinas, pero los usuarios solo conocen una dirección, mitienda.com. Alguien tiene que recibir cada petición y decidir a qué máquina mandarla, y dejar de enviar tráfico a la que se cuelgue.
Ese «alguien» es un proxy inverso que hace de balanceador de carga. En esta lección verás qué es un proxy, cómo configurarlo en Nginx, los algoritmos para repartir la carga, cómo detecta los servidores caídos y cómo proteger tu app con caché y límites de peticiones.
Proxies: intermediarios en el camino
Un proxy es un intermediario: recibe una petición, la reenvía en nombre de otro y devuelve la respuesta. Lo que cambia entre un tipo y otro es a quién representa.
Proxy directo (forward proxy)
Representa a los clientes: sus peticiones salen a internet a través de él.
- Representa a:
- Los clientes
- Ejemplo:
- Squid
Un proxy directo se coloca delante de los clientes. Los equipos de una empresa o un colegio se configuran para enviarle todas sus peticiones, y él las hace en su nombre. La web de destino ve la IP del proxy, no la de cada empleado.
Se usa para filtrar qué webs se pueden visitar, registrar el tráfico, guardar en caché lo que se descarga mucho o salir a internet desde una red que no tiene acceso directo.
Úsalo cuando
En redes corporativas o educativas que controlan la salida a internet, y en entornos de servidores que solo pueden salir por un punto autorizado.
Ten en cuenta
No es lo que montas delante de tu web: eso es un proxy inverso. Y para HTTPS, un proxy directo normal solo ve a qué dominio vas, no el contenido; para inspeccionarlo, tendría que descifrar el tráfico con un certificado instalado en cada equipo.
# El equipo envía su tráfico a través del proxy de la empresa
export HTTPS_PROXY=http://proxy.empresa.local:3128
curl https://ejemplo.com
# ejemplo.com ve la IP del proxy, no la de este equipoProxy inverso (reverse proxy)
Representa a tus servidores: los clientes solo hablan con él.
- Representa a:
- Los servidores
- Ejemplos:
- Nginx, HAProxy, Traefik
Un proxy inverso se coloca delante de los servidores. El navegador cree que habla con mitienda.com, pero en realidad habla con Nginx, que reenvía cada petición a la app que corresponda dentro de tu red y devuelve su respuesta. Los clientes nunca ven las máquinas de detrás.
Es el sitio natural para todo lo que es común a tus apps: terminar TLS, comprimir, cachear, limitar peticiones, repartir la carga y dirigir cada ruta a un servicio distinto. Nginx, HAProxy, Traefik, Caddy y Envoy son proxies inversos, igual que los balanceadores de los proveedores cloud o una CDN.
Úsalo cuando
Prácticamente siempre que publicas una app en internet, aunque solo tengas un servidor: TLS, compresión y protección en un solo punto.
Ten en cuenta
Pasa a ser una pieza por la que entra todo: si cae, cae todo. En producción suele haber al menos dos. Y tu app ve las peticiones como si vinieran del proxy, salvo que le pases las cabeceras correctas.
server {
listen 443 ssl;
server_name mitienda.com;
location / { proxy_pass http://127.0.0.1:3000; } # web (Next.js)
location /api/ { proxy_pass http://127.0.0.1:8080; } # API
location /admin/ { proxy_pass http://10.0.0.20:4000; } # panel interno
}proxy_pass
La directiva de Nginx que reenvía la petición a otro servidor.
- Módulo:
- ngx_http_proxy_module
- Trampa:
- La barra final
Dentro de un location, proxy_pass http://127.0.0.1:3000; hace que Nginx abra una conexión con tu app, le envíe la petición, espere la respuesta y se la devuelva al cliente. El destino puede ser una dirección y un puerto, un socket local o el nombre de un grupo upstream, que verás enseguida.
Un detalle importa mucho: si la dirección no lleva ruta, Nginx pasa la ruta original tal cual; si lleva una, aunque sea solo una / final, sustituye el prefijo del location por esa ruta.
Úsalo cuando
Para poner Nginx delante de cualquier app que hable HTTP: Node.js, Python, Java, Go, PHP con un servidor HTTP propio…
Ten en cuenta
La barra final es la fuente de errores número uno: con location /api/ y proxy_pass http://app:3000/, /api/usuarios llega a la app como /usuarios. Y los WebSockets necesitan cabeceras extra (Upgrade y Connection), que Nginx no pasa por su cuenta.
# Sin ruta: pasa la URI tal cual
location /api/ {
proxy_pass http://127.0.0.1:8080; # /api/usuarios → /api/usuarios
}
# Con ruta (la barra final cuenta): sustituye el prefijo
location /v2/ {
proxy_pass http://127.0.0.1:9090/; # /v2/usuarios → /usuarios
}
# WebSockets: hay que pasar la petición de «upgrade»
location /chat/ {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Cabeceras Host y X-Forwarded-*
Le cuentan a tu app quién es el cliente real y qué dominio y protocolo usó.
- IP del cliente:
- X-Forwarded-For
- Estándar:
- Forwarded
Para tu app, la petición llega desde Nginx: la IP de origen es la del proxy y la conexión es HTTP aunque el usuario usara HTTPS. Además, Nginx envía por defecto como Host el nombre del destino de proxy_pass, no el dominio original. Las cabeceras recuperan esa información.
Host lleva el dominio que pidió el usuario, X-Forwarded-For la lista de IP por las que ha pasado la petición, empezando por la del cliente, y X-Forwarded-Proto si usó http o https. Existe también una cabecera estándar, Forwarded, menos extendida.
Úsalo cuando
Siempre que tu app necesite la IP real (registros, límites, antifraude), genere URLs absolutas o compruebe si la conexión era segura, por ejemplo para marcar las cookies como Secure.
Ten en cuenta
Un cliente puede enviar su propio X-Forwarded-For falso. Configura tu app para confiar en esas cabeceras solo cuando vienen de tu proxy (en Express, con trust proxy) y quédate con la IP que añadió él.
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
# Lo que recibe la app:
# Host: mitienda.com
# X-Forwarded-For: 83.45.12.7
# X-Forwarded-Proto: httpsBalanceo de carga con upstream
Cuando tienes varias copias de tu app, el proxy inverso reparte las peticiones entre ellas. Así aguantas más tráfico y, si una máquina cae, las demás siguen atendiendo. La forma de elegir a quién le toca cada petición es el algoritmo de balanceo.
El bloque upstream
Un grupo de servidores con nombre al que proxy_pass puede enviar tráfico.
- Módulo:
- ngx_http_upstream_module
- Por defecto:
- Round robin
En el bloque http declaras upstream app { … } con una línea server por cada copia de tu app, y luego usas proxy_pass http://app;. Nginx reparte las peticiones entre ellos según el algoritmo que elijas, que por defecto es round robin.
Cada server admite parámetros: weight para el peso, max_fails y fail_timeout para detectar caídas, backup para un servidor de reserva que solo recibe tráfico si fallan los demás, y down para sacarlo del reparto a mano.
Úsalo cuando
En cuanto tengas más de una instancia de tu app, o quieras un servidor de reserva preparado.
Ten en cuenta
Por defecto, Nginx abre una conexión nueva con el backend en cada petición. Con keepalive en el upstream las reutiliza, pero necesitas también proxy_http_version 1.1 y vaciar la cabecera Connection.
upstream app {
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000 backup; # solo si fallan los demás
keepalive 16; # reutiliza conexiones con los backends
}
server {
listen 80;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection ""; # necesario para keepalive
}
}Round robin
Por turnos: uno a cada servidor, y vuelta a empezar.
- Directiva:
- Ninguna
- Mira la carga:
- No
Es el algoritmo por defecto: la primera petición va al primer servidor, la segunda al segundo, la tercera al tercero y la cuarta, de nuevo, al primero. Es sencillo, predecible y reparte bien cuando todos los servidores son iguales y todas las peticiones cuestan más o menos lo mismo.
Úsalo cuando
Con servidores idénticos y peticiones de duración parecida, que es el caso más común en una API sin estado.
Ten en cuenta
No mira cuánto trabajo tiene cada servidor: si a uno le tocan varias peticiones lentas seguidas, seguirá recibiendo su turno aunque esté saturado. Pruébalo en el simulador enviando peticiones lentas.
upstream app {
# Sin directiva de algoritmo: round robin
server a:3000;
server b:3000;
server c:3000;
}
# Peticiones: 1→a, 2→b, 3→c, 4→a, 5→b…Round robin con pesos
Más peticiones a las máquinas más potentes, según el peso que les des.
- Parámetro:
- weight
- Peso por defecto:
- 1
Con weight=3, un servidor recibe el triple de peticiones que uno con el peso por defecto, que es 1. Nginx no las agrupa (tres seguidas a uno y luego una a cada otro), sino que las intercala para que la carga sea uniforme en el tiempo.
Los pesos también sirven para meter una versión nueva con cuidado: le das un peso pequeño, miras si da errores y vas subiéndolo.
Úsalo cuando
Cuando tus servidores tienen distinta capacidad, o para mandar solo una parte del tráfico a una máquina nueva.
Ten en cuenta
El peso lo decides tú y no cambia solo: si una máquina potente se satura por otro motivo, seguirá recibiendo su parte. Los pesos también se tienen en cuenta con least_conn.
upstream app {
server grande:3000 weight=3; # el triple de capacidad
server mediano:3000; # weight=1 por defecto
server pequeno:3000;
}
# De cada 5 peticiones: 3 a grande, 1 a mediano y 1 a pequeno,
# intercaladas: grande, mediano, grande, pequeno, grandeleast_conn
Cada petición va al servidor que tiene menos conexiones activas en ese momento.
- Directiva:
- least_conn
- Mira la carga:
- Sí
Con least_conn;, Nginx cuenta cuántas peticiones tiene en curso con cada servidor y manda la nueva al que menos tiene. Si una máquina está atascada con peticiones lentas, deja de recibir más hasta que se libere. En caso de empate, reparte por turnos.
Úsalo cuando
Cuando la duración de las peticiones varía mucho: exportaciones de informes, subidas de archivos, conexiones largas como WebSockets.
Ten en cuenta
Cada worker de Nginx lleva su propia cuenta, salvo que el upstream tenga una zona de memoria compartida (zone), así que el reparto es aproximado. Con peticiones cortas y parecidas, apenas se diferencia de round robin.
upstream informes {
least_conn;
server a:3000;
server b:3000;
server c:3000;
}
# Si a tiene 5 informes en curso, b 1 y c 2, la siguiente va a bip_hash
El mismo cliente va siempre al mismo servidor, según su dirección IP.
- Directiva:
- ip_hash
- Clave:
- Tres primeros números de la IPv4
Con ip_hash;, Nginx calcula un hash a partir de la IP del cliente y lo usa para elegir el servidor. Como el hash siempre da lo mismo para la misma IP, ese cliente cae siempre en la misma máquina: lo que se llama sesiones pegajosas (sticky sessions). En IPv4 solo usa los tres primeros números, así que 83.45.12.7 y 83.45.12.200 van juntos.
Si su servidor no está disponible, el cliente pasa a otro. Para sacar uno en mantenimiento sin descolocar a los demás clientes, márcalo con down en vez de borrarlo.
Úsalo cuando
Con aplicaciones antiguas que guardan la sesión en la memoria de cada servidor y no puedes cambiar a tiempo.
Ten en cuenta
Muchos usuarios detrás de la misma red (una empresa, una operadora móvil) comparten IP y acaban todos en el mismo servidor. Y si un servidor cae, sus usuarios pierden la sesión. La solución de fondo es guardar las sesiones en un almacén compartido como Redis, que verás en el módulo de caché y mensajería.
upstream app {
ip_hash;
server a:3000;
server b:3000;
server c:3000 down; # mantenimiento: conserva el reparto de los demás
}
# 83.45.12.7 y 83.45.12.200 comparten «83.45.12» → mismo servidorComprobaciones de salud
Cómo detecta Nginx que un servidor ha caído y deja de enviarle tráfico.
- Código abierto:
- Pasivas
- Activas:
- NGINX Plus
El Nginx de código abierto hace comprobaciones pasivas: no pregunta a los servidores, sino que observa las peticiones reales. Si un servidor acumula max_fails fallos (por defecto 1) dentro del periodo fail_timeout (por defecto 10 segundos), lo da por no disponible durante ese mismo fail_timeout. Pasado ese tiempo, lo vuelve a probar con una petición real: si responde, vuelve al reparto.
Qué cuenta como fallo y cuándo reintentar la petición en otro servidor lo decide proxy_next_upstream: por defecto, los errores de conexión y los tiempos de espera agotados. Así el usuario no nota el fallo, porque su petición se repite en otra máquina.
Úsalo cuando
Siempre que tengas un upstream con más de un servidor. Ajusta max_fails y fail_timeout para no descartar un servidor por un fallo aislado ni tardar demasiado en detectar uno caído.
Ten en cuenta
Las comprobaciones activas, que consultan una ruta como /salud cada pocos segundos aunque no haya tráfico, son una función de NGINX Plus, la versión comercial (directiva health_check). Con el Nginx de código abierto, alguna petición real fallará antes de que se detecte la caída. HAProxy, Traefik o los balanceadores de los proveedores cloud sí las incluyen.
upstream app {
server a:3000 max_fails=3 fail_timeout=30s;
server b:3000 max_fails=3 fail_timeout=30s;
}
location / {
proxy_pass http://app;
# Qué cuenta como fallo y cuándo reintentar en otro servidor
proxy_next_upstream error timeout http_502 http_503;
proxy_connect_timeout 2s;
}Pruébalo
En el simulador, elige round robin, tira el servidor B y envía varias peticiones. Fíjate en que las dos primeras que le tocan fallan y se reintentan en otro servidor, y luego B deja de recibir tráfico. Levántalo enseguida: Nginx no lo sabrá hasta que pase el fail_timeout.
Interactivo
Un balanceador con tres servidores
Elige el algoritmo, envía peticiones desde distintas IP y tira un servidor para ver cómo reacciona Nginx. El reloj avanza 1 s por petición.
Algoritmo
IP del cliente
A
En marcha- Atendidas
- 0
- Activas
- 0
- Fallos
- 0/2
0 % del tráfico
B
En marcha- Atendidas
- 0
- Activas
- 0
- Fallos
- 0/2
0 % del tráfico
C
En marcha- Atendidas
- 0
- Activas
- 0
- Fallos
- 0/2
0 % del tráfico
Round robin reparte por turnos (A, B, C, A…) sin mirar cuánto trabajo tiene cada servidor. Prueba a enviar varias peticiones lentas.
Últimos eventos
- Aún no hay peticiones.
upstream app {
# round robin: el algoritmo por defecto
server 10.0.0.1:3000 max_fails=2 fail_timeout=10s;
server 10.0.0.2:3000 max_fails=2 fail_timeout=10s;
server 10.0.0.3:3000 max_fails=2 fail_timeout=10s;
}Proteger y aliviar a tu app
Como todas las peticiones pasan por el proxy, es el sitio ideal para responder sin molestar a la app cuando ya conoces la respuesta, y para frenar a quien pide demasiado.
Caché del proxy (proxy_cache)
Nginx guarda respuestas de tu app y las reutiliza sin volver a pedirlas.
- Directiva:
- proxy_cache
- Estados:
- HIT, MISS, EXPIRED
Con proxy_cache_path defines dónde se guardan las respuestas en el disco y una zona de memoria para el índice; con proxy_cache la activas en un location. La primera petición llega a la app (un MISS) y Nginx guarda la respuesta; las siguientes, mientras sea válida, se sirven desde la caché (un HIT) sin tocar la app.
Por defecto respeta las cabeceras de caché de tu app, solo guarda peticiones GET y HEAD, y no guarda respuestas que crean una cookie. Con proxy_cache_use_stale puede incluso servir una copia caducada si la app está caída.
Úsalo cuando
Para respuestas iguales para todos y costosas de generar: un catálogo, una página de inicio, un listado público. Incluso unos segundos de caché alivian mucho en un pico de tráfico.
Ten en cuenta
Nunca caches respuestas personalizadas sin incluir al usuario en la clave: podrías mostrarle a una persona los datos de otra. Añade X-Cache-Status a la respuesta para depurar, y planifica cómo invalidar la caché cuando cambien los datos.
# En el bloque http: dónde y cuánto guardar
proxy_cache_path /var/cache/nginx keys_zone=catalogo:10m max_size=1g inactive=1h;
server {
location /api/catalogo {
proxy_pass http://app;
proxy_cache catalogo;
proxy_cache_valid 200 5m; # respuestas 200, 5 minutos
proxy_cache_use_stale error timeout updating; # si la app falla, sirve lo guardado
add_header X-Cache-Status $upstream_cache_status; # HIT, MISS, EXPIRED…
}
}Límite de peticiones (limit_req)
Frena a quien hace demasiadas peticiones por segundo, por IP o por otra clave.
- Algoritmo:
- Cubo que gotea
- Error:
- 503 o 429
limit_req_zone define una zona con una clave (normalmente la IP del cliente) y un ritmo máximo, como 5r/m (cinco por minuto) o 10r/s. limit_req lo aplica en un location. Funciona como un cubo que gotea: las peticiones salen al ritmo fijado, y las que llegan de más esperan en una cola de tamaño burst o se rechazan.
Con nodelay, las peticiones que caben en el burst se atienden al momento en lugar de esperar. Las que no caben reciben un error, por defecto un 503; limit_req_status 429 lo cambia por el más correcto «429 Too Many Requests».
Úsalo cuando
Para proteger el inicio de sesión de ataques de fuerza bruta, las rutas caras de la API y los formularios públicos frente a bots.
Ten en cuenta
Si delante de Nginx hay una CDN u otro proxy, todas las peticiones llegan desde sus IP y todos tus usuarios compartirán límite. Usa el módulo realip para tomar la IP real de X-Forwarded-For, solo desde las IP de confianza. Y no sustituye a una protección contra ataques DDoS a gran escala.
# 5 peticiones por minuto por IP, en una zona de 10 MB de memoria
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429; # por defecto respondería 503
proxy_pass http://app;
}
}Interactivo
Algoritmos de balanceo
Todos combinan con la detección pasiva de caídas (max_fails y fail_timeout). Los servidores backup, en cambio, no se pueden usar con ip_hash.
| Característica | Round robin | Con pesos | least_conn |
|---|---|---|---|
| Cómo elige | Por turnos | Por turnos, más veces a los de más peso | El de menos conexiones activas |
| Configuración | Ninguna: es el valor por defecto | weight= en cada server | least_conn; |
| Mira la carga | No | No, solo la capacidad que declaras | Sí |
| Mismo cliente, mismo servidor | No | No | No |
| Ideal para | Servidores iguales y peticiones parecidas | Máquinas de distinta potencia | Peticiones de duración muy variable, WebSockets |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Quieres que mitienda.com sirva la web, /api/ vaya a tu API en otra máquina y /admin/ a un panel interno, sin que los usuarios sepan que son tres servicios.
Para recordar
- Un proxy directo representa a los clientes; un proxy inverso, a tus servidores, y es donde se centralizan TLS, caché, límites y balanceo.
- Con
proxy_pass, la barra final cambia la ruta que recibe la app; y sin las cabecerasHostyX-Forwarded-*, tu app no sabe quién es el cliente. - Round robin reparte por turnos, los pesos ajustan por capacidad,
least_connmira la carga real eip_hashfija cada cliente a un servidor. - El Nginx de código abierto detecta caídas de forma pasiva con
max_failsyfail_timeout; las comprobaciones activas son de NGINX Plus. proxy_cachealivia a la app con respuestas comunes ylimit_reqfrena los abusos; ojo con la IP real si hay otra capa delante.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Con
location /api/ { proxy_pass http://app:3000/; }, llega una petición a/api/usuarios. ¿Qué ruta recibe la app?2.¿Por qué se suele añadir
proxy_set_header Host $host;?3.Un servidor tiene
max_fails=3 fail_timeout=30sy falla tres veces en diez segundos. ¿Qué hace Nginx?4.Tus usuarios llegan a través de una CDN y has puesto
limit_reqpor$binary_remote_addr. Ahora usuarios legítimos reciben errores 429. ¿Por qué?5.¿Cuándo es
least_connclaramente mejor que round robin?