Resumen teórico
Imagina la cafetería de la escuela con una sola caja y una fila enorme. La fila avanza despacio, los niños se enojan y, si el cajero se enferma y se va, nadie come. Ahora imagina que la cafetería abre cuatro cajas y una maestra se para en la puerta diciendo "tú vas a la caja 1, tú vas a la caja 2...". La fila avanza cuatro veces más rápido y, si un cajero se enferma, la maestra simplemente deja de mandar niños allí. Esa maestra es un balanceador de carga (load balancer).
En internet es igual. Un sitio web o una aplicación es atendido por un programa en una computadora llamada servidoruna computadora que espera pedidos de otras computadoras y les responde. Un servidor solo puede hacer cierta cantidad de trabajo. Cuando millones de personas piden páginas al mismo tiempo, usamos muchos servidores, y algo tiene que decidir qué servidor responde a cada persona. Ese algo es el balanceador de carga. Te lo encuentras cada vez que abres un sitio grande, una app de video o un juego en línea, aunque nunca lo veas.
En este resumen aprenderás, paso a paso: por qué usamos muchos servidores en lugar de uno gigante, qué es un balanceador de carga y en qué se diferencia de un proxy inverso y de un API gateway, en qué "piso" de la red puede trabajar, cómo elige un servidor (los algoritmos), cómo se da cuenta de que un servidor murió, cómo mantiene a un usuario en el mismo servidor cuando hace falta, cómo maneja el candado secreto de HTTPS, cómo evitar que el propio balanceador tumbe todo y cómo configurarlo en NGINX y en Caddy. Al final podrás responder el quiz de esta área.
El panorama general y las palabras que usamos
Empecemos con algunas palabras que verás todo el tiempo. Un cliente es quien pide algo (tu navegador, la app del celular). Una petición (request) es una pregunta que envía el cliente, como "dame la página de inicio". Un servidor responde. Cuando ponemos muchos servidores iguales detrás de una sola puerta, cada uno se llama back end (o instancia, una copia del mismo programa). La puerta es el balanceador de carga.
┌──────────┐ ┌─> back end 1
clientes ─> │ balancea-│ ─────┼─> back end 2
│ dor │ └─> back end 3
└──────────┘
una dirección muchas copias iguales
que ven los de la aplicación
clientesLos clientes solo conocen la dirección del balanceador. Nunca saben cuántos back ends existen. Eso es muy útil: podemos agregar o quitar back ends y nadie de afuera lo nota.
Los dos trabajos de un balanceador de carga
- Repartir el trabajo: para que ningún servidor quede aplastado mientras otros están parados. Esto da más capacidad (más peticiones por segundo).
- Sobrevivir a las fallas: cuando un servidor muere, dejar de mandarle peticiones. Los usuarios siguen siendo atendidos por los demás. Esto da disponibilidadla fracción del tiempo en que un servicio funciona y se puede usar.
Escalado horizontal frente a vertical
Tu tienda recibe más clientes de los que puede atender. Hay dos cosas que puedes hacer. Puedes comprar una máquina gigante y rapidísima para reemplazar la vieja. O puedes comprar más máquinas normales y usarlas juntas. Estos dos caminos tienen nombre.
| Aspecto | Escalado vertical (scale up) | Escalado horizontal (scale out) |
|---|---|---|
| Qué haces | Hacer más grande una máquina: más núcleos, más memoria, discos más rápidos | Agregar más instancias del mismo servicio |
| Imagen | Cambiar al cajero por un supercajero | Abrir más cajas |
| ¿Hay que cambiar la aplicación? | Normalmente no, es la forma más simple | Sí: debe funcionar con varias copias a la vez |
| ¿Necesita balanceador de carga? | No, hay una sola máquina | Sí, algo debe repartir las peticiones |
| Límites | Existe un tamaño máximo, el precio sube rápido cerca de él, cambiar el tamaño muchas veces exige detener el servicio, y sigue siendo UNA máquina (un punto único de falla) | No hay un techo de una sola máquina, pero las partes compartidas (como una única base de datos) pueden convertirse en el nuevo límite |
Por qué los servicios stateless escalan fácil en horizontal
Un servicio es statelessno guarda, entre una petición y la siguiente, nada de lo que la siguiente dependa cuando cualquier copia puede responder cualquier petición. Los datos que deben sobrevivir (sesiones, carritos, pedidos) viven en un almacenamiento compartido, como una base de datos. Entonces el balanceador puede mandar cada petición a cualquier lugar, y las copias pueden aparecer o desaparecer libremente.
Dimensionamiento: N+1
Si una instancia atiende 400 peticiones por segundo y el momento de mayor movimiento trae 1500, necesitas ceil(1500 / 400) = ceil(3,75) = 4 instancias solo para atender el pico (siempre redondeamos hacia arriba). Llama a ese número N. Para seguir atendiendo todo el pico cuando muera una instancia, agregas una de reserva: N + 1 = 5.
La reserva sale más barata cuando las máquinas son pequeñas. Para sobrevivir a una falla con 2 máquinas grandes, cada una capaz de cargar el 100% del pico, pagas el 200% del pico. Con 8 máquinas pequeñas, cada una cargando 1/7 del pico, pagas 8/7, cerca del 114%. Una reserva entre 8 es una fracción menor que una reserva entre 2.
- 2 máquinas grandes
- 200 % del pico
- 8 máquinas pequeñas
- 114.3 % del pico
Elasticidad y sus trampas
Con el escalado horizontal puedes encender instancias en la hora pico y apagarlas en las horas tranquilas, una por una, mientras las demás siguen trabajando. Esto se llama elasticidadcapacidad que sigue a la demanda hacia arriba y hacia abajo. Una máquina escalada en vertical queda dimensionada para el pico todo el día, o tiene que detenerse en cada cambio de tamaño.
↑ Volver al índiceProxy inverso, balanceador de carga y API gateway
Un proxyun programa intermediario que pasa pedidos y respuestas entre dos lados se coloca entre dos partes. La pregunta es: ¿para quién trabaja?
| Tipo | Dónde está | A quién representa |
|---|---|---|
| Proxy directo (forward proxy) | Junto a los clientes (por ejemplo, una red de empresa donde todos salen a internet a través de él) | A los clientes. El servidor no ve quién está detrás |
| Proxy inverso (reverse proxy) | Junto a los servidores | A los servidores. El cliente habla solo con él y no necesita saber que existe |
Un proxy inverso es un lugar natural para hacer trabajos que toda petición necesita: TLS (el candado de HTTPS), caché, compresión, esconder la dirección real del origen y retener a los clientes lentos para que la aplicación quede libre para la siguiente petición. Por eso un proxy inverso es útil incluso con UN solo servidor detrás. Lo que no puede dar es redundancia, que requiere más de un servidor.
Un balanceador de carga de capa 7 es un proxy inverso que además decide cuál de varias copias equivalentes atiende cada petición. Por eso NGINX y Caddy cumplen los dos papeles con la misma configuración.
Un API gateway es un proxy inverso especializado en APIs. En un solo punto aplica reglas comunes a todas las rutas: quién puede llamar (autenticación), con qué frecuencia (rate limiting), a qué servicio va cada ruta y cómo se adaptan las peticiones o respuestas. La lógica de negocio queda en los servicios.
┌─> réplica 1 de pedidos
/orders │ (balanceador de pedidos)
clientes ─> API ───────┼─> réplica 2 de pedidos
gateway │
/users ├─> réplica 1 de usuarios
│ (balanceador de usuarios)
└─> réplica 2 de usuariosQué cambia cuando hay un proxy en el medio
El proxy mantiene dos conexiones separadas: una con el cliente, otra con el back end. Esto trae tres consecuencias prácticas.
- La aplicación ve la dirección del proxy, no la del cliente. El proxy pasa la dirección del cliente en una cabecera llamada
X-Forwarded-For. Es una lista que crece hacia la derecha: cada proxy agrega la dirección de quien le entregó la petición. Con saltos honestos,X-Forwarded-For: 203.0.113.7, 10.0.0.5, 10.0.0.9significa que el cliente es203.0.113.7(el primero), luego vino el proxy10.0.0.5, y10.0.0.9es otro salto. El último proxy no aparece en la lista, porque es el par de la conexión que ve la aplicación. - Nunca confíes ciegamente en esa cabecera. Cualquier cliente puede enviar
X-Forwarded-Forcon un valor inventado, y los proxies agregan al FINAL. Así que la entrada más a la izquierda es la que controla el cliente. Para límites de tasa o control de acceso, lee de derecha a izquierda, salta las direcciones de tus propios proxies de confianza y quédate con la primera que sobre. - Las cabeceras hop-by-hop se eliminan. Algunas cabeceras (
Connection,Keep-Alive,Transfer-Encoding,Upgrade) valen solo para una conexión entre dos vecinos, a diferencia de las cabeceras end-to-end, que viajan del cliente al origen. Como el proxy tiene dos conexiones independientes, descarta las hop-by-hop que recibió y crea las que sirven para la otra conexión.
Capa 4 frente a capa 7
Describimos las redes como un edificio con pisos, llamados capas. El modelo OSIuna forma estándar de describir las redes en siete capas, desde los cables hasta las aplicaciones tiene siete: física, enlace de datos, red, transporte, sesión, presentación y aplicación. Aquí solo importan dos. La capa 4 es el piso del transporte, donde viven TCP y UDP, con direcciones y puertos. La capa 7 es el piso de la aplicación, donde vive HTTP, con URLs, cabeceras y cookies. "L4" y "L7" dicen hasta qué piso lee el tráfico un balanceador antes de decidir.
| Aspecto | Capa 4 | Capa 7 |
|---|---|---|
| Ve | Direcciones y puertos de origen y destino | El mensaje HTTP completo: ruta, cabecera Host, cookies, otras cabeceras |
| Unidad de trabajo | La conexión: una decisión y luego solo reenvía bytes | La petición: cada petición se lee, se interpreta y se envía de nuevo |
| Conexiones | Reenvía los bytes de una conexión | Termina la conexión TCP del cliente y abre (o reutiliza) otra hacia el back end, así que hay dos conexiones |
| Protocolos | Cualquiera (no le importa lo que hay dentro) | Los que entiende, normalmente HTTP |
| Costo por petición | Menor | Mayor (más CPU y memoria) |
| Reglas de enrutamiento | Pobres: solo dirección y puerto | Ricas: por ruta, host, cabecera, cookie |
La regla para elegir es simple: mira qué necesita tu regla de enrutamiento. Separar servicios por direcciones o puertos distintos funciona en la capa 4. Si la diferencia está en la ruta de la URL (/api/* aquí, /static/* allá, mismo host y mismo puerto), en una cabecera o en una cookie, esa información solo aparece después de leer el HTTP, así que necesitas al menos la capa 7.
La trampa de la conexión larga
Un balanceador de capa 4 elige una vez por conexión. Si los clientes mantienen una conexión abierta y mandan miles de peticiones por ella (keep-alive de HTTP/1.1), todas van al mismo back end, y las conexiones con volúmenes distintos producen carga despareja. Lo mismo ocurre, con más fuerza, con HTTP/2 y gRPC, que multiplexan muchas llamadas en una sola conexión. Un cliente muy activo puede sobrecargar un back end mientras otros dos están parados. La solución es balancear en la unidad correcta, la petición o el stream: un proxy de capa 7, o un cliente que abre conexiones a varios back ends y se va alternando entre ellas.
capa 4: [conn A: req req req req req ...] -> back end 1
[conn B: req ................] -> back end 2
capa 7: req -> back end 1
req -> back end 2
req -> back end 3 (una elección por petición)Con HTTPS, un balanceador de capa 4 puede trabajar en passthrough: reenvía los bytes cifrados y el túnel secreto va del cliente al back end. Todavía ve lo que está fuera del cifrado (direcciones, puertos, cuándo se abren y cierran las conexiones), pero la ruta, las cabeceras y las cookies le son invisibles.
↑ Volver al índiceAlgoritmos de balanceo
Un algoritmouna receta fija de pasos para resolver un problema de balanceo responde una pregunta: ¿qué back end recibe la siguiente petición? Conozcamos los más comunes.
Round robin
Los back ends reciben peticiones por turnos, uno después del otro, y cuando la lista termina vuelve a empezar, como repartir cartas alrededor de una mesa. Ignora la carga y la velocidad de cada servidor. Con los back ends A, B, C empezando por A, la petición número k va a la posición (k − 1) mod 3 de la lista, contada desde cero. Para k = 7, (7 − 1) mod 3 = 0, así que la 7.ª petición va a A.
Round robin ponderado
Cada back end tiene un peso, un número escrito en la configuración. La parte de un servidor es su peso dividido entre la suma de todos los pesos. Con pesos 3, 2 y 1, la suma es 6, así que las partes son 3/6, 2/6 y 1/6. De 600 peticiones: 300, 200 y 100.
- servidor de peso 3
- 300 peticiones
- servidor de peso 2
- 200 peticiones
- servidor de peso 1
- 100 peticiones
Los pesos son estáticos: son una suposición hecha de antemano y no cambian solos cuando un servidor se vuelve más lento.
Menos conexiones (least connections)
El balanceador mantiene un contador para cada back end: suma 1 cuando envía una petición y resta 1 cuando esta termina. La siguiente petición va al back end con menos peticiones en curso. Es una estimación simple de qué tan ocupado está cada servidor. Le gana claramente al round robin cuando las peticiones tienen duraciones muy distintas, porque el round robin iguala el número de peticiones enviadas, no el trabajo.
El least connections ponderado divide las conexiones activas entre el peso y elige el menor resultado. Ejemplo: A tiene 12 conexiones y peso 1, B tiene 20 y peso 4, C tiene 9 y peso 1. Las razones son 12, 5 y 9, así que se elige B: un servidor de peso 4 aguanta cuatro veces más.
Un servidor lento, dos algoritmos
Toma tres back ends: dos responden en 10 ms y uno en 40 ms, y un número fijo de clientes manda la siguiente petición apenas termina la anterior. Con round robin, cada back end recibe un tercio de las peticiones. Pero el lento necesita cuatro veces más tiempo por petición, así que las peticiones se acumulan en él. Con least connections, cada back end mantiene el mismo número de peticiones en curso. Un back end que mantiene cierto número de peticiones las termina a una tasa de (en curso) dividido entre (tiempo por petición), así que las tasas quedan en la proporción 1/40 : 1/10 : 1/10 = 1 : 4 : 4. Eso suma 9 partes, así que el back end lento recibe 1/9 de las peticiones, cerca del 11%, y cada rápido 4/9, cerca del 44%.
- round robin
- 33.3 %
- least connections
- 11.1 %
Aleatorio y el poder de dos opciones
El aleatorio (random) elige un servidor al azar para cada petición. Con pocas peticiones el reparto parece despareja, pero con muchas cada uno de N servidores iguales recibe cerca de 1/N. No necesita contador ni posición, así que varios balanceadores pueden aplicarlo sin hablar entre sí. El poder de dos opciones (random two en NGINX, random_choose 2 en Caddy) sortea dos servidores y se queda con el menos cargado de los dos. Es mucho más equilibrado que el aleatorio simple y cuesta solo una comparación. También reduce el "efecto manada", en que varios balanceadores mandan todo al único servidor que parece el más libre.
Hash: IP hash y URL hash
Un hashuna función que convierte cualquier entrada en un número, siempre el mismo número para la misma entrada de una clave elige el servidor. En el IP hash la clave es la dirección del cliente, así que el mismo cliente vuelve al mismo servidor sin ninguna tabla (esto vale mientras el conjunto de servidores no cambie). En el URL hash la clave es la dirección pedida, así que la misma URL siempre va al mismo servidor. Es lo mejor para cachés: cada nodo guarda respuestas de las URLs que atiende, y la tasa de aciertos sube.
Dos trampas más de least connections
- Contadores no compartidos. Si dos procesos de balanceador mantienen contadores separados, cada uno decide con la mitad del cuadro y las decisiones pueden acumularse en el mismo back end. El estado debe compartirse (una zona de memoria compartida), o se usa una política que tolere información parcial.
- El servidor nuevo y vacío. Un servidor recién llegado tiene cero conexiones, así que en un pool ocupado atrae casi todo el tráfico nuevo de golpe, cuando todavía está frío (cachés vacíos, ninguna conexión abierta). Un slow start aumenta su parte poco a poco.
| Algoritmo | Decide por | Mejor cuando | Punto débil |
|---|---|---|---|
| Round robin | Un contador (turnos) | Servidores iguales, peticiones parecidas | Ciego a la carga y a la velocidad |
| Round robin ponderado | Contador y pesos | Servidores de distinto tamaño | Los pesos son suposiciones fijas |
| Least connections | Peticiones en curso | Peticiones con duraciones muy distintas | Necesita contadores compartidos, los servidores nuevos se inundan |
| Aleatorio / dos opciones | Azar (y una comparación) | Varios balanceadores sin coordinación | Desigualdad de corto plazo (aleatorio simple) |
| IP hash / URL hash | Un hash de una clave | Afinidad, cachés | Desparejo cuando las claves se concentran |
Sesiones fijas (sticky sessions)
A veces el usuario debe volver al mismo back end. La afinidad de sesión (session affinity, también llamada sticky session) es la regla por la cual el balanceador manda todas las peticiones de un cliente al mismo back end, en lugar de elegir de nuevo cada vez. El balanceador reconoce al cliente por una cookie o por la dirección IP.
¿Por qué alguien la necesitaría? Por el estado guardado dentro de una instancia. Si tu inicio de sesión o tu carrito de compras vive solo en la memoria del proceso que te atendió, una petición enviada a otra instancia no encuentra nada. Atar al cliente a la instancia rodea el problema, pero no lo resuelve.
Dos formas de reconocer al cliente
| Aspecto | Por cookie | Por dirección IP |
|---|---|---|
| Cómo | El balanceador elige un back end en la primera petición y agrega a la respuesta una cookie que identifica la elección. El navegador devuelve la cookie cada vez, y el balanceador repite el destino. El estado vive en el cliente, no en una tabla del balanceador | Un hash de la dirección del cliente elige el back end (ip_hash en NGINX, client_ip_hash en Caddy) |
| ¿Necesita capa 7? | Sí (lee y escribe cabeceras HTTP) | No |
| Punto débil | La cookie es un dato controlado por el cliente | Supone que una dirección = un usuario, lo que falla en ambos sentidos: el mismo usuario cambia de dirección al cambiar de red, y muchos usuarios detrás de un NAT parecen uno solo |
Lo que la afinidad no da
- No protege el estado. Si la sesión existe solo en la memoria de una instancia y esa instancia se cae, la sesión muere. El failover solo asegura que el usuario llegue a un servidor que no lo conoce.
- Perjudica el autoscaling. Las instancias sobrecargadas conservan todas las sesiones que ya tenían, y las instancias nuevas reciben solo clientes que todavía no están atados. Cuanto más largas las sesiones, más lenta la redistribución. Los servicios stateless se reequilibran en la siguiente petición.
- Puede desbalancear la carga cuando se comparten direcciones (ver IP hash).
La cura de verdad: quitar el estado
La afinidad solo hace falta mientras el estado de la sesión vive dentro de una instancia. Saca el estado de allí, ya sea a un almacenamiento compartido que todas las instancias consultan, o a un token firmado que el cliente envía en cada petición, y las instancias pasan a ser stateless. El balanceador puede elegir cualquiera, y las instancias pueden caerse o agregarse sin perder sesiones.
↑ Volver al índiceConsistent hashing
Usar el hash de una clave para elegir un servidor tiene un gran problema. La receta simple es hash(clave) mod N, donde N es el número de servidores (mod es el resto de la división). Si N cambia, la respuesta para casi todas las claves cambia.
Compruébalo con números. Claves 0, 1, 2, ... con clave mod N. Al pasar de N = 4 a N = 5, una clave conserva su servidor solo si el resto antiguo y el nuevo son iguales. El patrón se repite cada 20 claves, y los restos coinciden solo para las claves 0, 1, 2 y 3, es decir, 4 de 20. Así que el 80% de las claves cambia de servidor. Para un caché, eso significa perder casi todo el contenido caliente de una sola vez.
El consistent hashing se inventó para que un cambio en el conjunto de servidores mueva solo una pequeña fracción de las claves, cerca de 1/N.
0
S1 * * k1
* *
k2 * * S2
* *
S3 * * k3
una clave pertenece al primer servidor
que se encuentra avanzando en sentido
horario desde su posiciónLa misma función de hash da una posición en el círculo a cada servidor y a cada clave. En el diseño clásico, cada clave pertenece al primer servidor que se encuentra avanzando en sentido horario desde la posición de la clave. La regla depende solo de posiciones, así que cuando un servidor entra o sale, solo las claves de un tramo del anillo cambian de dueño.
Ejemplo: 4 servidores iguales se reparten las claves por igual, y entra un 5.º servidor igual. Las únicas claves que se mueven son las que el nuevo servidor ahora posee: 1/5. Cada servidor antiguo pasa de 1/4 a 1/5, cediendo 1/20 del total, y 4 × 1/20 = 1/5.
- clave mod N
- 80 %
- consistent hashing
- 20 %
Nodos virtuales
Si cada servidor tiene solo UNA posición en el anillo, los tramos (arcos) pueden ser de tamaños muy distintos, y cuando un servidor sale, su vecino en sentido horario hereda toda su parte. Ese vecino pasa a cargar dos partes y, si también falla, pasa tres al siguiente: una cascada de fallas.
La solución son los nodos virtuales (virtual nodes): cada servidor aparece decenas o cientos de veces en el anillo, en muchas posiciones. Así los arcos se vuelven uniformes, un servidor más grande puede recibir más posiciones (más parte), y cuando un servidor sale, sus claves se reparten entre muchos otros en lugar de uno solo.
↑ Volver al índiceHealth checks y failover
Un balanceador que sigue mandando peticiones a un servidor muerto es peor que ningún balanceador. Por eso debe averiguar quién está vivo. Esto es un health check (verificación de salud). Cuando descubre un problema y pasa el trabajo a los demás, eso es failover: la transferencia automática del trabajo de un componente que falló a uno sano. El failover necesita dos cosas: redundancia (otro componente puede tomar el relevo) y detección (algo nota la falla).
| Aspecto | Verificación activa | Verificación pasiva |
|---|---|---|
| Qué hace | El balanceador envía sus propias peticiones de prueba (probes) a intervalos fijos, por ejemplo a /health | El balanceador no envía probes. Observa las peticiones reales y cuenta fallas (errores de conexión, timeouts) |
| ¿Necesita tráfico? | No, funciona incluso sin clientes | Sí, necesita peticiones reales para aprender |
| Costo de descubrir un servidor muerto | Solo tráfico de prueba | Las primeras peticiones reales enviadas al servidor muerto fallan o esperan un timeout, así que usuarios reales pagan |
| Ejemplo | Caddy: health_uri, health_interval | NGINX: max_fails dentro de fail_timeout |
Las dos se complementan. La pasiva es inmediata, pero necesita tráfico. La activa funciona sin tráfico y es la única que puede traer un servidor de vuelta, porque nadie manda peticiones reales a un servidor que está fuera.
Un back end marcado como no saludable (unhealthy) no se borra. El balanceador solo deja de elegirlo para nuevas peticiones y reparte el tráfico entre los demás. Cuando la verificación vuelve a pasar, regresa a la rotación sin ningún paso manual.
¿En qué capa verificas?
Una verificación solo detecta problemas en la capa donde trabaja. Abrir una conexión TCP prueba que algún proceso está escuchando en el puerto, nada más. Si la aplicación responde estado HTTP 500 a todo, una verificación TCP dice "sano". Para notar errores de la aplicación, el probe debe hacer una petición HTTP y comprobar el código de estado (y el cuerpo cuando haga falta).
¿Cuánto tarda en retirarse un servidor muerto?
En el peor caso, tiempo de detección = intervalo entre probes × número de fallas exigidas + timeout del probe. Si los probes salen cada 2 s, se exigen tres fallas consecutivas y el timeout es mínimo, un servidor que muere justo después de un probe bueno se retira 2 × 3 = 6 segundos después. Durante esos 6 segundos todavía recibe peticiones. Los intervalos cortos detectan más rápido, pero aumentan las falsas alarmas: ese es el equilibrio que hay que buscar.
Los umbrales también dan a la decisión una histéresis (un retraso que evita cambiar de decisión con demasiada facilidad). Si un servidor que alterna entre funcionar y fallar pudiera entrar y salir del pool en cada probe, cada entrada costaría peticiones de clientes. Exigir varios éxitos consecutivos antes de volver cambia un regreso más lento por estabilidad.
Reintentar o no reintentar
Cuando un intento falla, ¿puede el balanceador enviar la misma petición a otro back end? Depende de lo que vio el primer back end.
| Qué pasó | ¿El back end vio la petición? | ¿Reintentar en otro back end? |
|---|---|---|
| Conexión rechazada o timeout de conexión | No | Siempre es seguro, cualquier método |
| La petición se envió y luego la conexión se cayó o no llegó respuesta | Quizá, y quizá se ejecutó | Solo para peticiones idempotentesuna operación que da el mismo resultado si se ejecuta dos veces que si se ejecuta una, como GET, HEAD y OPTIONS |
Liveness, readiness y verificaciones superficiales
- Liveness pregunta: ¿el proceso está atascado y necesita reiniciarse? Readiness pregunta: ¿puede recibir tráfico ahora? Una instancia que todavía está cargando cachés y abriendo conexiones a la base de datos está viva, pero no lista. El balanceador debe saltársela, y nadie debe reiniciarla.
- Una verificación profunda (que prueba dependencias compartidas, como ejecutar una consulta en la base de datos compartida) hace que todos los back ends fallen juntos cuando falla la dependencia. Quitarlos a todos no ayuda, y además tumba rutas que seguirían funcionando (como páginas en caché). La verificación que usa el balanceador debe ser superficial: ¿ESTA instancia puede atender? Vigila las dependencias por otros medios.
Retirar un servidor sin perder peticiones
El connection draining lo hace en dos pasos. Primero, el balanceador deja de elegir la instancia para nuevas peticiones. Luego espera a que terminen las peticiones en curso, hasta un plazo límite. Solo entonces se detiene el proceso. Los reintentos no reemplazan al draining, porque las peticiones no idempotentes no se pueden repetir con seguridad.
↑ Volver al índiceAlta disponibilidad y puntos únicos de falla
Un punto único de falla (SPOF, single point of failure) es cualquier parte del sistema que no tiene sustituto: si se detiene, todo se detiene, por muy redundantes que sean las demás partes. La pregunta útil para cada componente es: ¿qué pasa si solo falla este? Alta disponibilidad es el trabajo de encontrar los SPOF y duplicarlos.
Aquí está la sorpresa: cinco servidores de aplicación detrás de UN balanceador de carga. El balanceador elimina el SPOF de la capa de aplicación, pero si está solo se convierte en un nuevo SPOF. Por eso los balanceadores se despliegan en pares o grupos, con una forma de hacer que el tráfico llegue al sano, como una dirección IP virtualuna dirección que no está atada a una máquina y puede mudarse a otra que cambia de dueño, o el DNS. La redundancia en una capa no compensa su ausencia en otra.
Activo-pasivo y activo-activo
| Aspecto | Activo-pasivo | Activo-activo |
|---|---|---|
| Quién atiende | Un nodo atiende todo el tráfico, el otro espera en standby | Los dos atienden al mismo tiempo |
| Cuando uno falla | El standby toma el relevo | El otro absorbe toda la carga |
| Recursos | El standby está ocioso | Mejor aprovechamiento de los recursos |
| El inconveniente | El cambio toma algo de tiempo | Cada nodo necesita margen suficiente para cargar todo solo |
La aritmética de la disponibilidad
En paralelo (componentes redundantes, el conjunto funciona mientras al menos uno esté en pie): el conjunto solo falla si TODOS fallan juntos. Con fallas independientes, la indisponibilidad del conjunto es el producto de las indisponibilidades. Dos balanceadores de 99% cada uno: indisponibilidad 0,01 × 0,01 = 0,0001, así que disponibilidad 1 − 0,0001 = 99,99%.
En serie (ambas partes son necesarias): la disponibilidad del conjunto es el producto de las disponibilidades. El par de 99,99% delante de UNA base de datos de 99,9%: 0,9999 × 0,999 = 0,9989001, cerca de 99,89%. Nunca supera a la parte más débil. Agregar aún más balanceadores casi no ayuda. La ganancia está en hacer redundante la base de datos.
- un balanceador
- 99 %
- par de balanceadores
- 99.99 %
- par + una base de datos
- 99.89 %
Split brain
Dos balanceadores en un par activo-pasivo se vigilan con un heartbeat (un mensaje regular de "estoy vivo"). Supón que el enlace entre ellos se rompe mientras ambos están sanos. Ninguno puede distinguir la muerte de su par de una falla del enlace, pues en ambos casos el heartbeat se detiene. Los dos pueden decidir "yo debo ser el activo" y pelear por el mismo recurso, como la dirección IP virtual. Esto es el split brain (cerebro dividido). Para evitarlo necesitas información de fuera del enlace: un quórum con un tercer participante (un testigo) que decida quién puede seguir activo, o fencing, que garantiza que el otro nodo realmente fue apagado.
↑ Volver al índiceTerminación TLS
TLSla tecnología que cifra una conexión; es el candado detrás de HTTPS crea un túnel secreto entre dos extremos. Quien es el extremo del túnel guarda el certificado (la cédula de identidad del sitio) y la clave privada (el secreto que prueba que esa identidad es suya). Terminar el TLS es ser ese extremo: descifras los datos. ¿Dónde debe terminar el túnel cuando hay un balanceador? Hay tres formas.
| Modo | Qué ocurre | ¿Lee el HTTP (reglas de capa 7)? | ¿Tráfico interno cifrado? | ¿Dónde están certificado y clave? |
|---|---|---|---|---|
| Passthrough | El balanceador (capa 4) no descifra. El túnel va del cliente al back end | No | Sí, de extremo a extremo | En cada back end (distribuir y renovar en cada uno) |
| Terminación (simple) | El balanceador descifra y luego habla HTTP plano con los back ends | Sí | No, el tramo interno va en claro | Solo en el balanceador |
| Recifrado | El balanceador descifra, inspecciona y luego abre una nueva conexión TLS hacia el back end | Sí | Sí | En el balanceador y en los back ends |
passthrough: cliente ════════════════════> back end
terminación: cliente ═══════> balanceador ───> back end
(HTTPS) (HTTP plano)
recifrado: cliente ═══════> balanceador ═══> back end
(HTTPS) (nuevo HTTPS)El principal beneficio de terminar en el balanceador es operativo: el TLS (certificados, claves, versiones, cifrados) se configura en un solo lugar. Los back ends se vuelven más simples y gastan su CPU en la aplicación. El precio es que el tramo interno deja de estar cifrado, a menos que recifres.
Consecuencias que debes conocer
- El esquema se pierde. Después de la terminación, la aplicación ve solo la conexión interna, que es HTTP. Si los usuarios navegan por
https://pero la aplicación arma redirecciones que empiezan conhttp://, esa es la causa. El proxy informa el esquema original en la cabeceraX-Forwarded-Proto, y la aplicación debe usarla. Como cualquier cliente puede enviar esa cabecera, acéptala solo de proxies de confianza. - Cifrar no es autenticar. Si NGINX abre
proxy_pass https://backend;sin ninguna directivaproxy_ssl_*, no verifica el certificado del back end. Alguien dentro de la red que se haga pasar por el back end recibe tráfico que puede descifrar. Con recifrado debes activar la verificación explícitamente (proxy_ssl_verify on;) y dar la lista de autoridades certificadoras aceptadas.
Configuración de NGINX
A NGINX (aquí, la versión open source 1.30) se le dice qué hacer mediante un archivo de configuración. Para balancear haces dos cosas: declaras un grupo de servidores con nombre y apuntas una directiva de proxy a ese nombre.
upstream api {
server app1:3000;
server app2:3000;
server app3:3000;
}
server {
location /api/ {
proxy_pass http://api/;
proxy_set_header Host $host;
}
}upstream api { ... }declara un grupo de servidores y le da un nombre. El grupo solo no recibe tráfico.proxy_passdentro de unlocationlo convierte en un proxy inverso. Cuando el nombre en la URL es un grupo upstream, el destino se elige entre los servidores del grupo con el método de balanceo.- El método por defecto es el round robin ponderado, y cada servidor tiene peso 1 a menos que escribas
weight. Los otros métodos (least_conn,ip_hash,hash,random) solo se aplican cuando escribes la directiva dentro del bloque. Enip_hash, para IPv4 la clave son solo los tres primeros octetos de la dirección, así que192.168.1.10y192.168.1.200(misma red /24) van al mismo servidor, y para IPv6 es la dirección completa.
Parámetros del servidor
| Parámetro | Significado | Por defecto |
|---|---|---|
weight=3 | Proporción entre los servidores en la rotación | 1 |
max_fails=2 | Cuántos intentos fallidos, dentro de fail_timeout, hacen que el servidor se considere no disponible | 1 |
fail_timeout=15s | A la vez la ventana de conteo Y el tiempo que el servidor permanece no disponible | 10s |
backup | Un servidor de reserva: queda fuera de la rotación normal y solo se usa cuando los servidores primarios no están disponibles | no es backup |
Así, un grupo con server a weight=3;, server b; y server c backup; manda tráfico a a y b en la proporción 3 a 1, y c solo recibe peticiones cuando a y b no están disponibles.
Verificaciones pasivas y reintentos
NGINX open source tiene solo verificación pasiva (el health_check activo pertenece a la versión comercial). Juzga a un servidor por las peticiones reales. Cuando se alcanza max_fails dentro de fail_timeout, el servidor se salta durante fail_timeout, y después se usa una petición real de un cliente para probarlo de nuevo. Mientras el servidor sigue muerto, este ciclo se repite. Mientras tanto, proxy_next_upstream (por defecto error timeout) pasa la petición fallida a otro servidor. Las peticiones no idempotentes (POST, LOCK, PATCH) ya enviadas quedan fuera de este reintento, salvo que agregues non_idempotent.
Dos detalles que muerden
- La barra final de proxy_pass. Si
proxy_passtiene una parte de URI después del nombre del destino (incluso una sola/), NGINX reemplaza la parte de la petición que coincidió con el location por esa URI. Conlocation /api/ { proxy_pass http://backend/; }, una petición a/api/usersllega al back end como/users. Sin la barra (http://backend) la ruta llega como/api/users. - La cabecera Host. NGINX reescribe
Hostcon$proxy_host, el destino escrito enproxy_pass. Una aplicación que usaHostpara elegir el sitio o armar enlaces vería un nombre interno.proxy_set_header Host $host;pasa el nombre que el cliente realmente pidió.
Configuración de Caddy
Caddy (versión 2.11) hace el mismo trabajo con un archivo más corto, llamado Caddyfile. No hay un bloque aparte para declarar el grupo: la directiva reverse_proxy es proxy inverso y balanceador de carga a la vez. Listas las direcciones en la misma línea, y las opciones van en un bloque entre llaves.
example.com {
reverse_proxy app1:3000 app2:3000 app3:3000 {
lb_policy round_robin
health_uri /health
health_interval 5s
lb_try_duration 5s
}
}HTTPS automático
Cuando la dirección de un sitio es un nombre de dominio público (como example.com arriba), Caddy activa HTTPS sin ninguna configuración extra. Obtiene el certificado de una autoridad certificadora usando ACME, lo renueva automáticamente y redirige HTTP a HTTPS. Como balanceador de carga, eso es terminación TLS sin administrar archivos de certificados.
Valores por defecto distintos a los de NGINX
| Idea | NGINX (open source) | Caddy |
|---|---|---|
| Grupo de back ends | upstream nombre { server ...; } más proxy_pass | reverse_proxy a b c |
| Algoritmo por defecto | Round robin ponderado | random |
| Otras políticas | least_conn, ip_hash, hash, random two | lb_policy round_robin, least_conn, client_ip_hash, cookie, random_choose 2 |
| Verificación activa | No (solo en la versión comercial) | health_uri (apagada hasta que la escribas); health_interval por defecto 30s, health_timeout por defecto 5s |
| Verificación pasiva | Activada: max_fails=1, fail_timeout=10s | Apagada hasta que fail_duration sea mayor que 0 |
| Reintento tras un intento fallido | Activado: proxy_next_upstream error timeout | Apagado hasta definir lb_try_duration (o lb_retries); lb_try_interval es la pausa, 250 ms por defecto |
lb_try_duration 5s le dice a Caddy que siga eligiendo upstreams e intentando de nuevo hasta por 5 segundos. Eso oculta al cliente la pérdida de un back end, a cambio de algo de espera. En las verificaciones activas, health_uri hace que el propio proxy envíe peticiones a esa ruta en cada upstream, haya o no tráfico de clientes. Un upstream que falla sale de la selección y regresa cuando el probe vuelve a tener éxito.
Juntando todo
Ahora podemos leer la historia completa. Un servidor no basta, así que escalamos en horizontal (más instancias) y ponemos un balanceador de carga delante. El balanceador es un proxy inverso que decide qué copia igual atiende cada petición. En la capa 4 decide por conexión, en la capa 7 por petición, con reglas más ricas. Elige con un algoritmo, evita los servidores muertos gracias a los health checks, termina el TLS en un solo lugar y necesita duplicarse para no ser él mismo un punto único de falla.
| Si necesitas... | Mira... |
|---|---|
| Más capacidad | Escalado horizontal, dimensionamiento N+1, encontrar el cuello de botella real |
| Enrutar por ruta, host o cookie | Balanceador de capa 7 o API gateway |
| Balancear gRPC u otras conexiones largas | Proxy de capa 7 o un cliente que reparta las llamadas |
| Reparto igual con servidores desiguales | Pesos, o least connections |
| Un acierto de caché en el mismo nodo | URL hash, consistent hashing |
| Mantener a un usuario en un servidor | Afinidad por cookie (pero prefiere estado compartido o token firmado) |
| Sobrevivir a un servidor muerto | Health checks, reintentos solo para peticiones idempotentes, draining |
| Sobrevivir a un balanceador muerto | Un par con IP virtual, quórum o fencing |
| HTTPS en un solo lugar | Terminación TLS, con X-Forwarded-Proto |
Errores comunes que debes evitar
- Creerle ciegamente a
X-Forwarded-For: léelo desde la derecha, saltando los proxies de confianza. - Agregar instancias cuando la base de datos es el verdadero límite, o cuando los pools de conexiones no van a caber.
- Usar round robin simple con servidores de velocidades distintas.
- Usar IP hash cuando muchos usuarios comparten una dirección.
- Confiar en las sticky sessions para proteger el estado: usa almacenamiento compartido o tokens.
- Esperar que Caddy reintente y verifique la salud sin configuración, o que NGINX open source haga verificaciones activas.
- Reintentar un POST que quizá ya se ejecutó.
- Olvidar que un balanceador de carga solo es un SPOF, y que las partes dependientes en serie limitan la disponibilidad.
- Cifrar hasta el back end pero no verificar su certificado.