Patrón Health Check: Sondeos de Liveness y Readiness
Implementa sondeos de liveness y readiness para orquestadores. Cubre probes en Kubernetes, chequeos de dependencias y degradación elegante.
Visión General
Los health checks exponen endpoints que le indican al orquestador si una aplicación está corriendo y lista para servir tráfico. Kubernetes, ECS y otros orquestadores usan dos tipos de probe distintos: liveness (¿el proceso está vivo?) y readiness (¿el proceso puede atender requests?). El probe de liveness determina si un contenedor debería reiniciarse; el de readiness determina si debería recibir tráfico. Confundirlos provoca reinicios innecesarios o tráfico enviado a instancias que no están listas.
Este patrón cubre el lado del orquestador en el contrato — ciclo de vida de los probes, timings, umbrales y comportamiento durante el apagado. Si lo que necesitas son los endpoints como superficie de monitoreo general para dashboards y chequeos externos, eso es el Patrón de Monitoreo de Endpoint de Salud.
Cuándo Usar
- Cualquier aplicación corriendo en un orquestador de contenedores (Kubernetes, ECS, Nomad)
- Servicios con dependencias que tardan en inicializarse (conexiones a base de datos, calentamiento de caché)
- Aplicaciones que pueden quedar bloqueadas sin crashear (deadlocks, agotamiento de threads)
- Servicios que necesitan drenar conexiones antes de apagarse
- Health checks de load balancer para el enrutado de tráfico
Cuándo No Usar
- Scripts simples o batch jobs — no hay proceso de larga duración que sondear
- Aplicaciones sin dependencias externas — un chequeo de proceso basta
- Cuando el orquestador no soporta probes — usa un gestor de procesos en su lugar
- Para monitoreo interno de salud — usa métricas y alertas, no probes
Solución
Endpoint de salud básico (Python/FastAPI)
# Python — endpoint de health check básico
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/health")
async def health():
"""Chequeo básico de liveness — el proceso está vivo."""
return {"status": "healthy"}
@app.get("/health/live")
async def liveness():
"""Probe de liveness — ¿el proceso está corriendo?"""
return {"status": "alive"}
@app.get("/health/ready")
async def readiness():
"""Probe de readiness — ¿podemos servir tráfico?"""
checks = await run_dependency_checks()
all_healthy = all(checks.values())
if not all_healthy:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"status": "not ready", "checks": checks},
)
return {"status": "ready", "checks": checks}
async def run_dependency_checks():
return {
"database": await check_database(),
"redis": await check_redis(),
"message_queue": await check_message_queue(),
}
Chequeos de dependencias con timeouts
# Python — chequeos de dependencias con timeouts
import asyncio
import aio_pika
import asyncpg
import redis.asyncio as aioredis
async def check_database(timeout: float = 2.0) -> bool:
try:
async with asyncpg.create_pool("postgresql://localhost/myapp") as pool:
async with pool.acquire() as conn:
await asyncio.wait_for(
conn.execute("SELECT 1"),
timeout=timeout,
)
return True
except Exception:
return False
async def check_redis(timeout: float = 1.0) -> bool:
try:
client = aioredis.from_url("redis://localhost:6379")
pong = await asyncio.wait_for(client.ping(), timeout=timeout)
await client.aclose()
return pong
except Exception:
return False
async def check_message_queue(timeout: float = 2.0) -> bool:
try:
# Comprueba si podemos conectar a RabbitMQ
connection = await asyncio.wait_for(
aio_pika.connect_robust("amqp://localhost"),
timeout=timeout,
)
await connection.close()
return True
except Exception:
return False
Configuración de probes en Kubernetes
# Deployment de Kubernetes con probes de liveness y readiness
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:1.0.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Health checks en Express.js
// JavaScript — endpoints de health check en Express
const express = require('express');
const app = express();
// Liveness — el proceso está vivo
app.get('/health/live', (req, res) => {
res.json({ status: 'alive', uptime: process.uptime() });
});
// Readiness — puede servir tráfico
app.get('/health/ready', async (req, res) => {
// Ejecuta los chequeos en paralelo — awaits secuenciales
// apilarían la latencia de cada chequeo y arriesgarían
// agotar el timeout del probe
const [database, redisOk, queue] = await Promise.all([
checkDatabase(),
checkRedis(),
checkQueue(),
]);
const checks = { database, redis: redisOk, queue };
const allHealthy = Object.values(checks).every(v => v === true);
if (allHealthy) {
res.json({ status: 'ready', checks });
} else {
res.status(503).json({ status: 'not ready', checks });
}
});
// Endpoint de salud completo
app.get('/health', async (req, res) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
queue: await checkQueue(),
diskSpace: checkDiskSpace(),
memoryUsage: checkMemoryUsage(),
};
const status = Object.values(checks).every(v => v === true)
? 'healthy'
: 'degraded';
res.json({
status,
uptime: process.uptime(),
timestamp: new Date().toISOString(),
checks,
});
});
async function checkDatabase() {
try {
await db.raw('SELECT 1');
return true;
} catch {
return false;
}
}
async function checkRedis() {
try {
await redis.ping();
return true;
} catch {
return false;
}
}
function checkMemoryUsage() {
const used = process.memoryUsage().rss;
const max = 512 * 1024 * 1024; // límite de 512MB
return used < max;
}
Health checks en Java Spring Boot
Spring Boot Actuator ya trae grupos de estado de liveness y readiness listos para usar; la receta Chequeos de Salud Personalizados con Spring Boot Actuator explica cómo cablearlos. Los indicadores personalizados se ven así:
// Java — health indicators de Spring Boot Actuator
import org.springframework.boot.actuate.health.*;
import org.springframework.stereotype.Component;
@Component
public class DatabaseHealthIndicator extends AbstractHealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected void doHealthCheck(Health.Builder builder) {
try (Connection conn = dataSource.getConnection()) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 1");
}
builder.up().withDetail("database", "PostgreSQL").withDetail("status", "connected");
} catch (SQLException e) {
builder.down().withDetail("error", e.getMessage());
}
}
}
@Component
public class RedisHealthIndicator extends AbstractHealthIndicator {
private final RedisTemplate<String, String> redisTemplate;
public RedisHealthIndicator(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doHealthCheck(Health.Builder builder) {
try {
String response = redisTemplate.getConnectionFactory()
.getConnection().ping();
builder.up().withDetail("redis", response);
} catch (Exception e) {
builder.down().withDetail("error", e.getMessage());
}
}
}
// application.yml — configurar los endpoints de salud
// management:
// endpoints:
// web:
// exposure:
// include: health,info,metrics
// endpoint:
// health:
// probes:
// enabled: true
// show-details: always
// health:
// livenessstate:
// enabled: true
// readinessstate:
// enabled: true
Apagado elegante con health checks
# Python — apagado elegante con probe de readiness
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.responses import JSONResponse
shutting_down = False
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
# Se ejecuta al apagar (aquí aterriza el SIGTERM de Kubernetes)
global shutting_down
shutting_down = True
# Dale a kube-proxy/endpoints unos segundos para observar
# readiness=503 antes de que el servidor deje de aceptar conexiones
await asyncio.sleep(5)
await drain_connections(timeout=25)
app = FastAPI(lifespan=lifespan)
@app.get("/health/ready")
async def readiness():
if shutting_down:
return JSONResponse(
status_code=503,
content={"status": "shutting down"},
)
# Chequeo normal de readiness
checks = await run_dependency_checks()
if not all(checks.values()):
return JSONResponse(status_code=503, content={"status": "not ready", "checks": checks})
return {"status": "ready", "checks": checks}
async def drain_connections(timeout: int = 25):
"""Espera a que las requests en curso terminen."""
deadline = asyncio.get_event_loop().time() + timeout
while getattr(app.state, "active_requests", 0) > 0:
if asyncio.get_event_loop().time() > deadline:
break
await asyncio.sleep(0.5)
Probe de arranque para inicializaciones lentas
# Python — startup probe para aplicaciones con inicialización lenta
import time
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
startup_time = time.time()
ready_since = None
@app.get("/health/startup")
async def startup_probe():
"""Startup probe — ¿la app sigue inicializándose?"""
if ready_since is None:
return JSONResponse(status_code=503, content={"status": "starting"})
return {"status": "started", "ready_since": ready_since}
@app.get("/health/ready")
async def readiness():
global ready_since
if ready_since is None:
# Comprueba si la inicialización está completa
if await is_initialization_complete():
ready_since = time.time()
else:
return JSONResponse(status_code=503, content={"status": "initializing"})
# Chequeo normal de readiness
return {"status": "ready"}
Health check compuesto con circuit breakers
// JavaScript — salud compuesta con estados de circuit breaker
const CircuitBreaker = require('opossum');
const dbBreaker = new CircuitBreaker(queryDatabase, {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
app.get('/health/ready', async (req, res) => {
const checks = {};
// Chequeo de base de datos vía circuit breaker
try {
await dbBreaker.fire('SELECT 1');
checks.database = {
status: dbBreaker.opened ? 'degraded' : 'healthy',
circuitState: dbBreaker.status,
};
} catch {
checks.database = {
status: 'unhealthy',
circuitState: dbBreaker.status,
};
}
const allHealthy = Object.values(checks)
.every(c => c.status === 'healthy');
res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'ready' : 'degraded',
checks,
});
});
Explicación
Es el kubelet —no un controlador central— quien ejecuta cada probe contra cada contenedor. Cada tipo de probe responde una pregunta distinta y dispara una acción distinta cuando falla:
| Probe | Pregunta que responde | Qué hace Kubernetes cuando falla |
|---|---|---|
startupProbe | ¿El contenedor terminó de arrancar? | Sigue sondeando hasta agotar failureThreshold × periodSeconds y luego reinicia el contenedor. Mientras corre, liveness y readiness quedan en pausa. |
readinessProbe | ¿El contenedor puede servir tráfico ahora? | Saca el pod de los endpoints del Service. El proceso sigue corriendo — no hay reinicio. |
livenessProbe | ¿El proceso está vivo o bloqueado? | Reinicia el contenedor tras failureThreshold fallos consecutivos. |
De este modelo se deducen dos consecuencias. Primero, un probe de readiness que falla es barato: el pod simplemente deja de recibir requests hasta recuperarse, así que es el lugar correcto para los chequeos de dependencias. Segundo, un probe de liveness que falla es caro: el contenedor se mata y reinicia, perdiendo trabajo en curso y cachés calientes. Esa asimetría es la razón por la que liveness solo debería probar si el proceso está atascado, nunca si una base de datos o una API externa responde.
El startupProbe existe porque initialDelaySeconds obliga a un mal compromiso: con un valor bajo los arranques lentos se matan a mitad de la inicialización, y con uno alto los contenedores realmente muertos tardan minutos en reiniciarse. El startup probe reemplaza esa apuesta por un bucle — failureThreshold: 30 con periodSeconds: 5 le da a la app 150 segundos para arrancar, y después los probes normales toman el control a toda velocidad.
Los health checks también tienen un presupuesto de coste. Un probe de readiness que consulta tres dependencias cada 5 segundos genera carga real sobre tu base de datos y Redis: con 10 réplicas son 6 conexiones por segundo por dependencia. Mantén los chequeos baratos (SELECT 1, PING), acótalos con timeouts cortos y no dejes que una dependencia lenta consuma todo el presupuesto del probe — para eso sirve la variante con circuit breaker de arriba.
Variantes
Health check con grafo de dependencias
# Python — health check con grafo de dependencias
class HealthChecker:
def __init__(self):
self.dependencies = {}
def register(self, name, check_fn, dependencies=None):
self.dependencies[name] = {
"check": check_fn,
"deps": dependencies or [],
}
async def check_all(self):
results = {}
for name, config in self.dependencies.items():
# Chequea primero las dependencias
dep_results = {dep: results.get(dep, False) for dep in config["deps"]}
if not all(dep_results.values()):
results[name] = {
"status": "skipped",
"reason": f"Dependencies unhealthy: {[d for d, v in dep_results.items() if not v]}",
}
continue
try:
healthy = await config["check"]()
results[name] = {"status": "healthy" if healthy else "unhealthy"}
except Exception as e:
results[name] = {"status": "unhealthy", "error": str(e)}
return results
# Uso
checker = HealthChecker()
checker.register("database", check_database)
checker.register("redis", check_redis)
checker.register("cache", check_cache, dependencies=["redis"])
checker.register("api", check_api, dependencies=["database", "cache"])
Health check por TCP
# Kubernetes — probe de socket TCP para servicios sin HTTP
apiVersion: v1
kind: Pod
spec:
containers:
- name: tcp-service
image: tcp-service:1.0
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 5
periodSeconds: 5
Health check por comando
Para procesos worker sin servidor HTTP, usa un probe exec que ejecuta un script dentro del contenedor. La misma idea alimenta la instrucción HEALTHCHECK de Docker — mira Configuración de Health Check en Docker para la variante nativa de Docker.
# Python — probe por comando para procesos worker
# probe_script.py
import sys
import psycopg2
def check_worker_health():
try:
conn = psycopg2.connect("postgresql://localhost/myapp")
cur = conn.cursor()
# Sondeo de conectividad barato — si la query corre, el worker está bien
cur.execute("SELECT COUNT(*) FROM jobs WHERE status = 'processing'")
cur.fetchone()
cur.close()
conn.close()
return True
except Exception:
return False
if __name__ == "__main__":
if check_worker_health():
print("healthy")
sys.exit(0)
else:
print("unhealthy")
sys.exit(1)
# Kubernetes — probe por comando
spec:
containers:
- name: worker
image: worker:1.0
livenessProbe:
exec:
command:
- python
- /app/probe_script.py
initialDelaySeconds: 30
periodSeconds: 60
Mejores Prácticas
- Para una guía más profunda, mira el Patrón de Monitoreo de Endpoint de Salud.
- Separa liveness de readiness — liveness comprueba si el proceso está vivo, readiness si puede servir tráfico
- Mantén liveness simple — no chequees dependencias en liveness, solo el propio proceso
- Usa startup probes para inicializaciones lentas — evita reinicios prematuros durante el arranque
- Configura timeouts razonables — 2-3 segundos para probes HTTP, más para el arranque
- Devuelve 503 cuando no esté lista — los orquestadores miran el código HTTP, no el cuerpo de la respuesta
- Incluye el detalle de dependencias en readiness — ayuda a depurar por qué un servicio no está listo
- Implementa el apagado elegante — devuelve 503 en readiness durante el apagado para cortar el tráfico
- No abuses de la frecuencia — sondear cada segundo es excesivo; cada 5-10 segundos es suficiente
- Usa circuit breakers en los health checks — que una dependencia lenta no haga expirar tu probe
Errores Comunes
- Mismo endpoint para liveness y readiness: si readiness falla (base de datos caída), liveness también falla y provoca reinicios innecesarios. Usa endpoints separados.
- Chequear dependencias en liveness: liveness solo debería verificar que el proceso corre. Los chequeos de dependencias pertenecen a readiness.
- Sin startup probe: las aplicaciones de arranque lento se matan antes de terminar de inicializar. Usa startup probes con un
failureThresholdalto. - Probes demasiado agresivos: sondear cada segundo con timeout de 1 segundo produce falsos fallos bajo carga. Usa intervalos de 5-10 segundos.
- No gestionar el apagado elegante: durante el apagado el proceso está vivo pero no puede servir tráfico. Readiness debería devolver 503 de inmediato.
Ver También
- Repositorio companion — demo ejecutable en FastAPI con los tres endpoints de probe, un Deployment de Kubernetes y un laboratorio docker-compose con PostgreSQL y Redis
- Configure Liveness, Readiness and Startup Probes — documentación oficial de Kubernetes
- Spring Boot Actuator endpoints — grupos de estado de liveness y readiness incorporados
- Container probes — semántica de los probes y referencia de campos
Preguntas frecuentes
¿Cuál es la diferencia entre los probes de liveness y readiness?
Liveness responde "¿el proceso está atascado?" — si falla, Kubernetes reinicia el contenedor, que es caro. Readiness responde "¿puede atender requests ahora mismo?" — si falla, el pod solo sale de los endpoints del Service y sigue corriendo. Un síntoma clásico de confundirlos: tu base de datos parpadea, readiness falla, y si liveness comparte el mismo endpoint todas las réplicas se reinician a la vez en lugar de esperar a que pase la caída.
¿Cuándo conviene usar un startup probe?
Siempre que el arranque varíe o supere los ~30 segundos — cargar modelos de ML, calentar cachés, abrir muchas conexiones. Sin él te ves forzado a inflar initialDelaySeconds en el probe de liveness, lo que retrasa la detección de contenedores realmente muertos. Un startup probe con failureThreshold: 30 y periodSeconds: 5 concede 150 segundos de arranque y después le cede el control a chequeos de liveness rápidos.
¿Los health checks deberían chequear todas las dependencias?
Solo las necesarias para servir requests, y solo en readiness. Si tu app puede degradar con elegancia sin Redis (fallback cache-aside), Redis tampoco debería condicionar readiness. Liveness no debería chequear nada externo — una base de datos caída nunca significa que tu proceso necesite un reinicio.
¿Qué código HTTP deberían devolver los health checks?
200 cuando está sano o listo, 503 cuando no. El kubelet solo mira el código de estado — el cuerpo JSON es para humanos y dashboards. Cualquier código en el rango 200-399 cuenta como éxito, pero quédate con 200/503 para que dashboards y humanos lean la misma señal que el orquestador.
¿Cómo gestiono el apagado elegante?
Al recibir SIGTERM, pasa readiness a 503 de inmediato para que Kubernetes deje de enrutar tráfico nuevo — pero sigue sirviendo, porque la baja del endpoint es asíncrona y tarda unos segundos en propagarse por kube-proxy. Luego drena las requests en curso y sal antes de que expire terminationGracePeriodSeconds (30 por defecto) y llegue el SIGKILL.
Recursos Relacionados
Patron de Monitoreo de Endpoints de Salud
Expone endpoints de salud ligeros para que orquestadores, balanceadores de carga y herramientas de monitoreo verifiquen la disponibilidad del servicio.
RecipeEndpoint de Health Check
Cómo implementar un endpoint de health check listo para producción para monitoreo y load balancers.
PatternCircuit Breaker con Monitoring
Cómo exponer circuit breaker state como métricas para observability. Cubre Prometheus integration, reglas de alerting, dashboards y state transitions.
PatternStructured Logging: logs JSON con campos consistentes
Cómo emitir logs JSON estructurados con campos consistentes para hacerlos consultables. Cubre structlog en Python, Winston, pino, Serilog, niveles de log y agregación.
PatternPatrón Correlation ID
Cómo propagar correlation IDs a través de service boundaries para end-to-end request tracing. Cubre HTTP headers, message queues, e integración con logging.