Liveness, Readiness y Startup Probe en Kubernetes

1. Descripción general

Cuando ejecutas una aplicación en Kubernetes, el clúster no sabe automáticamente si tu pod está “vivo” o “listo para recibir tráfico” — tiene que preguntarlo a través de probes. Este artículo explica las diferencias entre liveness, readiness y startup probe, con ejemplos de configuración usando Spring Boot Actuator y YAML de Deployment. Cuando termines de leerlo, podrás configurar correctamente los probes para un servicio real como OrderService sin que los pods entren en loops infinitos de restart innecesarios.

2. Por qué necesitamos probes

Por defecto, Kubernetes solo sabe si el proceso dentro del contenedor sigue ejecutándose (estado del contenedor Running) o no. No sabe si la aplicación está en deadlock, si perdió la conexión a la base de datos, o si todavía está arrancando.

Las consecuencias sin probes son:

  • Pod colgado (deadlock) pero aún recibiendo tráfico → requests se agotan en timeout constantemente.
  • Pod acaba de arrancar, no ha establecido conexión a la BD, y ya el Service lo incluye en la lista de endpoints → errores 500.

Los tres tipos de probe resuelven tres problemas distintos:

Probe Responde la pregunta Cuando falla
Liveness ¿El contenedor sigue vivo? Kubernetes reinicia el contenedor
Readiness ¿El contenedor está listo para recibir tráfico? Kubernetes quita el pod de los endpoints del Service (sin reiniciar)
Startup ¿El contenedor ha terminado de arrancar? Kubernetes espera, desactiva temporalmente liveness y readiness

3. Liveness probe

El liveness probe responde: “¿Este proceso está en un estado del que no puede recuperarse por sí solo?”. Si el probe falla más de failureThreshold veces, kubelet mata y reinicia el contenedor.

3.1. Agregar Spring Boot Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoint.health.probes.enabled=true
management.endpoints.web.exposure.include=health,info

La línea management.endpoint.health.probes.enabled=true activa dos endpoints: /actuator/health/liveness y /actuator/health/readiness, exactamente como Kubernetes los espera.

3.2. Declarar en el Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 20
            periodSeconds: 10
            failureThreshold: 3
  • initialDelaySeconds: 20: espera 20 segundos después de que el contenedor arranque antes de comenzar a probar.
  • periodSeconds: 10: llama al probe cada 10 segundos.
  • failureThreshold: 3: falla 3 veces seguidas para ser considerado muerto → reinicio.

Nota importante: El liveness probe solo debe verificar “¿la aplicación sigue funcionando?”, no debe verificar dependencias externas (BD, Redis, etc.). Si la BD muere y el liveness falla por eso, Kubernetes reiniciará el pod constantemente sin resolver nada — este es un error muy común.

4. Readiness probe

El readiness probe responde: “¿el pod está listo para procesar requests?”. Cuando falla, el pod se quita de los endpoints del Service — el tráfico no se envía a este pod, pero el contenedor no se reinicia.

4.1. Indicador readiness personalizado

Supongamos que OrderService necesita conectar a PaymentGateway antes de estar listo:

@Component
public class PaymentGatewayReadinessIndicator implements HealthIndicator {

    private final PaymentGatewayClient paymentGatewayClient;

    public PaymentGatewayReadinessIndicator(PaymentGatewayClient paymentGatewayClient) {
        this.paymentGatewayClient = paymentGatewayClient;
    }

    @Override
    public Health health() {
        if (paymentGatewayClient.isConnected()) {
            return Health.up().withDetail("gateway", "connected").build();
        }
        return Health.down().withDetail("gateway", "unreachable").build();
    }
}

Para incluir este indicador en el grupo readiness (sin afectar liveness), declara el grupo:

management.endpoint.health.group.readiness.include=readinessState,paymentGateway
  • health() retorna Health.down() cuando PaymentGatewayClient no puede conectar — el probe readiness fallará, el pod no recibirá tráfico hasta que el gateway esté disponible.
  • Spring automáticamente renombra el bean paymentGatewayReadinessIndicator como la clave paymentGateway en la respuesta health.

4.2. Test para el indicador

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>
class PaymentGatewayReadinessIndicatorTest {

    @Test
    void shouldReportDownWhenGatewayNotConnected() {
        PaymentGatewayClient client = mock(PaymentGatewayClient.class);
        when(client.isConnected()).thenReturn(false);

        PaymentGatewayReadinessIndicator indicator = new PaymentGatewayReadinessIndicator(client);
        Health health = indicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.DOWN);
        assertThat(health.getDetails()).containsEntry("gateway", "unreachable");
    }
}
  • mock(PaymentGatewayClient.class) simula la dependencia externa, sin necesidad de una conexión real.
  • assertThat(health.getStatus()) verifica que el estado sea DOWN cuando gateway no está conectado — el comportamiento esperado del readiness.

4.3. Declarar readiness en el Deployment

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 3
  successThreshold: 1
  • successThreshold: 1: solo 1 probe exitoso para que el pod sea considerado ready nuevamente (distinto a liveness, donde típicamente no necesitas este campo porque el default ya es 1).
  • periodSeconds: 5 es más corto que liveness porque readiness necesita reaccionar rápido a cambios de estado en las dependencias.

5. Startup probe

Con aplicaciones de arranque lento (cargar cachés grandes, ejecutar migraciones de BD), si solo usas liveness con initialDelaySeconds fijo, tienes que adivinar un número lo suficientemente grande — fácil de equivocarse y pierdes tiempo esperando en cada restart posterior.

El startup probe resuelve esto: mientras el startup probe no pase, Kubernetes desactiva temporalmente liveness y readiness, evitando que se mate el contenedor injustamente mientras la aplicación está arrancando legitimamente.

startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  failureThreshold: 30
  periodSeconds: 10
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 10
  failureThreshold: 3
  • startupProbe usa failureThreshold: 30 y periodSeconds: 10 → permite que la app arranque máximo 300s (30 × 10s) antes de ser considerada fallida.
  • Una vez que startupProbe pasa por primera vez, Kubernetes cambia a monitorear livenessProbe con threshold más pequeño (3 veces × 10s = 30s) — reacción más rápida ante problemas reales durante la ejecución estable.

Nota: Si tu aplicación arranca rápido (menos de unos segundos, como una REST API simple sin cargar cachés grandes), no necesitas declarar startupProbe — solo agrega más líneas al YAML sin beneficio claro.

6. Combinando los tres en un Deployment

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: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            failureThreshold: 30
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            periodSeconds: 5
            failureThreshold: 3

El flujo de ejecución cuando se crea un nuevo pod:

  1. El contenedor arranca, Kubernetes comienza a llamar a startupProbe inmediatamente (sin initialDelaySeconds porque startup lo maneja).
  2. Mientras tanto livenessProbe y readinessProbe no se ejecutan.
  3. Cuando startupProbe retorna HTTP 200 por primera vez, Kubernetes detiene el startup probe, comienza a ejecutar liveness y readiness en paralelo.
  4. Readiness falla → el pod se quita del Service, el tráfico no llega, pero el contenedor sigue vivo.
  5. Liveness falla → el contenedor se mata, Kubernetes crea un nuevo contenedor (mismo pod), el proceso se repite desde el paso 1.

Verifica el estado real de los probes con kubectl describe pod:

kubectl describe pod order-service-7d9f8c6b5-xk2pq
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True

Events:
  Type     Reason     Age   From     Message
  Warning  Unhealthy  2m    kubelet  Readiness probe failed: HTTP probe failed with statuscode: 503

La línea Readiness probe failed indica que readiness está fallando — revisa los logs de la aplicación o llama directamente a /actuator/health/readiness para ver qué detalle está DOWN.

7. Errores comunes

  • Usar el mismo endpoint para liveness y readiness: cuando una dependencia externa (BD, caché) se cae, ambos probes fallan → el pod se reinicia constantemente (crash loop) aunque la aplicación principal esté perfecta. Separa claramente /actuator/health/liveness (solo verifica internals) de /actuator/health/readiness (incluye dependencias).
  • initialDelaySeconds muy pequeño para apps que arrancan lentamente: el probe liveness falla mientras la app aún carga, causando reinicio infinito. Solución: usa startupProbe en lugar de aumentar initialDelaySeconds arbitrariamente.
  • periodSeconds × failureThreshold demasiado grande: cuando hay un problema real, tarda muchos minutos en ser detectado, afectando el SLA.
  • Readiness probe llamando a un servicio downstream no crítico: por ejemplo, llamar a un servicio de logging; si ese servicio cae, todo tu OrderService se considera not-ready injustamente, aunque su funcionalidad principal siga en perfecto estado.

8. Conclusión

Los tres tipos de probe sirven tres propósitos distintos: startupProbe protege la aplicación durante el arranque lento, livenessProbe detecta deadlock para que Kubernetes reinicie automáticamente, y readinessProbe enruta el tráfico según el estado real de las dependencias. El principio más importante: nunca hagas que el liveness probe dependa de sistemas externos — eso convierte un problema downstream en un crash loop de tu propio servicio. Configurar correctamente estos tres probes ayuda al clúster a recuperarse automáticamente sin problemas, reduciendo significativamente el downtime durante despliegues o cuando hay incidentes de infraestructura.

kubernetesspring-bootdevopshealth-checkmicroservices