Manejo del Graceful Shutdown en Kubernetes para desplegar sin interrupciones

Cuando trabajamos con aplicaciones desplegadas en Kubernetes, uno de los aspectos más críticos es cómo gestionamos el apagado de los pods para evitar interrupciones en el servicio.

Si además trabajamos aplicando CI/CD con decenas de despliegues al día, esto se vuelve más relevante si cabe.

Es bastante habitual encontrarse con la perdida de requests en un servicio que se quedan sin responder sin motivo aparente (been there, done that). Esto se debe a que out-of-the-box Kubernetes no se preocupa por el estado de nuestros pods y simplemente termina unos para arrancar nuevas versiones de los mismos lo más rápido posible.

El graceful shutdown nos permite controlar adecuadamente el proceso de terminación de un Pod, dándole el tiempo necesario para finalizar sus tareas pendientes antes de ser eliminado. De esta manera, garantizamos que no haya pérdida de datos, requests sin contestar ni afectaciones a la experiencia del usuario.

En este post, exploraremos cómo funciona el graceful shutdown en Kubernetes, las señales que intervienen en este proceso y cómo podemos configurar nuestras aplicaciones para realizar un cierre controlado. Además, veremos algunas buenas prácticas para asegurar que los despliegues sean más robustos y estén preparados para escalar sin problemas.

¿Qué es el Graceful Shutdown?

Cuando un Pod en Kubernetes es marcado para su eliminación, el proceso de graceful shutdown se activa para garantizar que la aplicación pueda cerrar de manera ordenada y sin interrupciones abruptas.

El proceso se basa en una secuencia de señales enviadas al contenedor:

  • SIGTERM: Al recibir esta señal, el contenedor inicia el cierre de la aplicación.
    • Aquí es donde debe realizar tareas críticas como completar solicitudes en curso, cerrar conexiones abiertas y liberar recursos.
    • Kubernetes otorga un tiempo de espera configurable (grace period) para que esto ocurra.
  • Grace period: Kubernetes espera un tiempo determinado (por defecto 30 segundos) antes de forzar la terminación del Pod.
    • Durante este tiempo, el Pod sigue funcionando para finalizar sus operaciones pendientes.
  • SIGKILL: Si el Pod no ha terminado después del periodo de gracia, Kubernetes envía la señal SIGKILL, que fuerza la finalización inmediata del contenedor.
    • El objetivo del graceful shutdown es asegurar que el proceso de apagado sea suave y que no haya pérdida de datos ni interrupciones en la disponibilidad del servicio.

Implementando nuestro propio Graceful Shutdown

Deployment en Kubernetes

La forma más común de implementar un graceful shutdown en Kubernetes es directamente en el manifiesto del Deployment.

El hook preStop es un mecanismo en Kubernetes que nos permite ejecutar comandos o scripts personalizados justo antes de que un Pod reciba la señal de SIGTERM en el proceso de graceful shutdown. Este hook es útil cuando necesitamos realizar acciones adicionales antes de iniciar el apagado del contenedor, como notificar a otros servicios, cerrar conexiones externas, o hacer un flush de registros.

Cuando se define un hook preStop, Kubernetes ejecuta la tarea especificada y espera a que termine antes de enviar la señal SIGTERM al contenedor. Esto nos da un control adicional para asegurarnos de que todas las tareas previas al apagado se realicen correctamente.

En resumen, el hook preStop complementa el proceso de graceful shutdown al permitir que ejecutemos tareas previas al cierre del contenedor, mejorando así la robustez y el control durante el apagado.

Veamos un ejemplo de como definir un hook preStop en un Deployment de Kubernetes:

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: my-service
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 5"]

Servidor FastAPI

Si no podemos/queremos tocar manifiestos de Kubernetes algunos frameworks tienen su propia solución, veamos cómo implementar un graceful shutdown en una aplicación FastAPI desplegada en Kubernetes.

FastAPI proporciona un evento de ciclo de vida (lifespan) que nos permite definir acciones específicas, como por ejemplo esperar 5 segundos antes de matar el pod, que se ejecutan cuando la aplicación se inicia y cuando se cierra, lo cual es ideal para gestionar el graceful shutdown.

Podemos, por tanto, utilizar este lifespan para asegurarnos de que nuestra aplicación realiza las tareas necesarias antes de apagarse, como cerrar conexiones a bases de datos, liberar recursos o finalizar trabajos en segundo plano.

Para usar el lifespan en FastAPI, simplemente tenemos que definir una función que maneje el ciclo de vida de la aplicación y asignársela a la aplicación al iniciarla:

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from time import sleep

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator:
    logger.info("Starting FastAPI server...")

    yield

    # Graceful shutdown
    sleep(5)  # wait for the app to finish processing requests
    logger.info("FastAPI server finished!")

app = FastAPI(lifespan=lifespan)

En el bloque de shutdown (parte posterior al yield), podemos realizar cualquier tarea necesaria antes de que la aplicación se apague, lo cual complementa el proceso de graceful shutdown cuando Kubernetes envía la señal SIGTERM. De esta manera, FastAPI puede manejar el apagado de manera controlada, asegurando que todas las conexiones y recursos se cierren adecuadamente.

Conclusiones

En resumen, implementar un graceful shutdown en Kubernetes es esencial para garantizar que nuestras aplicaciones se terminen de manera ordenada y sin afectar la experiencia del usuario. Utilizando herramientas como el hook preStop o el ciclo de vida (lifespan) de FastAPI, podemos controlar de forma precisa cómo se cierran los pods y asegurarnos de que todas las tareas pendientes se completen antes del apagado.

Al aplicar estas prácticas, no solo mejoramos la resiliencia de nuestras aplicaciones, sino que también aseguramos una mayor estabilidad y escalabilidad en nuestros entornos de producción.

!Espero que este post te haya sido de utilidad!