Apagado Controlado y Manejo de Señales
Maneja SIGTERM y SIGINT para que los servidores HTTP completen las solicitudes en curso, los workers en segundo plano se detengan limpiamente y las implementaciones continuas de Kubernetes no descarten conexiones activas.
Los orquestadores envían SIGTERM antes de matar un contenedor.
http.Server.Shutdown deja de aceptar nuevas conexiones y espera las solicitudes activas hasta la fecha límite de un contexto.
Las goroutines en segundo plano necesitan cancelación explícita a través de context.Context o cerrando canales.
golang.org/x/sync/errgroup coordina múltiples listeners y workers que se apagan juntos.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
quit := make ( chan os . Signal , 1 )
signal. Notify (quit, syscall.SIGINT, syscall.SIGTERM)
<- quit
ctx, cancel := context. WithTimeout (context. Background (), 15 * time.Second)
defer cancel ()
_ = server. Shutdown (ctx)
Cuándo usar esto:
Despliegues de Kubernetes con actualizaciones continuas
Eventos de reducción de escala automática (scale-in)
Desarrollo local cuando Ctrl+C no debe corromper escrituras en curso
Servicios con consumidores en segundo plano que deben confirmar offsets antes de salir
Procesos multiserver (HTTP + métricas + gRPC) que se apagan en orden
package main
import (
" context "
" errors "
" log/slog "
" net/http "
" os "
" os/signal "
" syscall "
" time "
" golang.org/x/sync/errgroup "
)
func main () {
ctx, stop := signal. NotifyContext (context. Background (), syscall.SIGINT, syscall.SIGTERM)
defer stop ()
mux := http. NewServeMux ()
mux. HandleFunc ( "GET /work" , func ( w http . ResponseWriter , r * http . Request ) {
select {
case <- time. After ( 2 * time.Second):
w. Write ([] byte ( "done" ))
case <- r. Context (). Done ():
return
}
})
api := & http . Server {Addr: ":8080" , Handler: mux, ReadHeaderTimeout: 5 * time.Second}
admin := & http . Server {Addr: ":9090" , Handler: http. HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w. Write ([] byte ( "metrics" ))
})}
g, gctx := errgroup. WithContext (ctx)
g. Go ( func () error {
slog. Info ( "api listening" , "addr" , api.Addr)
if err := api. ListenAndServe (); err != nil && ! errors. Is (err, http.ErrServerClosed) {
return err
}
return nil
})
g. Go ( func () error {
slog. Info ( "admin listening" , "addr" , admin.Addr)
if err := admin. ListenAndServe (); err != nil && ! errors. Is (err, http.ErrServerClosed) {
return err
}
return nil
})
g. Go ( func () error {
<- gctx. Done ()
slog. Info ( "shutdown started" )
shutdownCtx, cancel := context. WithTimeout (context. Background (), 15 * time.Second)
defer cancel ()
if err := api. Shutdown (shutdownCtx); err != nil {
slog. Error ( "api shutdown" , "err" , err)
}
if err := admin. Shutdown (shutdownCtx); err != nil {
slog. Error ( "admin shutdown" , "err" , err)
}
slog. Info ( "shutdown complete" )
return nil
})
if err := g. Wait (); err != nil {
slog. Error ( "server error" , "err" , err)
os. Exit ( 1 )
}
}
Lo que esto demuestra:
signal.NotifyContext cancela el contexto raíz al recibir SIGTERM
Dos servidores HTTP se apagan con un presupuesto de tiempo compartido
La goroutine de errgroup espera la señal y luego llama a Shutdown en cada servidor
Los manejadores respetan la cancelación de r.Context() durante el vaciado
ListenAndServe se bloquea hasta que Shutdown cierra el listener.
Shutdown marca el servidor como cerrado, cierra las conexiones inactivas y espera los manejadores activos.
Los manejadores deben respetar r.Context().Done() para salir rápidamente cuando los clientes se desconectan.
Después de vaciar el tráfico HTTP, cierre los pools de bases de datos, vacíe los logs y detenga los exportadores de trazas.
Paso Acción 1 Dejar de aceptar nuevo trabajo HTTP/gRPC (Shutdown) 2 Cancelar el contexto del worker para que los consumidores dejen de consultar 3 Esperar la finalización de los manejadores y workers en curso 4 Cerrar pools y clientes de DB 5 Vaciar telemetría (TracerProvider.Shutdown)
Fuente Valor Típico Contexto de apagado de la aplicación 10-30s terminationGracePeriodSeconds de Kubernetes30s por defecto Retraso de desregistro del balanceador de carga Contar en el presupuesto total
// Workers vinculados al contexto de apagado
func runWorker ( ctx context . Context ) error {
for {
select {
case <- ctx. Done ():
return ctx. Err ()
default :
processOneJob (ctx)
}
}
}
Llamar a os.Exit sin Shutdown - Descarta solicitudes activas en el despliegue; siempre vaciar primero.
Tiempo de espera de apagado demasiado corto - Las cargas largas fallan a mitad de transmisión; alinear con la duración de la solicitud P99.
Las goroutines en segundo plano ignoran el contexto - El proceso se cuelga más allá del período de gracia y recibe SIGKILL.
Solo apagar un servidor - Los listeners de métricas o depuración mantienen el proceso vivo inesperadamente.
Bloquear Shutdown en el hilo principal sin una goroutine - No se puede manejar la señal en el mismo hilo si no está estructurado correctamente.
Ignorar http.ErrServerClosed - Normal después de Shutdown; tratar como éxito, no como fallo.
Readiness sigue siendo true durante el apagado - Cambiar readiness a false antes de vaciar para que el LB detenga el tráfico nuevo temprano.
Alternativa Usar Cuando No Usar Cuando signal.NotifyContextServicios modernos de Go 1.16+ Necesitas patrones de canal de señales heredados Canal de señales manual Control detallado sobre las señales NotifyContext más simple es suficienteerrgroupMúltiples servidores o workers Binarios pequeños con un solo ListenAndServe Close en lugar de ShutdownParada forzada inmediata (solo pruebas) Despliegues de producción
¿Cuál es la diferencia entre Shutdown y Close?
Shutdown vacía las solicitudes activas de forma controlada.
Close cierra las conexiones inmediatamente.
¿Cómo dejo de recibir tráfico nuevo de Kubernetes primero?
Establece readiness en false (o implementa una pausa en el hook preStop) antes de llamar a Shutdown para que los endpoints se vacíen.
¿Deben los workers usar el mismo contexto que HTTP?
A menudo sí: cancela un contexto raíz compartido al recibir SIGTERM para que todos los subsistemas se detengan juntos.
¿Qué pasa si Shutdown excede el tiempo de espera?
Registra el trabajo restante, opcionalmente llama a Close en el servidor y sal con un código distinto de cero para observabilidad.
¿Shutdown espera a los WebSockets?
Las conexiones de larga duración necesitan un manejo explícito de cierre en los manejadores; el vaciado por defecto puede no terminarlas rápidamente.
¿Cómo pruebo el apagado controlado?
Inicia el servidor en una goroutine, envíate SIGTERM a ti mismo en la prueba, y verifica que el manejador se complete dentro del tiempo de espera.
¿Qué pasa con gRPC GracefulStop?
Usa grpc.Server.GracefulStop() con una opción de tiempo de espera de respaldo a Stop() para terminación forzada.
¿Debo manejar SIGHUP?
Común para la recarga de configuraciones de rotación de logs; separado del manejo de SIGTERM de despliegue.
¿Puedo anidar llamadas a Shutdown?
Llama una vez por instancia de servidor; las llamadas duplicadas devuelven ErrServerClosed.
¿Cómo ayuda errgroup?
Propaga el primer error y espera a todas las goroutines, útil cuando los servidores de administración y API se ejecutan juntos.
¿Qué logs debo emitir al apagar?
Registra la señal recibida, el inicio del vaciado, la finalización por subsistema y la salida final para la correlación del despliegue.
¿Necesito apagado para herramientas CLI?
Las CLI cortas a menudo solo salen al cancelar el contexto.
Los demonios y servidores de larga ejecución necesitan patrones de vaciado completos.
Versiones de Stack: Esta página fue escrita para Go 1.26.x (predeterminado GC Green Tea, go fix modernizers - verificar parche en build) , chi (última - verificar en build), gin (última - verificar en build), echo (última - verificar en build), google.golang.org/grpc (última - verificar en build), sigs.k8s.io/controller-runtime (última - verificar en build), kubebuilder (última - verificar en build), tinygo (última - verificar objetivos de placa en build), wazero (última - verificar en build), y golangci-lint (última - verificar conjunto de linters en build).