3 pontos por GN⁺ 2025-05-06 | 1 comentários | Compartilhar no WhatsApp
  • Encerramento gracioso (graceful shutdown) consiste em um procedimento no qual, após receber um sinal de encerramento, a aplicação bloqueia novas requisições, conclui as requisições em andamento e limpa os recursos
  • Em Go, é possível usar o pacote os/signal para tratar diretamente sinais de encerramento como SIGINT e SIGTERM, e também usar signal.NotifyContext para controle de encerramento baseado em context
  • Ao encerrar um servidor HTTP, é mais estável bloquear o tráfego fazendo a readiness probe falhar antes de chamar Server.Shutdown() e, após aguardar alguns segundos, executar o shutdown
  • Todos os handlers devem ser capazes de detectar o sinal de encerramento via context e finalizar, e isso pode ser tratado de forma unificada com BaseContext ou middleware
  • Após receber o sinal de encerramento, recursos externos como banco de dados, message broker e cache devem ser limpos intencionalmente, e registrá-los com defer facilita o gerenciamento da ordem de encerramento

O que é Graceful Shutdown?

  • O encerramento gracioso é o processo pelo qual, ao finalizar uma aplicação, ela passa por bloqueio de novas requisições, espera pela conclusão das requisições em andamento e limpeza de recursos
  • Este artigo trata principalmente de servidores HTTP e ambientes com contêineres, mas é um conceito aplicável a qualquer aplicação

1. Tratamento de sinais de encerramento

  • Em sistemas do tipo Unix, SIGTERM, SIGINT e SIGHUP são usados como sinais de encerramento
  • O runtime do Go encerra a aplicação por padrão ao receber SIGTERM ou SIGINT, mas é possível tratá-los diretamente com os/signal.Notify
  • Usar um canal com buffer (capacidade 1) ajuda a evitar perda de sinal durante a inicialização
  • A partir do Go 1.16, signal.NotifyContext tornou o controle de sinais baseado em context mais simples

2. Considerar o tempo de encerramento

  • No Kubernetes, por padrão há um período de tolerância de 30 segundos para encerramento (terminationGracePeriodSeconds)
  • Para encerrar com segurança, é recomendável reservar 20% de margem e concluir o processo de encerramento em até 25 segundos

3. Parar de receber novas requisições

  • http.Server.Shutdown() bloqueia novas conexões e espera até que as requisições existentes sejam concluídas
  • Em ambientes Kubernetes, primeiro faz-se a readiness probe falhar para bloquear a entrada de tráfego e, após uma pequena espera, executa-se o shutdown
  • No handler de readiness, é possível usar uma variável global para verificar o estado de encerramento e retornar HTTP 503

4. Finalizar o processamento das requisições

  • É necessário definir um timeout adequado para o context de encerramento (context.WithTimeout)
  • Se o shutdown context expirar, as conexões restantes serão encerradas à força
  • Todos os handlers devem ser projetados para usar context.Context, de modo a detectar o sinal de encerramento e poder interromper a execução
  • Para isso, é possível injetar o contexto de encerramento em todas as requisições por meio de middleware ou BaseContext

5. Limpeza de recursos

  • Fechar recursos imediatamente após receber o sinal de encerramento pode causar problemas nos handlers ainda em execução
  • Somente após a conclusão do shutdown devem ser limpos conexões com banco de dados, message brokers, caches etc.
  • Com defer em Go, é possível executar a rotina de encerramento na ordem inversa da inicialização, facilitando o gerenciamento de dependências
  • Além de recursos como memória e file descriptors que o SO limpa automaticamente, também existem recursos que exigem encerramento explícito, como flush de dados e rollback de transações

Resumo do exemplo completo

  • Recebimento do sinal de encerramento com signal.NotifyContext
  • Implementação do endpoint de readiness /healthz
  • Injeção do contexto de encerramento em todas as requisições com BaseContext
  • Execução do shutdown após aguardar 5 segundos da readiness
  • Inclui fallback de encerramento forçado caso a chamada a server.Shutdown falhe

Referências e recursos relacionados

1 comentários

 
GN⁺ 2025-05-06
Comentários do Hacker News
  • No Kubernetes, às vezes a atualização do IP de destino no load balancer demora. Em 90% dos casos, o problema é garantir que o tráfego esteja realmente sendo drenado

    • Adicionar um tempo de espera de 15 segundos ao hook global preStop melhorou bastante a taxa de HTTP 503
    • Isso cria um intervalo entre o cancelamento do registro no load balancer e a entrega do SIGTERM, simplificando o processamento da aplicação
  • Ao usar log.Fatal, o conteúdo dentro de defer não é executado

    • log.Fatal chama os.Exit e encerra imediatamente
    • Se usar panic, o conteúdo de defer será executado
  • Quando o endpoint /metrics do Prometheus é coletado periodicamente, as métricas registradas entre a última coleta e o encerramento do processo podem não ser propagadas

    • É possível perder os logs dos últimos segundos ao encerrar o serviço
    • Quando um arquivo de log é monitorado por um processo sidecar, pode ocorrer uma condição de corrida
  • Se um sistema distribuído depende do encerramento correto do cliente, ele pode falhar gravemente

  • Falta explicação sobre como reiniciar a aplicação sem derrubar conexões quando uma nova instância do serviço recebe sockets da instância anterior

    • No systemd, isso é relativamente simples de implementar
    • O nginx oferece suporte a isso há mais de 20 anos
    • Kubernetes e Docker não oferecem suporte a isso
  • Falta discussão sobre liveness

    • Já vi vários apps usando o mesmo endpoint para liveness e readiness
  • Se um programa não consegue lidar de forma limpa com comandos como ctrl c, ele foi mal escrito

  • Elixir projeta processos como pequenos processos de VM, evitando a necessidade de criar intencionalmente rotinas de encerramento gracioso

  • Foi criada uma pequena biblioteca no projeto para lidar com encerramento gracioso

    • Ela fornece uma API para integrar serviços com diferentes mecanismos de inicialização e encerramento
  • Depois de atualizar a readiness probe, espere alguns segundos para que o sistema não envie novas requisições

    • Um pod em encerramento não está pronto
    • O serviço marca o endpoint como em encerramento
    • Mesmo após SIGTERM, ainda pode haver uma pequena janela, mas isso não é um grande problema
    • O importante é não aceitar novas conexões e encerrar normalmente as conexões existentes