Graceful Shutdown e Signal Handling em Go: Encerramento Seguro em Produção
Como encerrar um serviço Go com segurança em produção: tratar sinais do sistema, drenar requisições e fechar workers e conexões.
Publicada em · 5 min de leitura
Graceful Shutdown e Signal Handling em Go: Encerramento Seguro em Produção
Quando um microsserviço é encerrado em ambientes conteinerizados (Kubernetes, AWS ECS, Docker Swarm ou Google Cloud Run) durante um novo deploy, auto-scaling de réplicas ou manutenção de nós, o orquestrador envia um sinal do sistema operacional solicitando o desligamento do processo.
Se a aplicação Go não interceptar esse sinal e morrer abruptamente, o impacto é imediato:
- Conexões HTTP ativas de clientes são interrompidas com erros
502 Bad GatewayouConnection Reset by Peer; - Transações de banco de dados no database-sql e Connection Pooling ou PostgreSQL com pgx são abortadas no meio;
- Mensagens de filas no Mensageria Prática com RabbitMQ e Kafka ficam em estado inconsistente ou sem confirmação (ACK);
- Arquivos sendo transmitidos ao Upload de Arquivos para AWS S3 são corrompidos.
O Graceful Shutdown (Encerramento Gracioso) é a capacidade de um serviço parar de aceitar novos trabalhos, drenar com segurança todas as requisições em voo, descarregar buffers e fechar conexões de forma ordenada antes de finalizar o processo.
1. O Ciclo de Vida do Desligamento no Kubernetes
O Kubernetes segue um protocolo determinístico de encerramento em duas fases:
As Fases do Processo:
- Remoção do Tráfego: O pod é marcado como
Terminatinge retirado da lista de Endpoints ativos dos Services (o Ingress/Load Balancer para de enviar novos clientes). - Envio do
SIGTERM: O Kubelet envia o sinalSIGTERMpara o processo principal (PID 1) do container Go. - Janela de Carência (Grace Period): A aplicação tem até
terminationGracePeriodSeconds(por padrão, 30 segundos) para concluir as requisições em voo e encerrar por conta própria com código0. - O Golpe Final (
SIGKILL): Se a aplicação não finalizar antes do término da janela de carência, o kernel envia um sinal forçadoSIGKILLque destrói o processo instantaneamente.
2. A Ordem Estrita de Desligamento de Recursos
Um dos erros mais comuns de arquitetura é fechar as conexões do banco de dados antes de desligar o servidor HTTP. Se o servidor ainda estiver processando uma requisição em voo, essa requisição falhará porque o banco foi fechado embaixo dos seus pés!
A ordem deve ser rigorosamente da borda externa para o núcleo de persistência:
- Camada de Borda (HTTP / gRPC): Fecha os listeners de rede com
server.Shutdown(ctx). Nenhuma nova conexão é aceita, mas conexões já abertas continuam sendo atendidas. - Camada de Processamento Assíncrono (Workers / Filas): Notifica os workers de background para encerrarem e aguarda a conclusão com
sync.WaitGroup. - Camada de Persistência (Bancos e Caches): Fecha com segurança pools do PostgreSQL, conexões com Redis e brokers RabbitMQ/Kafka.
- Finalização do Processo: Executa
os.Exit(0).
3. O Padrão Moderno: signal.NotifyContext
No Go antigo, capturar sinais exigia criar canais manuais com buffer e chamar signal.Notify(ch, ...). A partir do Go 1.16, a Standard Library disponibiliza a função idiomática signal.NotifyContext, que vincula sinais do sistema operacional diretamente à mecânica universal do context.Context:
package main
import (
"context"
"os"
"os/signal"
"syscall"
)
func main() {
// Cria um contexto que é cancelado automaticamente quando o SO enviar SIGINT (Ctrl+C) ou SIGTERM (Kubernetes/Docker)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// A aplicação roda normalmente...
<-ctx.Done() // Bloqueia até que o sinal seja recebido!
}
4. Implementação Completa em Produção
Veja como orquestrar Servidor HTTP, Workers de fila e Banco de Dados com controle estrito de timeout de encerramento:
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
type Application struct {
server *http.Server
wg sync.WaitGroup
}
func main() {
// 1. Contexto vinculado aos sinais do SO
rootCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
app := &Application{}
// 2. Inicializa o Servidor HTTP
mux := http.NewServeMux()
mux.HandleFunc("POST /api/v1/checkout", app.handleCheckout)
app.server = &http.Server{
Addr: ":8080",
Handler: mux,
}
// 3. Dispara o Servidor em uma goroutine dedicada
go func() {
log.Println("Servidor HTTP ouvindo na porta :8080")
if err := app.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Erro fatal no servidor HTTP: %v", err)
}
}()
// 4. Dispara Workers de Background
app.startBackgroundWorkers(rootCtx, 5)
// 5. Aguarda o sinal de término do Kubernetes/SO
<-rootCtx.Done()
log.Println("Sinal de término recebido! Iniciando processo de Graceful Shutdown...")
// 6. Define um tempo limite estrito para o encerramento gracioso (ex: 15 segundos)
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// FASE 1: Fecha a borda HTTP (interrompe novos aceites e aguarda conexões ativas)
if err := app.server.Shutdown(shutdownCtx); err != nil {
log.Printf("Aviso: Forçando fechamento do servidor HTTP por timeout: %v\n", err)
} else {
log.Println("Servidor HTTP finalizado com sucesso.")
}
// FASE 2: Aguarda todos os workers em background finalizarem suas mensagens
workersDone := make(chan struct{})
go func() {
app.wg.Wait()
close(workersDone)
}()
select {
case <-workersDone:
log.Println("Todos os workers de background finalizaram suas tarefas.")
case <-shutdownCtx.Done():
log.Println("Aviso: Timeout esgotado! Alguns workers de background foram abortados.")
}
// FASE 3: Fecha conexões com o banco de dados e caches
// dbPool.Close()
// redisClient.Close()
log.Println("Recursos de banco e rede liberados. Encerrando processo limpo (Exit 0).")
}
func (app *Application) handleCheckout(w http.ResponseWriter, r *http.Request) {
// Simula processamento que leva 2 segundos
time.Sleep(2 * time.Second)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"pedido_confirmado"}`))
}
func (app *Application) startBackgroundWorkers(ctx context.Context, numWorkers int) {
for i := 1; i <= numWorkers; i++ {
app.wg.Add(1)
go func(workerID int) {
defer app.wg.Done()
for {
select {
case <-ctx.Done():
log.Printf("Worker #%d finalizou seu ciclo de consumo.\n", workerID)
return
default:
// Consome mensagens da fila com timeout curto...
time.Sleep(500 * time.Millisecond)
}
}
}(i)
}
}
5. Dica de Ouro no Kubernetes: O Atraso do preStop Hook
Mesmo com um Graceful Shutdown perfeito no código Go, existe uma condição de corrida no Kubernetes: quando o pod entra em estado de término, o kube-proxy e os Ingress Controllers demoram alguns segundos para atualizar as regras de roteamento de rede (iptables). Se a sua aplicação chamar server.Shutdown() no milissegundo em que recebe o SIGTERM, o Ingress Controller pode tentar enviar novas requisições para o pod que acabou de fechar a porta!
Para eliminar 100% dos erros 502 em deploys contínuos, adicione um hook preStop com sleep no manifesto do Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: meu-microsservico
spec:
template:
spec:
containers:
- name: app
image: meu-app:v2
lifecycle:
preStop:
exec:
# Aguarda 5 segundos para que todos os Ingress do cluster removam o pod da rota
command: ["/bin/sh", "-c", "sleep 5"]
# Garante tempo suficiente para o sleep 5 + 15s de shutdown da aplicação Go
terminationGracePeriodSeconds: 30
6. Resumo Conceitual
- O encerramento abrupto corrompe transações em voo, corta streams HTTP e gera erros
502em produção. - Use
signal.NotifyContextpara associar sinaisSIGINTeSIGTERMao ciclo de vida docontext.Context. - Obedeça à ordem correta de encerramento: Borda HTTP (
server.Shutdown) $\to$ Workers de Mensageria (sync.WaitGroup) $\to$ Pools de Banco de Dados (Close). - Defina sempre um Timeout Máximo de Drenagem (
context.WithTimeout) para garantir que o processo finalize antes doSIGKILLforçado do Kubernetes. - Adicione um hook
preStop: sleep 5nos manifestos Kubernetes para permitir a propagação das tabelas de roteamento antes de fechar a porta HTTP da aplicação.