StackPractices
beginner Por Mathias Paulenko

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.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Visión General

El Patron de Monitoreo de Endpoints de Salud expone endpoints ligeros que reportan si un servicio esta vivo y listo para recibir trafico. Los balanceadores de carga, orquestadores de contenedores y herramientas de monitoreo pueden consultar estos endpoints para decidir si enrutar trafico hacia una instancia o reiniciarla.

Este patron es la base de los sistemas auto-curativos y es esencial para cualquier servicio que se ejecute en un entorno dinamico donde las instancias pueden fallar o reiniciarse en cualquier momento.

Cuándo Usar

Usa este patron cuando:

  • Ejecutes servicios en contenedores o detras de un balanceador de carga
  • Quieras que un orquestador reinicie instancias no saludables automaticamente
  • Necesites distinguir entre “el proceso esta corriendo” y “el servicio es usable”
  • Quieras agregar health checks de dependencias sin modificar el codigo cliente
  • Necesites mostrar datos de salud en un dashboard de monitoreo o sistema de alertas

Solución

// Endpoints de salud Express con probes de liveness y readiness
const express = require('express');
const app = express();

app.get('/health/live', (req, res) => {
  res.status(200).json({ status: 'alive' });
});

app.get('/health/ready', async (req, res) => {
  const dbHealthy = await checkDatabaseConnection();
  const cacheHealthy = await checkCacheConnection();
  if (dbHealthy && cacheHealthy) {
    res.status(200).json({ status: 'ready' });
  } else {
    res.status(503).json({ status: 'not ready' });
  }
});

app.listen(3000);
# Probes de liveness y readiness en Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
spec:
  template:
    spec:
      containers:
      - name: api
        image: api:latest
        livenessProbe:
          httpGet:
            path: /health/live
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 15
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10

Explicación

Los endpoints de salud separan dos preocupaciones:

  • Liveness: el proceso esta corriendo y no deberia reiniciarse. Si el liveness falla, el orquestador mata el contenedor e inicia uno nuevo.
  • Readiness: el servicio esta listo para recibir trafico. Si el readiness falla, el balanceador deja de enviar solicitudes pero no reinicia la instancia.

Al verificar dependencias como bases de datos, caches y colas de mensajes, los probes de readiness evitan que el trafico llegue a una instancia que no puede atender solicitudes correctamente. Esto mejora la confiabilidad y reduce las tasas de error durante despliegues o interrupciones.

Variantes

EndpointPropositoRespuesta
LivenessEl proceso esta vivo?200 cuando corre, 500 en caso contrario
ReadinessPuede atender trafico?200 cuando las dependencias estan saludables, 503 en caso contrario
StartupHa terminado de iniciar?200 cuando la inicializacion completa
Deep healthEstado detallado de subsistemasJSON con salud por dependencia

Lo que Funciona

  • Manten el probe de liveness ligero y libre de dependencias
  • Haz que el probe de readiness refleje la capacidad real de atender solicitudes
  • Devuelve codigos de estado consistentes (200 saludable, 503 no saludable)
  • Evita operaciones pesadas en los health checks para prevenir falsos fallos
  • Agrega timeouts y presupuestos de reintentos para verificaciones de dependencias
  • Registra los fallos de health checks para debugging pero no satures los logs en cada llamada

Errores Comunes

  • Usar un unico endpoint que devuelve OK incluso cuando el servicio esta roto
  • Hacer que los health checks dependan de servicios externos que no son criticos
  • Devolver 500 para liveness, causando reinicios innecesarios
  • Olvidar probar los readiness probes durante los despliegues
  • Exponer endpoints de salud publicamente sin autenticacion o rate limiting

Soluciones Avanzadas

Endpoint de salud profundo con verificaciones de dependencias

Implementa un endpoint de salud comprehensivo que verifica todas las dependencias:

const express = require('express');
const app = express();

const healthChecks = {
  database: async () => {
    try {
      await pool.query('SELECT 1');
      return { status: 'healthy', latency: Date.now() - start };
    } catch (error) {
      return { status: 'unhealthy', error: error.message };
    }
  },
  cache: async () => {
    try {
      await cache.ping();
      return { status: 'healthy' };
    } catch (error) {
      return { status: 'unhealthy', error: error.message };
    }
  },
  messageQueue: async () => {
    try {
      await channel.checkQueue();
      return { status: 'healthy' };
    } catch (error) {
      return { status: 'unhealthy', error: error.message };
    }
  }
};

app.get('/health/deep', async (req, res) => {
  const results = {};
  let overallHealthy = true;

  for (const [name, check] of Object.entries(healthChecks)) {
    try {
      const start = Date.now();
      const result = await check();
      results[name] = { ...result, checkTime: Date.now() - start };
      if (result.status !== 'healthy') {
        overallHealthy = false;
      }
    } catch (error) {
      results[name] = { status: 'error', error: error.message };
      overallHealthy = false;
    }
  }

  res.status(overallHealthy ? 200 : 503).json({
    status: overallHealthy ? 'healthy' : 'unhealthy',
    checks: results,
    timestamp: new Date().toISOString()
  });
});

Probe de startup para servicios de inicializacion lenta

Usa un probe de startup para servicios que toman tiempo en inicializar:

# Deployment de Kubernetes con probe de startup
apiVersion: apps/v1
kind: Deployment
metadata:
  name: slow-startup-service
spec:
  template:
    spec:
      containers:
      - name: app
        image: app:latest
        startupProbe:
          httpGet:
            path: /health/startup
            port: 3000
          initialDelaySeconds: 0
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 30  # Permitir hasta 5 minutos para iniciar
        livenessProbe:
          httpGet:
            path: /health/live
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 15
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 10
// Endpoint de startup que devuelve exito solo despues de inicializacion
let isInitialized = false;

async function initialize() {
  // Realizar tareas de inicializacion lentas
  await loadConfiguration();
  await warmUpCache();
  await connectToExternalServices();
  isInitialized = true;
}

app.get('/health/startup', (req, res) => {
  if (isInitialized) {
    res.status(200).json({ status: 'initialized' });
  } else {
    res.status(503).json({ status: 'initializing' });
  }
});

// Iniciar inicializacion en background
initialize();

Endpoint de salud con circuit breaker

Agrega el patron de circuit breaker para prevenir tormentas de health checks:

class HealthCheckCircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'closed'; // closed, open, half-open
  }

  recordSuccess() {
    this.failureCount = 0;
    this.state = 'closed';
  }

  recordFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.threshold) {
      this.state = 'open';
    }
  }

  shouldAllowCheck() {
    if (this.state === 'closed') return true;
    
    if (this.state === 'open') {
      const timeSinceLastFailure = Date.now() - this.lastFailureTime;
      if (timeSinceLastFailure > this.timeout) {
        this.state = 'half-open';
        return true;
      }
      return false;
    }
    
    return true;
  }
}

const circuitBreaker = new HealthCheckCircuitBreaker();

app.get('/health/ready', async (req, res) => {
  if (!circuitBreaker.shouldAllowCheck()) {
    return res.status(503).json({ status: 'circuit open' });
  }

  try {
    const dbHealthy = await checkDatabaseConnection();
    const cacheHealthy = await checkCacheConnection();
    
    if (dbHealthy && cacheHealthy) {
      circuitBreaker.recordSuccess();
      res.status(200).json({ status: 'ready' });
    } else {
      circuitBreaker.recordFailure();
      res.status(503).json({ status: 'not ready' });
    }
  } catch (error) {
    circuitBreaker.recordFailure();
    res.status(503).json({ status: 'error', message: error.message });
  }
});

Preguntas frecuentes

¿Es este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.