- 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
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
preStopmelhorou bastante a taxa de HTTP 503SIGTERM, simplificando o processamento da aplicaçãoAo usar
log.Fatal, o conteúdo dentro dedefernão é executadolog.Fatalchamaos.Exite encerra imediatamentepanic, o conteúdo dedeferserá executadoQuando o endpoint
/metricsdo Prometheus é coletado periodicamente, as métricas registradas entre a última coleta e o encerramento do processo podem não ser propagadasSe 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
Falta discussão sobre liveness
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
Depois de atualizar a readiness probe, espere alguns segundos para que o sistema não envie novas requisições
SIGTERM, ainda pode haver uma pequena janela, mas isso não é um grande problema