paint-brush
Dominando desligamentos elegantes em Go: um guia abrangente para Kubernetespor@gopher

Dominando desligamentos elegantes em Go: um guia abrangente para Kubernetes

por Alex6m2024/08/14
Read on Terminal Reader

Muito longo; Para ler

Este guia se aprofundará no mundo dos desligamentos suaves, focando especificamente em sua implementação em aplicativos Go executados no Kubernetes.
featured image - Dominando desligamentos elegantes em Go: um guia abrangente para Kubernetes
Alex HackerNoon profile picture

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.

Sinais em sistemas Unix

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:

  • SIGTERM - enviado a um processo para solicitar seu término. Mais comumente usado, e vamos nos concentrar nele mais tarde.
  • SIGKILL - “sair imediatamente”, não pode ser interferido.
  • SIGINT - sinal de interrupção (como Ctrl+C)
  • SIGQUIT - sinal de saída (como Ctrl+D)


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.


Nosso serviço de porquinhos da índia

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:

  1. Implante o aplicativo Redis e Go no Kubernetes.
  2. Use vegeta para enviar 1000 solicitações (25/s em 40 segundos).
  3. Enquanto o vegeta estiver em execução, inicialize uma atualização contínua do Kubernetes atualizando a tag de imagem.
  4. Conecte-se ao Redis para verificar o “contador”, ele deve ser 1000.


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. 😢

Manipulando Sinais em Go

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:

  1. Temos uma goroutine processRequest lenta e, como não lidamos com o sinal de término, o programa sai automaticamente, o que significa que todas as goroutines em execução também são encerradas.
  2. O programa não fecha nenhuma conexão.


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:

  • Adicionado signal.NotifyContext para escutar o sinal de término SIGTERM.
  • Foi introduzido um sync.WaitGroup para rastrear solicitações em andamento (processRequest goroutines).
  • Encapsulei o servidor em uma goroutine e usei server.Shutdown com contexto para parar de aceitar novas conexões.
  • Use wg.Wait() para garantir que todas as solicitações em andamento (goroutines processRequest) sejam concluídas antes de prosseguir.
  • Limpeza de recursos: adicionado redisdb.Close() para fechar corretamente a conexão Redis antes de sair.
  • Saída Limpa: Usado os.Exit(0) para indicar um término bem-sucedido.

Agora, se repetirmos nosso processo de verificação, veremos que todas as 1000 solicitações foram processadas corretamente. 🎉


Estruturas da Web / Biblioteca HTTP

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):

  • Sua estrutura para de aceitar solicitações de entrada
  • Ele aguarda a conclusão de quaisquer solicitações de entrada existentes (implicitamente aguardando o término das goroutines).


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.

Opcional: Tempo limite de desligamento

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) }

Ciclo de vida de término do Kubernetes

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:

  1. O pod é definido para o estado “Terminando” e removido da lista de endpoints de todos os serviços.
  2. O gancho preStop é executado se definido.
  3. O sinal SIGTERM é enviado para o pod. Mas ei, agora nosso aplicativo sabe o que fazer!
  4. O Kubernetes aguarda um período de carência ( terminateGracePeriodSeconds ), que é de 30 segundos por padrão.
  5. O sinal SIGKILL é enviado ao pod, e o pod é removido.

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.

Conclusão

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 .

Recursos