paint-brush
Cómo dominar los apagados elegantes en Go: una guía completa para Kubernetespor@gopher

Cómo dominar los apagados elegantes en Go: una guía completa para Kubernetes

por Alex6m2024/08/14
Read on Terminal Reader

Demasiado Largo; Para Leer

Esta guía profundizará en el mundo de los apagados elegantes, centrándose específicamente en su implementación en aplicaciones Go que se ejecutan en Kubernetes.
featured image - Cómo dominar los apagados elegantes en Go: una guía completa para Kubernetes
Alex HackerNoon profile picture

¿Alguna vez ha desenchufado el cable de alimentación de su ordenador por frustración? Aunque parezca una solución rápida, puede provocar la pérdida de datos y la inestabilidad del sistema. En el mundo del software, existe un concepto similar: el apagado brusco. Esta terminación abrupta puede causar problemas al igual que su contraparte física. Afortunadamente, existe una forma mejor: el apagado elegante.


Al integrar el apagado ordenado, proporcionamos una notificación anticipada al servicio, lo que le permite completar solicitudes en curso, potencialmente guardar información de estado en el disco y, en última instancia, evitar la corrupción de datos durante el apagado.


Esta guía profundizará en el mundo de los apagados elegantes, centrándose específicamente en su implementación en aplicaciones Go que se ejecutan en Kubernetes.

Señales en sistemas Unix

Una de las herramientas clave para lograr apagados elegantes en sistemas basados en Unix es el concepto de señales, que son, en términos simples, una forma sencilla de comunicar una cosa específica a un proceso desde otro proceso. Al comprender cómo funcionan las señales, podemos aprovecharlas para implementar procedimientos de finalización controlados dentro de nuestras aplicaciones, lo que garantiza un proceso de apagado sin problemas y seguro para los datos.


Hay muchas señales, y puedes encontrarlas aquí , pero nos interesan solo las señales de apagado:

  • SIGTERM : se envía a un proceso para solicitar su finalización. Es el más utilizado y nos centraremos en él más adelante.
  • SIGKILL - “salir inmediatamente”, no se puede interferir con ello.
  • SIGINT - señal de interrupción (como Ctrl+C)
  • SIGQUIT - señal de salida (como Ctrl+D)


Estas señales pueden ser enviadas desde el usuario (Ctrl+C / Ctrl+D), desde otro programa/proceso o desde el propio sistema (kernel / SO), por ejemplo, un SIGSEGV , también conocido como fallo de segmentación, es enviado por el SO.


Nuestro servicio para conejillos de indias

Para explorar el mundo de los apagados elegantes en un entorno práctico, creemos un servicio simple con el que podamos experimentar. Este servicio "conejillo de indias" tendrá un único punto final que simula un trabajo del mundo real (agregaremos un pequeño retraso) al llamar al comando INCR de Redis. También proporcionaremos una configuración básica de Kubernetes para probar cómo la plataforma maneja las señales de finalización.


El objetivo final: garantizar que nuestro servicio gestione los apagados correctamente sin perder ninguna solicitud o dato. Al comparar la cantidad de solicitudes enviadas en paralelo con el valor del contador final en Redis, podremos verificar si nuestra implementación de apagado correcto es exitosa.

No entraremos en detalles sobre la configuración del clúster de Kubernetes y Redis, pero puedes encontrar la configuración completa en nuestro repositorio de Github .


El proceso de verificación es el siguiente:

  1. Implementar aplicaciones Redis y Go en Kubernetes.
  2. Utilice vegeta para enviar 1000 solicitudes (25/s durante 40 segundos).
  3. Mientras Vegeta se esté ejecutando, inicialice una actualización continua de Kubernetes actualizando la etiqueta de imagen.
  4. Conéctese a Redis para verificar el “contador”, debe ser 1000.


Comencemos con nuestro servidor HTTP Go base.

apagado forzado/main.go

 package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }

Cuando ejecutamos nuestro procedimiento de verificación usando este código, veremos que algunas solicitudes fallan y el contador es menor que 1000 (el número puede variar en cada ejecución).


Lo que claramente significa que perdimos algunos datos durante la actualización continua. 😢

Manejo de señales en Go

Go proporciona un paquete de señales que permite manejar señales Unix. Es importante tener en cuenta que, de forma predeterminada, las señales SIGINT y SIGTERM hacen que el programa Go salga. Y para que nuestra aplicación Go no salga tan abruptamente, necesitamos manejar las señales entrantes.

Hay dos opciones para hacerlo.


Usando el canal:

 c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM)


Utilizando el contexto (el enfoque preferido hoy en día):

 ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()


NotifyContext devuelve una copia del contexto padre que está marcado como terminado (su canal Done está cerrado) cuando llega una de las señales enumeradas, cuando se llama a la función stop() devuelta o cuando se cierra el canal Done del contexto padre, lo que ocurra primero.


Hay algunos problemas con nuestra implementación actual del servidor HTTP:

  1. Tenemos una goroutine de proceso lento, y como no manejamos la señal de terminación, el programa sale automáticamente, lo que significa que todas las goroutines en ejecución también finalizan.
  2. El programa no cierra ninguna conexión.


Vamos a reescribirlo.


apagado elegante/main.go

 package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }


Aquí está el resumen de las actualizaciones:

  • Se agregó signal.NotifyContext para escuchar la señal de terminación SIGTERM.
  • Se introdujo un sync.WaitGroup para rastrear solicitudes en vuelo (goroutines processRequest).
  • Envolví el servidor en una goroutine y usé server.Shutdown con contexto para dejar de aceptar nuevas conexiones sin problemas.
  • Se utilizó wg.Wait() para garantizar que todas las solicitudes en vuelo (goroutines processRequest) finalicen antes de continuar.
  • Limpieza de recursos: se agregó redisdb.Close() para cerrar correctamente la conexión Redis antes de salir.
  • Salida limpia: se utiliza os.Exit(0) para indicar una finalización exitosa.

Ahora, si repetimos nuestro proceso de verificación veremos que las 1000 solicitudes se procesan correctamente. 🎉


Marcos web / Biblioteca HTTP

Los frameworks como Echo, Gin, Fiber y otros generarán una goroutine para cada solicitud entrante, dándole un contexto y luego llamarán a su función/controlador según el enrutamiento que haya decidido. En nuestro caso, sería la función anónima dada a HandleFunc para la ruta “/incr”.


Cuando interceptas una señal SIGTERM y le pides a tu marco que se apague correctamente, suceden dos cosas importantes (para simplificar):

  • Su marco deja de aceptar solicitudes entrantes
  • Espera a que finalicen todas las solicitudes entrantes existentes (implícitamente espera a que finalicen las goroutines).


Nota: Kubernetes también deja de dirigir el tráfico entrante desde el balanceador de carga a su pod una vez que lo ha etiquetado como Terminando.

Opcional: Tiempo de espera de apagado

Terminar un proceso puede ser complejo, especialmente si implica muchos pasos, como cerrar conexiones. Para garantizar que todo funcione sin problemas, puede establecer un tiempo de espera. Este tiempo de espera actúa como una red de seguridad, ya que permite salir del proceso sin problemas si tarda más de lo esperado.


 shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) }

Ciclo de vida de terminación de Kubernetes

Dado que usamos Kubernetes para implementar nuestro servicio, analicemos en profundidad cómo finaliza los pods. Una vez que Kubernetes decide finalizar el pod, se producirán los siguientes eventos:

  1. El pod se establece en el estado “Terminando” y se elimina de la lista de puntos finales de todos los servicios.
  2. El gancho preStop se ejecuta si está definido.
  3. Se envía la señal SIGTERM al pod. ¡Pero ahora nuestra aplicación sabe qué hacer!
  4. Kubernetes espera un período de gracia ( terminationGracePeriodSeconds ), que es de 30 segundos de manera predeterminada.
  5. Se envía la señal SIGKILL al pod y este se retira.

Como puede ver, si tiene un proceso de finalización de larga duración, puede ser necesario aumentar la configuración endingGracePeriodSeconds , lo que le permitirá a su aplicación tener tiempo suficiente para cerrarse correctamente.

Conclusión

Los apagados elegantes protegen la integridad de los datos, mantienen una experiencia de usuario fluida y optimizan la gestión de recursos. Con su rica biblioteca estándar y el énfasis en la concurrencia, Go permite a los desarrolladores integrar sin esfuerzo prácticas de apagado elegante, una necesidad para las aplicaciones implementadas en entornos orquestados o en contenedores como Kubernetes.

Puede encontrar el código Go y los manifiestos de Kubernetes en nuestro repositorio de Github .

Recursos