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 executados no Kubernetes. aplicativos Go 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 , mas nossa preocupação são apenas os sinais de desligamento: aqui - enviado a um processo para solicitar seu término. Mais comumente usado, e vamos nos concentrar nele mais tarde. SIGTERM - “sair imediatamente”, não pode ser interferido. SIGKILL - sinal de interrupção (como Ctrl+C) SIGINT - sinal de saída (como Ctrl+D) SIGQUIT 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 , também conhecido como falha de segmentação, é enviado pelo SO. SIGSEGV 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 do Redis. Também forneceremos uma configuração básica do Kubernetes para testar como a plataforma lida com sinais de término. INCR 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: Implante o aplicativo Redis e Go no Kubernetes. Use para enviar 1000 solicitações (25/s em 40 segundos). vegeta Enquanto o vegeta estiver em execução, inicialize uma do Kubernetes atualizando a tag de imagem. atualização contínua 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 (o número pode variar a cada execução). contador é menor que 1000 O que significa claramente que perdemos alguns dados durante a atualização contínua. 😢 Manipulando Sinais em Go Go fornece um pacote 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. de sinais 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() 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 retornada é chamada ou quando o canal Done do contexto pai é fechado, o que ocorrer primeiro. NotifyContext stop() Existem alguns problemas com nossa implementação atual do Servidor HTTP: 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. 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 para escutar o sinal de término SIGTERM. signal.NotifyContext Foi introduzido um para rastrear solicitações em andamento (processRequest goroutines). sync.WaitGroup Encapsulei o servidor em uma goroutine e usei com contexto para parar de aceitar novas conexões. server.Shutdown Use para garantir que todas as solicitações em andamento (goroutines processRequest) sejam concluídas antes de prosseguir. wg.Wait() Limpeza de recursos: adicionado para fechar corretamente a conexão Redis antes de sair. redisdb.Close() Saída Limpa: Usado para indicar um término bem-sucedido. os.Exit(0) 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 e pede para seu framework desligar normalmente, duas coisas importantes acontecem (para simplificar): SIGTERM 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: O pod é definido para o estado “Terminando” e removido da lista de endpoints de todos os serviços. O gancho é executado se definido. preStop O sinal é enviado para o pod. Mas ei, agora nosso aplicativo sabe o que fazer! SIGTERM O Kubernetes aguarda um período de carência ( ), que é de 30 segundos por padrão. terminateGracePeriodSeconds O sinal é enviado ao pod, e o pod é removido. SIGKILL Como você pode ver, se você tiver um processo de encerramento longo, pode ser necessário aumentar a configuração , dando ao seu aplicativo tempo suficiente para encerrar normalmente. terminateGracePeriodSeconds 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 pacote os/signal Ciclo de vida do pod do Kubernetes