3 puntos por GN⁺ 2025-05-06 | 1 comentarios | Compartir por WhatsApp
  • El apagado ordenado (graceful shutdown) consiste en un proceso en el que la aplicación, tras recibir una señal de terminación, bloquea nuevas solicitudes, finaliza las solicitudes en curso y limpia los recursos
  • En Go se pueden manejar directamente señales de terminación como SIGINT y SIGTERM usando el paquete os/signal, y también es posible un control de apagado basado en context con signal.NotifyContext
  • Al cerrar un servidor HTTP, es más estable bloquear el tráfico haciendo fallar primero el readiness probe antes de llamar a Server.Shutdown(), esperar unos segundos y luego ejecutar el shutdown
  • Todos los handlers deben poder detectar la señal de terminación del context y finalizar, y esto puede manejarse de forma unificada mediante BaseContext o middleware
  • Tras recibir la señal de terminación, los recursos externos como bases de datos, brokers de mensajes y cachés deben limpiarse de forma intencional, y registrarlos con defer facilita gestionar el orden de apagado

¿Qué es Graceful Shutdown?

  • El apagado ordenado es el proceso por el que, al cerrarse una aplicación, se pasa por bloquear nuevas solicitudes, esperar a que terminen las solicitudes en curso y limpiar los recursos
  • Este artículo se centra sobre todo en servidores HTTP y entornos con contenedores, pero es un concepto aplicable a cualquier aplicación

1. Manejo de señales de terminación

  • En sistemas tipo Unix, SIGTERM, SIGINT y SIGHUP se usan como señales de terminación
  • El runtime de Go finaliza la aplicación por defecto al recibir SIGTERM o SIGINT, pero se puede manejar directamente con os/signal.Notify
  • Usar un canal con buffer (capacidad 1) ayuda a evitar la pérdida de señales durante la inicialización
  • Desde Go 1.16, signal.NotifyContext facilita el control de señales basado en context

2. Tener en cuenta el tiempo de terminación

  • En Kubernetes, por defecto se otorga un período de gracia de 30 segundos para la terminación (terminationGracePeriodSeconds)
  • Para terminar de forma segura, conviene dejar un margen del 20% y completar el apagado dentro de 25 segundos

3. Dejar de aceptar nuevas solicitudes

  • http.Server.Shutdown() bloquea nuevas conexiones y espera a que terminen las solicitudes existentes
  • En entornos Kubernetes, primero se hace fallar el readiness probe para bloquear la entrada de tráfico, luego se espera un poco y después se ejecuta el shutdown
  • En el handler de readiness, se puede evaluar el estado de terminación con una variable global para devolver HTTP 503

4. Finalizar el procesamiento de solicitudes

  • Es necesario configurar un timeout adecuado en el context de terminación (context.WithTimeout)
  • Si el shutdown context expira, las conexiones restantes se cierran de forma forzada
  • Todos los handlers deben diseñarse para que, usando context.Context, puedan detectar la señal de terminación e interrumpirse
  • Para ello, se puede inyectar el context de terminación en todas las solicitudes mediante middleware o BaseContext

5. Limpieza de recursos

  • Si se cierran los recursos apenas llega la señal de terminación, pueden producirse problemas en los handlers que aún están procesando
  • Una vez completado el shutdown, se deben limpiar conexiones a bases de datos, brokers de mensajes, cachés, etc.
  • Con defer en Go, es posible ejecutar las rutinas de apagado en orden inverso a la inicialización, lo que facilita la gestión de dependencias
  • Además de recursos que el SO limpia automáticamente, como memoria o descriptores de archivo, también hay recursos que requieren cierre explícito, como flush de datos o rollback de transacciones

Resumen del ejemplo completo

  • Recepción de señales de terminación con signal.NotifyContext
  • Implementación del endpoint de readiness /healthz
  • Inyección del context de terminación en todas las solicitudes con BaseContext
  • Ejecución del shutdown tras esperar 5 segundos por readiness
  • Incluye fallback de cierre forzado si falla la llamada a server.Shutdown

Referencias y recursos relacionados

1 comentarios

 
GN⁺ 2025-05-06
Opiniones de Hacker News
  • En Kubernetes, a veces la actualización de la IP objetivo del balanceador de carga tarda mucho. El 90% del problema es verificar si el tráfico realmente se está drenando

    • Agregar una espera de 15 segundos al hook global preStop mejoró mucho la tasa de HTTP 503
    • Esto crea tiempo entre la baja del registro en el balanceador de carga y la entrega de SIGTERM, lo que simplifica el manejo en la aplicación
  • Al usar log.Fatal, no se ejecuta el contenido dentro de defer

    • log.Fatal llama a os.Exit y termina inmediatamente
    • Si se usa panic, sí se ejecuta el contenido de defer
  • Cuando el endpoint /metrics de Prometheus se scrapea periódicamente, puede que las métricas registradas entre el último scrape y el fin del proceso no se propaguen

    • Al apagar el servicio, se pueden perder los logs de los últimos segundos
    • Puede haber una condición de carrera cuando un proceso sidecar está monitoreando el archivo de logs
  • Si un sistema distribuido depende del apagado ordenado del cliente, el sistema puede fallar gravemente

  • Falta una explicación sobre cómo reiniciar una aplicación sin cortar conexiones cuando una nueva instancia del servicio recibe sockets de la instancia anterior

    • En systemd, implementarlo es relativamente sencillo
    • nginx soporta esto desde hace más de 20 años
    • Kubernetes y Docker no lo soportan
  • Falta discusión sobre liveness

    • Se ha visto muchas veces apps que usan el mismo endpoint para liveness/readiness
  • Si un programa no puede manejar limpiamente comandos como ctrl c, está mal escrito

  • Elixir diseña los procesos como pequeños procesos de VM, por lo que no hace falta crear intencionalmente rutinas de apagado ordenado

  • Se creó una pequeña librería en un proyecto para manejar el apagado ordenado

    • Proporciona una API para integrar servicios con distintos mecanismos de inicio y apagado
  • Después de actualizar el readiness probe, hay que esperar unos segundos para que el sistema deje de enviar nuevas solicitudes

    • Un pod que se está apagando no está listo
    • El servicio marca los endpoints como en terminación
    • Incluso después de SIGTERM puede haber una pequeña ventana, pero no es un gran problema
    • Lo importante es no aceptar nuevas conexiones y cerrar ordenadamente las conexiones existentes