Email copiado — support@tuurt.com
Cargando experiencia
kubernetes · 18 de agosto de 2026 · 6 min

Readiness y liveness probes en Kubernetes: por qué un pod sano puede seguir recibiendo tráfico roto

Sin readinessProbe y sin startupProbe, un pod puede aparecer sano y seguir causando 502 durante cada rolling deploy. Repasamos las tres probes, sus valores por defecto y cómo dimensionarlas para arranques lentos en JVM y .NET.

Por Equipo Tuurt

Readiness y liveness probes en Kubernetes: por qué un pod sano puede seguir recibiendo tráfico roto

Un rolling deploy termina, kubectl get pods muestra todo en Running, el dashboard de Kubernetes está en verde, y aun así hay una ventana de varios segundos en la que algunos clientes reciben 502 o connection refused. El diagnóstico casi siempre es el mismo: el pod está vivo, pero no estaba listo cuando el Service ya le mandaba tráfico. Es la diferencia entre dos preguntas que Kubernetes puede responder por separado y que casi ningún manifiesto por defecto distingue bien.

Tres probes, tres preguntas distintas

Kubernetes no tiene un único chequeo de salud, tiene tres, y cada uno responde algo distinto:

  • liveness: ¿el proceso sigue funcionando o hay que matarlo y reiniciarlo? Si falla, el kubelet reinicia el contenedor.
  • readiness: ¿puede este pod recibir tráfico ahora mismo? Si falla, el pod se saca de los endpoints del Service, pero no se reinicia nada.
  • startup: ¿ya terminó el arranque? Mientras no pase, liveness y readiness no se evalúan.

El error más común que vemos en manifiestos existentes no es no tener probes, es tener solo livenessProbe y asumir que eso también protege el tráfico entrante. No lo hace. Sin readinessProbe, Kubernetes considera que un contenedor está listo apenas pasa a estado Running, que es una señal del runtime del contenedor, no de la aplicación. Un proceso Java puede estar "Running" mientras la JVM todavía está cargando clases y el pool de conexiones a la base de datos ni siquiera se ha inicializado. Durante esa ventana, si el Service ya lo tiene como endpoint, cualquier request que le llegue falla.

El caso típico: arranque lento en JVM y .NET

Este problema se nota más en runtimes con arranque no trivial. Una aplicación Spring Boot típica abre el puerto HTTP antes de que todos los @Bean terminen de inicializarse si el orden de arranque no está pensado para eso. Un servicio .NET con Kestrel puede aceptar conexiones TCP en el puerto configurado antes de que el IHostedService que calienta la caché o abre el pool de conexiones haya terminado. En ambos casos, el puerto está abierto —el pod parece listo desde fuera— pero la aplicación todavía no puede atender una request real sin fallar o colgarse.

# Configuración incompleta: sin readinessProbe, el Service manda
# tráfico apenas el puerto responde, no cuando la app está lista.
containers:
  - name: api
    image: registro/mi-api:1.4.0
    ports:
      - containerPort: 8080
    livenessProbe:
      httpGet:
        path: /health
        port: 8080

Aquí no hay nada roto en el YAML. Simplemente falta la pregunta correcta.

Por qué la configuración por defecto tampoco alcanza

Cuando sí se añade un readinessProbe, el segundo problema es dejar los valores por defecto de Kubernetes sin ajustar. Los valores por defecto de una probe son initialDelaySeconds: 0, periodSeconds: 10, timeoutSeconds: 1, successThreshold: 1 y failureThreshold: 3. Eso significa que el primer chequeo ocurre apenas el contenedor arranca, y con tres fallos seguidos —tres intentos, treinta segundos— el kubelet ya está actuando sobre esa probe.

Para una readinessProbe sola, treinta segundos fallando solo mantiene al pod fuera del Service, lo cual es correcto: mejor sin tráfico que con tráfico roto. El problema real aparece cuando esos mismos valores por defecto se copian a la livenessProbe de un servicio con arranque lento. Si la aplicación tarda cuarenta segundos en estar realmente operativa y la livenessProbe empieza a fallar contra el mismo endpoint desde el segundo cero, a los treinta segundos el kubelet reinicia el contenedor. Vuelve a arrancar, vuelve a tardar cuarenta segundos, vuelve a fallar la probe, vuelve a reiniciarse. Eso es un CrashLoopBackOff que no lo causa ningún bug de la aplicación: lo causa una probe mal calibrada matando un proceso que solo necesitaba más tiempo.

startupProbe: la pieza que falta

La solución no es alargar initialDelaySeconds de la livenessProbe a un número generoso y cruzar los dedos. Eso funciona hasta que alguien cambia el tiempo de arranque típico y nadie vuelve a tocar ese valor. La solución es usar startupProbe, que existe exactamente para este caso: mientras la startupProbe no tenga éxito, ni livenessProbe ni readinessProbe se evalúan. Una vez que la startupProbe pasa, deja de ejecutarse y las otras dos toman el control con sus propios thresholds, que ahora pueden ser estrictos porque ya no tienen que cubrir el arranque.

containers:
  - name: api
    image: registro/mi-api:1.4.0
    ports:
      - containerPort: 8080
    startupProbe:
      httpGet:
        path: /health/startup
        port: 8080
      periodSeconds: 5
      failureThreshold: 24        # hasta 120s de margen para arrancar
    readinessProbe:
      httpGet:
        path: /health/ready
        port: 8080
      periodSeconds: 5
      failureThreshold: 2         # 10s sin tráfico si algo se degrada
    livenessProbe:
      httpGet:
        path: /health/live
        port: 8080
      periodSeconds: 10
      failureThreshold: 3         # estricto: ya pasó el arranque

Con esta configuración, un arranque lento tiene hasta 120 segundos para completarse sin que nada lo mate, pero una vez arrancado, la livenessProbe reacciona en 30 segundos a un proceso realmente colgado, y la readinessProbe saca el pod del tráfico en 10 segundos si una dependencia empieza a fallar.

Dimensionar los thresholds sin adivinar

El número de failureThreshold para la startupProbe no sale de una tabla genérica, sale de medir el arranque real de esa aplicación bajo carga comparable a producción, con logs de arranque en mano, y dejar un margen razonable sobre el peor caso observado, no sobre el caso típico. Un servicio que arranca en 15 segundos en el portátil del desarrollador y en 45 segundos en el clúster con los límites de CPU reales del pod es la razón por la que ese margen importa: resources.requests.cpu bajo en un contenedor con arranque pesado alarga el tiempo de inicialización porque el kubelet limita cuánta CPU puede usar el proceso, y eso hace que el mismo binario tarde distinto según el clúster.

El error de compartir el mismo endpoint

El segundo anti-patrón, además de omitir la startupProbe, es apuntar readinessProbe y livenessProbe al mismo endpoint con la misma lógica interna. Si ese endpoint verifica la conexión a la base de datos y la base de datos tiene una interrupción, las dos probes fallan a la vez: el pod sale del Service (correcto) y además el kubelet empieza a reiniciar el contenedor (incorrecto, porque reiniciar el proceso no arregla la base de datos caída). El resultado es un conjunto de pods en bucle de reinicio que no resuelve el problema real y que además genera ruido en los logs que dificulta encontrar la causa.

La separación correcta es que livenessProbe verifique solo que el proceso interno responde —sin tocar dependencias externas— y que readinessProbe sí verifique las dependencias que la aplicación necesita para atender tráfico. Un proceso puede estar perfectamente vivo y aun así no estar listo porque su base de datos no responde; ese pod no necesita un reinicio, necesita salir del Service hasta que la dependencia vuelva.

Lo que esto no resuelve

Ajustar bien las tres probes evita que un pod reciba tráfico antes de estar listo o que se reinicie en bucle por un arranque lento, pero no cubre el otro extremo del ciclo de vida: el apagado. Cuando Kubernetes decide terminar un pod, puede seguir enviándole tráfico durante una ventana breve mientras el endpoint se propaga, y si el contenedor no maneja SIGTERM con un preStop hook o un terminationGracePeriodSeconds suficiente para drenar conexiones en curso, esa ventana produce el mismo tipo de error que intentamos evitar con las probes de arranque, solo que en el otro extremo. Es un problema relacionado pero distinto, y vale la pena tratarlo por separado en vez de intentar resolverlo también desde readinessProbe.

kubernetes devops probes cloud
← Volver al blog