- 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
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
preStopmejoró mucho la tasa de HTTP 503SIGTERM, lo que simplifica el manejo en la aplicaciónAl usar
log.Fatal, no se ejecuta el contenido dentro dedeferlog.Fatalllama aos.Exity termina inmediatamentepanic, sí se ejecuta el contenido dedeferCuando el endpoint
/metricsde Prometheus se scrapea periódicamente, puede que las métricas registradas entre el último scrape y el fin del proceso no se propaguenSi 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
Falta discusión sobre liveness
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
Después de actualizar el readiness probe, hay que esperar unos segundos para que el sistema deje de enviar nuevas solicitudes
SIGTERMpuede haber una pequeña ventana, pero no es un gran problema