Você já arrancou o cabo de energia do seu computador por frustração? Embora isso possa parecer uma solução rápida, pode levar à perda de dados e instabilidade do sistema. No mundo do software, existe um conceito semelhante: o desligamento forçado. Esse encerramento abrupto pode causar problemas, assim como sua contraparte física. Felizmente, há uma maneira melhor: o desligamento gracioso.
Ao integrar o desligamento gracioso, fornecemos notificação antecipada ao serviço. Isso permite que ele conclua solicitações em andamento, potencialmente salve informações de estado no disco e, finalmente, evite corrupção de dados durante o desligamento.
Este guia se aprofundará no mundo dos desligamentos suaves, focando especificamente em sua implementação em aplicativos Go executados no Kubernetes.
Uma das principais ferramentas para obter desligamentos harmoniosos em sistemas baseados em Unix é o conceito de sinais, que são, em termos simples, uma maneira simples de comunicar uma coisa específica a um processo, de outro processo. Ao entender como os sinais funcionam, podemos aproveitá-los para implementar procedimentos de encerramento controlados em nossos aplicativos, garantindo um processo de desligamento suave e seguro para os dados.
Existem muitos sinais, e você pode encontrá-los aqui , mas nossa preocupação são apenas os sinais de desligamento:
Esses sinais podem ser enviados pelo usuário (Ctrl+C / Ctrl+D), por outro programa/processo ou pelo próprio sistema (kernel/SO). Por exemplo, um SIGSEGV , também conhecido como falha de segmentação, é enviado pelo SO.
Para explorar o mundo dos desligamentos graciosos em um ambiente prático, vamos criar um serviço simples com o qual podemos experimentar. Este serviço "cobaia" terá um único ponto de extremidade que simula algum trabalho do mundo real (adicionaremos um pequeno atraso) chamando o comando INCR do Redis. Também forneceremos uma configuração básica do Kubernetes para testar como a plataforma lida com sinais de término.
O objetivo final: garantir que nosso serviço lide graciosamente com desligamentos sem perder nenhuma solicitação/dado. Ao comparar o número de solicitações enviadas em paralelo com o valor final do contador no Redis, poderemos verificar se nossa implementação de desligamento gracioso é bem-sucedida.
Não entraremos em detalhes sobre a configuração do cluster Kubernetes e do Redis, mas você pode encontrar a configuração completa em nosso repositório Github .
O processo de verificação é o seguinte:
Vamos começar com nosso servidor HTTP Go base.
desligamento forçado/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") }
Quando executamos nosso procedimento de verificação usando esse código, veremos que algumas solicitações falham e o contador é menor que 1000 (o número pode variar a cada execução).
O que significa claramente que perdemos alguns dados durante a atualização contínua. 😢
Go fornece um pacote de sinais que permite que você manipule sinais Unix. É importante notar que, por padrão, os sinais SIGINT e SIGTERM fazem com que o programa Go saia. E para que nosso aplicativo Go não saia tão abruptamente, precisamos manipular sinais de entrada.
Há duas opções para fazer isso.
Usando canal:
c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM)
Usando contexto (abordagem preferida atualmente):
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()
NotifyContext retorna uma cópia do contexto pai que é marcado como concluído (seu canal Done é fechado) quando um dos sinais listados chega, quando a função stop() retornada é chamada ou quando o canal Done do contexto pai é fechado, o que ocorrer primeiro.
Existem alguns problemas com nossa implementação atual do Servidor HTTP:
Vamos reescrever isso.
desligamento gracioso/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") }
Aqui está o resumo das atualizações:
Agora, se repetirmos nosso processo de verificação, veremos que todas as 1000 solicitações foram processadas corretamente. 🎉
Frameworks como Echo, Gin, Fiber e outros gerarão uma goroutine para cada requisição recebida, dando a ela um contexto e então chamarão sua função/handler dependendo do roteamento que você decidiu. No nosso caso, seria a função anônima dada a HandleFunc para o caminho “/incr”.
Quando você intercepta um sinal SIGTERM e pede para seu framework desligar normalmente, duas coisas importantes acontecem (para simplificar):
Observação: o Kubernetes também para de direcionar o tráfego de entrada do balanceador de carga para seu pod depois que ele é rotulado como Encerrando.
Terminar um processo pode ser complexo, especialmente se houver muitas etapas envolvidas, como fechar conexões. Para garantir que as coisas corram bem, você pode definir um tempo limite. Esse tempo limite atua como uma rede de segurança, encerrando graciosamente o processo se ele demorar mais do que o 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) }
Já que usamos o Kubernetes para implantar nosso serviço, vamos nos aprofundar em como ele encerra os pods. Depois que o Kubernetes decidir encerrar o pod, os seguintes eventos ocorrerão:
Como você pode ver, se você tiver um processo de encerramento longo, pode ser necessário aumentar a configuração terminateGracePeriodSeconds , dando ao seu aplicativo tempo suficiente para encerrar normalmente.
Desligamentos graciosos protegem a integridade dos dados, mantêm uma experiência de usuário perfeita e otimizam o gerenciamento de recursos. Com sua rica biblioteca padrão e ênfase em simultaneidade, o Go capacita os desenvolvedores a integrar sem esforço práticas de desligamento gracioso – uma necessidade para aplicativos implantados em ambientes em contêineres ou orquestrados como o Kubernetes.
Você pode encontrar o código Go e os manifestos do Kubernetes em nosso repositório Github .