
Você já fez um deploy e, por alguns segundos, os usuários receberam erros? Ou o Kubernetes matou um pod enquanto ele ainda processava requisições? Esse é o famoso shutdown abrupto, e ele causa perda de dados, experiência ruim e incidentes desnecessários.
Em Go, temos todas as ferramentas para implementar um graceful shutdown – o desligamento gracioso que espera as requisições em andamento terminarem, fecha conexões e só então finaliza o processo. Neste post, vamos mostrar como fazer isso direito, com exemplos para servidores HTTP, gRPC, workers de fila e integração com Kubernetes.
O que é graceful shutdown?
Quando um processo recebe um sinal de término (SIGTERM, SIGINT), o comportamento padrão é morrer imediatamente. Conexões abertas são cortadas, requisições em andamento são abandonadas, conexões com banco ficam pendentes.
O graceful shutdown intercepta esse sinal, executa uma rotina de limpeza e só então sai.
Em produção (Kubernetes, systemd, supervisord): isso é essencial. O Kubernetes envia SIGTERM para um pod antes de removê-lo do balanceamento. Se seu serviço não se preparar, requisições em andamento serão perdidas.
Exemplo 1: Servidor HTTP com graceful shutdown
A biblioteca padrão net/http já oferece http.Server.Shutdown.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // simula trabalho
w.Write([]byte("OK"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Goroutine para rodar o servidor
go func() {
log.Println("Servidor iniciado na porta 8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("erro: %v", err)
}
}()
// Canal para aguardar sinal de término
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Sinal recebido, desligando graciosamente...")
// Timeout para as requisições em andamento terminarem
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("erro no shutdown: %v", err)
}
log.Println("Servidor finalizado com sucesso")
}
O que acontece:
- O servidor continua aceitando requisições até o
Shutdown. Shutdownbloqueia novas conexões e aguarda as existentes terminarem (ou o timeout).- Após isso, o programa termina.
Exemplo 2: Servidor gRPC com graceful stop
Para gRPC, use grpc.Server.GracefulStop().
import (
"google.golang.org/grpc"
)
func main() {
grpcServer := grpc.NewServer()
// Registrar serviços...
go func() {
listener, _ := net.Listen("tcp", ":50051")
log.Println("gRPC server rodando")
if err := grpcServer.Serve(listener); err != nil {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Parando gRPC server graciosamente...")
grpcServer.GracefulStop()
log.Println("gRPC server finalizado")
}
Exemplo 3: Worker de fila (NATS, RabbitMQ, Kafka)
Se você consome mensagens de uma fila, precisa finalizar o consumo atual antes de parar.
type Worker struct {
messages <-chan Message
wg sync.WaitGroup
done chan struct{}
}
func (w *Worker) Start() {
w.wg.Add(1)
go func() {
defer w.wg.Done()
for {
select {
case msg, ok := <-w.messages:
if !ok {
return
}
w.process(msg)
case <-w.done:
// Espera terminar o processamento atual?
// Depende do design. Prefira drenar o canal.
return
}
}
}()
}
func (w *Worker) Stop() {
close(w.done)
w.wg.Wait()
}
Melhor prática: feche o canal de mensagens para que o worker processe o que já está na fila e depois termine.
Graceful shutdown com conexões de banco de dados
Conexões de banco (pool) também devem ser fechadas no shutdown.
func closeDB(db *sql.DB) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := db.Close(); err != nil {
log.Printf("erro ao fechar banco: %v", err)
}
}
Kubernetes: o preStop hook
Mesmo com graceful shutdown no código, o Kubernetes pode enviar SIGTERM e, se o pod não terminar em terminationGracePeriodSeconds (padrão 30s), ele será morto à força.
Adicione um preStop hook no deployment para dar tempo extra:
spec:
containers:
- name: api
image: myapp
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
Isso faz o pod esperar 15 segundos antes de receber o SIGTERM, dando tempo para o Service remover o pod do balanceamento.
Recomendação da Jacobus: Combine o preStop com um graceful shutdown de até 30 segundos no código.
Exemplo completo para Kubernetes + Go
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 45 # tempo total para o pod terminar
containers:
- name: app
image: jacobus/myapp:latest
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
env:
- name: SHUTDOWN_TIMEOUT
value: "30"
// main.go
shutdownTimeout, _ := strconv.Atoi(os.Getenv("SHUTDOWN_TIMEOUT"))
if shutdownTimeout == 0 {
shutdownTimeout = 30
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(shutdownTimeout)*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err)
}
Testando graceful shutdown localmente
Você pode enviar SIGTERM manualmente para testar:
go run main.go &
PID=$!
kill -TERM $PID
Ou use Ctrl+C para SIGINT.
Erros comuns e como evitá-los
| Erro | Consequência | Solução |
|---|---|---|
Não capturar SIGTERM | Pod morre na hora, requisições perdidas | Use signal.Notify |
| Timeout muito curto | Shutdown não completa, pod morto à força | Aumente terminationGracePeriodSeconds e timeout |
| Não fechar conexões com banco | Conexões vazam | Feche no defer ou no shutdown handler |
| Travar em goroutines que nunca terminam | Shutdown nunca completa | Use canais done ou context cancel |
Não tratar http.ErrServerClosed | Log de erro falso | Verifique o erro após srv.Shutdown |
Conclusão
Graceful shutdown não é luxo – é necessidade para qualquer serviço que roda em produção. Em Go, implementar é simples e a biblioteca padrão já fornece as ferramentas. Combine com o preStop no Kubernetes e você terá deploys sem downtime e sem perda de requisições.
Na Jacobus Software, todo microsserviço Go que entregamos tem graceful shutdown. Porque serviço que morre sem aviso prejudica o negócio e a experiência do usuário.
🛑 Seu serviço Go morre sem aviso? Vamos arrumar.
Nossos especialistas revisam seus deploys e implementam graceful shutdown, health probes e configurações de Kubernetes para zero downtime.
