Módulo 6: Balanceo de carga y statelessness
6. Health checks
Descripción
Todo este módulo dio por hecho algo que el balanceador no sabe por magia: qué servidor está vivo. La lección 2 dijo que el balanceador "saca de rotación a los caídos", pero ¿cómo se entera de que uno cayó? La respuesta son los health checks (comprobaciones de salud): el balanceador sondea periódicamente a cada servidor del pool —le manda una petición ligera, típicamente un GET /healthz— y observa la respuesta. Si el servidor responde bien, sigue en rotación; si deja de responder, el balanceador lo saca del pool y reparte solo entre los sanos; cuando vuelve a responder, lo readmite. Sin health checks, el balanceador seguiría mandando tráfico a un servidor muerto, y una fracción de las peticiones fallaría —justo lo que el balanceador debía evitar—.
Vas a ver los dos tipos de health check (activo, el balanceador sondea; pasivo, observa los fallos reales), y sobre todo los umbrales que evitan el caos: cuántos fallos seguidos hacen falta para expulsar un servidor y cuántos éxitos para readmitirlo. Vas a medir por qué importan: con umbral 1, un servidor que parpadea (falla y responde alternadamente) entra y sale del pool sin parar —6 cambios de estado—, un fenómeno llamado flapping que desestabiliza el reparto; con umbral 3, el mismo servidor solo cambia de estado 2 veces, solo ante una caída sostenida. También verás la diferencia entre liveness y readiness, y el peligro del health check profundo que puede expulsar todo el pool a la vez.
Conexión con el módulo: los health checks son los ojos del balanceador de la lección 2 —el mecanismo concreto detrás de "sacar de rotación a los caídos"—. Y son la precondición operativa de la lección 7: para añadir una instancia, el balanceador espera a que pase el health check antes de mandarle tráfico; para quitarla con gracia, deja de mandarle antes de apagarla. La frontera: qué hace un servidor cuando su dependiente (la base de datos) está lento o caído —protegerse con un circuit breaker, reintentar con backoff— es la guía de resiliencia. Aquí vemos qué hace el balanceador cuando un servidor no responde: sacarlo del pool. Son dos mecanismos complementarios en capas distintas.
El capataz que revisa a su cuadrilla
Piénsalo así. Un capataz dirige una cuadrilla de obreros y reparte el trabajo entre ellos. Cada cierto rato pasa y le pregunta a cada uno: "¿todo bien? ¿puedes seguir?". Si un obrero responde "sí", sigue recibiendo trabajo. Si un obrero no contesta —se desmayó, se fue, está agotado—, el capataz deja de darle tareas y las reparte entre los demás, hasta que el obrero se recupere y vuelva a responder. Esa ronda de preguntas es el health check, y el capataz es el balanceador.
Pero un capataz sensato no expulsa a un obrero por una respuesta floja. Si preguntas "¿todo bien?" justo cuando el obrero está tomando agua y tarda en contestar, no lo mandas a casa —esperas a ver si falla varias veces seguidas—. Y al revés: cuando un obrero que estaba descansando vuelve, no le echas encima todo el trabajo por un solo "ya estoy"; esperas a confirmar que de verdad se recuperó. Ese "varias veces seguidas" es el umbral, y es lo que distingue a un capataz firme de uno histérico que expulsa y readmite a la gente cada dos minutos según el humor del momento. Un capataz histérico —umbral de 1— desestabiliza a toda la cuadrilla: nadie sabe quién está trabajando. Un capataz firme —umbral de 3— solo saca a quien de verdad no puede seguir.
La analogía trae la lección entera: el health check no es solo "¿responde?", es "¿responde de forma consistente?". Una sola respuesta —buena o mala— no debería cambiar el pool. Se necesita un patrón sostenido, y los umbrales son lo que codifica "sostenido". El resto de la lección los mide.
Activo contra pasivo: dos formas de vigilar la salud
Hay dos maneras de que el balanceador sepa de la salud de un servidor, y los sistemas reales suelen usar ambas:
- Health check activo. El balanceador sondea a cada servidor a intervalos regulares (cada pocos segundos) con una petición dedicada —un
GET /healthz— y mira la respuesta. Es proactivo: detecta un servidor enfermo antes de mandarle tráfico real, porque lo está probando aparte. El costo: tráfico de sondeo constante, y una ventana de detección (si sondeas cada 5 s, tardas hasta 5 s en notar una caída). - Health check pasivo. El balanceador observa las peticiones reales que ya está enrutando: si un servidor empieza a devolver errores o a no responder a las peticiones de los usuarios, lo saca. Es reactivo, sin tráfico extra, pero solo detecta el problema cuando ya afectó a peticiones reales —algunos usuarios ya vieron el error—.
Los dos se complementan: el activo atrapa al servidor que se cayó estando ocioso (antes de que le llegue tráfico), y el pasivo atrapa al que falla bajo carga real aunque responda bien al sondeo ligero. En esta lección modelamos el activo, que es el que tiene la lógica de umbrales más clara, pero recuerda que en producción ambos corren juntos.
Los umbrales, ejecutados: por qué evitan el flapping
Vamos al corazón de la lección. Un health check no cambia el pool con una sola respuesta: acumula. Expulsa a un servidor tras down_threshold fallos seguidos, y lo readmite tras up_threshold éxitos seguidos. Vamos a implementar ese rastreador y a correr dos escenarios: una caída limpia con recuperación, y un servidor que parpadea:
# health_checks.py — el LB saca de rotacion al server enfermo y lo devuelve al sanar
class HealthTracker:
"""Sigue un backend: lo saca tras `down` fallos seguidos, lo devuelve tras `up` exitos."""
def __init__(self, up_threshold, down_threshold):
self.up_th = up_threshold
self.down_th = down_threshold
self.in_pool = True
self.consec_fail = 0
self.consec_ok = 0
def observe(self, healthy):
if healthy:
self.consec_ok += 1
self.consec_fail = 0
if not self.in_pool and self.consec_ok >= self.up_th:
self.in_pool = True # readmitido
else:
self.consec_fail += 1
self.consec_ok = 0
if self.in_pool and self.consec_fail >= self.down_th:
self.in_pool = False # expulsado
# Escenario 1: app-1 cae, se recupera. Umbral down=3, up=2.
# Timeline de las sondas de salud a app-1 (True=responde 200, False=falla):
timeline = [True, True, False, False, False, False, False, True, True, True]
t = HealthTracker(up_threshold=2, down_threshold=3)
print("Escenario 1 — caida y recuperacion (down=3, up=2):")
print(" sonda | salud | en_pool")
for i, healthy in enumerate(timeline):
t.observe(healthy)
print(f" {i:>2} | {'OK ' if healthy else 'FAIL'} | {'SI' if t.in_pool else 'NO'}")
# Escenario 2: server que parpadea (flapping). Umbral=1 lo saca/mete sin parar;
# umbral=3 lo aguanta salvo caida sostenida.
flaky = [True, False, True, False, True, False, False, False, False, True]
for down_th in (1, 3):
t = HealthTracker(up_threshold=1, down_threshold=down_th)
trace = []
for healthy in flaky:
t.observe(healthy)
trace.append("O" if t.in_pool else "x") # O=en pool, x=fuera
flips = sum(trace[i] != trace[i-1] for i in range(1, len(trace)))
print(f"\nEscenario 2 — flapping, down_threshold={down_th}:")
print(f" estado del pool: {' '.join(trace)} (cambios de estado: {flips})")
Qué esperar. Con python health_checks.py:
Escenario 1 — caida y recuperacion (down=3, up=2):
sonda | salud | en_pool
0 | OK | SI
1 | OK | SI
2 | FAIL | SI
3 | FAIL | SI
4 | FAIL | NO
5 | FAIL | NO
6 | FAIL | NO
7 | OK | NO
8 | OK | SI
9 | OK | SI
Escenario 2 — flapping, down_threshold=1:
estado del pool: O x O x O x x x x O (cambios de estado: 6)
Escenario 2 — flapping, down_threshold=3:
estado del pool: O O O O O O O x x O (cambios de estado: 2)
Lee el escenario 1 primero. app-1 responde bien (sondas 0-1), luego empieza a fallar (sonda 2). Fíjate en que no sale del pool en la primera falla: sigue en SI durante las sondas 2 y 3, porque el umbral es 3 fallos seguidos. Recién en la sonda 4 —el tercer fallo consecutivo— se cumple el umbral y sale (NO). Cuando se recupera (sonda 7, primer OK), tampoco vuelve de inmediato: espera al segundo éxito seguido (sonda 8) para readmitirse. Los umbrales introducen esa histéresis deliberada: cuestan un poco de reacción tardía a cambio de no reaccionar a ruido pasajero.
Ahora el escenario 2, que es la razón de ser de los umbrales. El servidor parpadea: responde, falla, responde, falla… Con down_threshold=1 (expulsa al primer fallo), el pool lo mete y lo saca sin parar —O x O x O x x x x O, 6 cambios de estado—. Eso es flapping: un servidor que entra y sale de rotación constantemente, y cada cambio es disruptivo (reconfigura el reparto, mueve conexiones, ensucia las métricas). Con down_threshold=3, el mismo parpadeo produce solo 2 cambios: el servidor se mantiene en el pool durante los fallos aislados y solo sale cuando falla de verdad sostenidamente (los cuatro fallos seguidos del medio). El umbral filtra el ruido y reacciona solo a la señal.
La lección: el umbral es lo que convierte los health checks de un interruptor histérico en un juicio confiable. Umbral 1 reacciona a cada hipo; umbral 3 espera un patrón. El precio de un umbral alto es detectar las caídas reales un poco más tarde (tres sondas en vez de una); el precio de uno bajo es el flapping. El punto dulce típico —2 o 3 fallos para expulsar, 2 éxitos para readmitir, sondeando cada pocos segundos— equilibra reacción rápida con estabilidad.
Liveness contra readiness: qué debe comprobar /healthz
No basta con tener un health check; importa qué comprueba. Hay dos preguntas distintas que un servidor puede responder, y confundirlas causa problemas graves:
- Liveness ("¿estás vivo?"). ¿El proceso del servidor está corriendo y puede responder? Un
/healthzde liveness devuelve 200 si el servidor está en pie, sin más. Si falla, el servidor está muerto o colgado y hay que reiniciarlo. - Readiness ("¿estás listo para recibir tráfico?"). ¿El servidor puede de verdad atender una petición útil ahora mismo? Un
/healthzde readiness comprueba que las dependencias que necesita —la caché, la base de datos— están alcanzables. Si falla, el servidor está vivo pero no debe recibir tráfico todavía (arrancando, o con una dependencia caída), así que se saca del pool sin reiniciarlo.
La distinción importa al añadir instancias (lección 7): un servidor recién arrancado está vivo (liveness OK) pero quizás aún no listo (todavía calienta su caché local, abre sus conexiones a la base de datos). El balanceador debe esperar al readiness antes de mandarle tráfico, o los primeros usuarios que caigan en él verán errores.
Pero hay una trampa peligrosa en los readiness checks profundos, y conviene verla porque es un fallo clásico de diseño:
El peligro del health check profundo. Si el
/healthzde cada servidor comprueba "¿puedo llegar a la base de datos?", y la base de datos tiene un hipo de dos segundos, entonces todos los servidores fallan su health check a la vez. El balanceador, obediente, los saca a todos del pool —y ahora no queda ningún servidor en rotación, así que Enlace cae por completo, ¡por un hipo de la base de datos que quizás ni siquiera afectaba a las peticiones reales! Un problema menor y transitorio en una dependencia compartida se amplifica en una caída total.
La mitigación: los readiness checks profundos deben ser conservadores —no expulsar todo el pool por un fallo transitorio de una dependencia compartida—. En la práctica, se prefiere un liveness check ligero para el health check del balanceador (que solo mira si el proceso responde), y se maneja la salud de las dependencias con otros mecanismos: el propio servidor se protege del dependiente lento con timeouts y circuit breakers —que es justo el tema de la guía de resiliencia, no de aquí—. La regla: el health check del balanceador comprueba que el servidor está sano, no que todo el sistema aguas abajo lo está; mezclar las dos capas es cómo un hipo de la base de datos tumba el pool entero.
Errores comunes
Umbral de 1: expulsar al primer fallo (de flapping). Qué pasa: alguien configura el health check para sacar un servidor en cuanto falla una sonda, buscando "reaccionar rápido". Un servidor que tiene fallos transitorios (un pico de GC, una sonda que llegó en mal momento) entra y sale del pool sin parar, desestabilizando el reparto —lo mediste: 6 cambios de estado con un servidor que parpadea—. Por qué pasa: se confunde "reaccionar rápido" con "reaccionar a cada hipo". Cómo detectarlo: si tus servidores entran y salen de rotación con frecuencia y las métricas de pool oscilan, tienes flapping. Cómo corregirlo: sube el down_threshold a 2 o 3 fallos seguidos, para expulsar solo ante caídas sostenidas. El health check debe reaccionar a un patrón, no a un punto.
Mandar tráfico a un servidor que aún no está listo (de readiness). Qué pasa: se añade una instancia nueva y el balanceador le manda tráfico en cuanto el proceso arranca (liveness OK), pero la instancia todavía no abrió sus conexiones a la base de datos ni calentó nada. Los primeros usuarios que caen en ella ven errores o latencias enormes. Por qué pasa: se comprueba liveness (¿el proceso vive?) cuando había que comprobar readiness (¿puede atender ya?). Cómo detectarlo: si cada vez que escalas hay un pico de errores en los primeros segundos de las instancias nuevas, no esperaste al readiness. Cómo corregirlo: el balanceador debe esperar a que el health check de readiness pase antes de enrutar tráfico a una instancia nueva —dar tráfico solo a quien confirma que está listo, no solo vivo—.
Health check profundo que tumba todo el pool (de amplificación). Qué pasa: el /healthz de cada servidor comprueba la base de datos; la base de datos tiene un hipo transitorio; todos los servidores fallan el health check a la vez; el balanceador los saca a todos; Enlace cae por completo. Por qué pasa: se mete la salud de una dependencia compartida en el health check de cada servidor, convirtiendo un problema menor en uno total. Cómo detectarlo: si un hipo breve de la base de datos vacía el pool entero, tu health check es demasiado profundo. Cómo corregirlo: mantén el health check del balanceador ligero (liveness, o un readiness conservador que no expulse todo por un fallo transitorio compartido), y protege al servidor de su dependiente con timeouts y circuit breakers (guía de resiliencia). El balanceador comprueba la salud del servidor, no la de todo lo que hay debajo.
Ejercicios
Ejercicio 1 — Traza el pool. Un servidor tiene down_threshold=2 y up_threshold=2. Las sondas dan: OK, OK, FAIL, FAIL, FAIL, OK, FAIL, OK, OK. Traza cuándo está en el pool (SI/NO), empezando en el pool. ¿En qué sonda sale y en qué sonda vuelve?
Ver solución
Con down_threshold=2 (2 fallos seguidos para salir) y up_threshold=2 (2 éxitos seguidos para volver), empezando en el pool:
| Sonda | Salud | Fallos seg. | Éxitos seg. | En pool |
|---|---|---|---|---|
| 0 | OK | 0 | 1 | SI |
| 1 | OK | 0 | 2 | SI |
| 2 | FAIL | 1 | 0 | SI |
| 3 | FAIL | 2 | 0 | NO (2º fallo seguido → sale) |
| 4 | FAIL | 3 | 0 | NO |
| 5 | OK | 0 | 1 | NO (solo 1 éxito, necesita 2) |
| 6 | FAIL | 1 | 0 | NO (el éxito aislado se reinició) |
| 7 | OK | 0 | 1 | NO |
| 8 | OK | 0 | 2 | SI (2º éxito seguido → vuelve) |
Sale en la sonda 3 (segundo fallo consecutivo) y vuelve en la sonda 8 (segundo éxito consecutivo). Nota cómo el OK aislado de la sonda 5 no lo readmite: el FAIL de la sonda 6 reinicia el contador de éxitos, y hacen falta dos seguidos, que recién se cumplen en la 8. La histéresis en acción.
Ejercicio 2 — Elige el umbral. Para cada situación, di si conviene un down_threshold bajo (1, reacción rápida) o alto (3+, estable) y por qué. (a) Servidores que ocasionalmente tienen pausas de GC de medio segundo que hacen fallar una sonda aislada. (b) Un servidor que, cuando de verdad muere, empieza a corromper datos, así que quieres sacarlo cuanto antes. (c) Un pool en una red inestable donde las sondas fallan de vez en cuando por pérdida de paquetes, no por servidores enfermos.
Ver solución
- (a) Umbral alto (3+). Las pausas de GC causan fallos aislados que no significan "servidor muerto". Un umbral alto los filtra: el servidor solo sale si falla sostenidamente, no por un hipo de GC. Umbral 1 causaría flapping por cada GC.
- (b) Umbral bajo (1-2). Si un servidor muerto hace daño activo (corrompe datos), el costo de dejarlo un instante más supera el costo de un flapping ocasional. Aquí sí quieres reacción rápida, aceptando algo más de sensibilidad. (En la práctica el daño de datos se previene de otras formas, pero el principio de "reacción rápida cuando el fallo es caro" se sostiene.)
- (c) Umbral alto (3+). Si las sondas fallan por la red, no por los servidores, un umbral bajo expulsaría servidores sanos por pérdida de paquetes —flapping causado por el medio, no por los backends—. Un umbral alto exige un patrón sostenido antes de creer que el servidor (y no la red) es el problema.
Ejercicio 3 — El health check profundo. Enlace configura el /healthz de cada servidor de app para que compruebe "¿puedo hacer un SELECT a la base de datos?". Un día, la base de datos tiene un pico de latencia de 3 segundos. (a) ¿Qué le pasa al pool de servidores de app? (b) ¿Por qué es peor que no tener health check para esa dependencia? (c) ¿Cómo debería estar diseñado el health check en su lugar?
Ver solución
- (a) Todos los servidores de app fallan su
/healthza la vez (todos comparten la misma base de datos, que está lenta), así que el balanceador los saca a todos del pool. No queda ningún servidor en rotación: Enlace cae por completo, por un pico transitorio de la base de datos. - (b) Porque amplifica el problema en vez de contenerlo. Sin ese health check profundo, un pico de 3 s de la base de datos causaría, como mucho, algunas peticiones lentas —pero el sistema seguiría en pie, y la caché (módulo 4) absorbería la mayoría de las lecturas—. Con el health check profundo, un problema menor y transitorio de una dependencia compartida se convierte en una caída total: el remedio es peor que la enfermedad.
- (c) El health check del balanceador debe ser ligero —comprobar que el proceso del servidor responde (liveness), no que toda la infraestructura aguas abajo está perfecta—. La salud de la base de datos se maneja donde corresponde: el servidor se protege del dependiente lento con timeouts y circuit breakers (guía de resiliencia), y la caché amortigua los picos. El balanceador comprueba la salud del servidor, no la del sistema entero; meter la base de datos en el health check de cada servidor es cómo un hipo tumba el pool.
Resumen y siguiente paso
En esta lección le diste al balanceador los ojos que dabas por hecho: los health checks, las sondas con las que sabe qué servidor está vivo. Con el capataz que revisa a su cuadrilla viste que la clave no es "¿responde?" sino "¿responde de forma consistente?", y que expulsar o readmitir por una sola respuesta es de capataz histérico. Distinguiste el health check activo (el balanceador sondea, proactivo) del pasivo (observa los fallos reales, reactivo), y ejecutaste la lógica de umbrales: un servidor sale tras N fallos seguidos y vuelve tras M éxitos seguidos, con la histéresis que eso implica. Mediste por qué importan: un servidor que parpadea causa 6 cambios de estado con umbral 1 (flapping) y solo 2 con umbral 3 (que filtra el ruido). Y separaste liveness (¿vive?) de readiness (¿está listo?), con la advertencia del health check profundo que expulsa todo el pool por un hipo de la base de datos —un problema que se contiene manteniendo el check ligero y dejando la protección del dependiente a los circuit breakers de la guía de resiliencia—.
Antes de avanzar deberías poder: explicar cómo un balanceador sabe que un servidor cayó; distinguir health check activo de pasivo; justificar los umbrales a partir del flapping que evitan; distinguir liveness de readiness; y explicar por qué un health check profundo puede tumbar el pool entero.
Lo que sigue es cosechar todo el módulo. Con servidores stateless (lecciones 4-5), un balanceador con algoritmos de reparto (lecciones 2-3) y health checks que saben quién está vivo (esta lección), tienes por fin las tres piezas para hacer lo que el módulo prometió: añadir y quitar instancias a voluntad. En la lección 7 vas a ver el escalado horizontal en acción —cuántas instancias necesita Enlace, cómo entra una nueva (arranca, pasa el readiness, entra a rotación) y cómo sale una con gracia (connection draining)—, y por qué la statelessness es lo que hace que todo esto sea rutina en vez de riesgo.
Recursos
- HAProxy — Health checks (
check,rise,fall,inter) — la documentación de HAProxy con los parámetros reales de esta lección:fall(fallos para expulsar, eldown_threshold),rise(éxitos para readmitir, elup_threshold) einter(intervalo de sondeo). El puente directo de la teoría de umbrales a la configuración de producción. - Kubernetes — Liveness, Readiness and Startup Probes — la distinción liveness/readiness formalizada en el orquestador más usado, con los mismos conceptos (thresholds, periodos, qué reiniciar contra qué sacar de rotación). Muestra cómo los health checks de esta lección se configuran en un sistema real.
- nginx — Health checks (upstream,
max_fails,fail_timeout) — la configuración de health checks pasivos y activos en nginx, con los umbrales de fallo y el arranque lento (slow_start) que evita mandar tráfico completo a una instancia recién readmitida. Un tercer ángulo sobre los mismos mecanismos.