Graceful shutdown: encerrando serviços com elegância
1. Por que o Graceful Shutdown é Essencial?
Quando um servidor Go é encerrado abruptamente — seja por um kill -9, um crash ou um deploy mal planejado — as consequências podem ser graves. Conexões abertas são cortadas no meio de uma requisição, dados em trânsito são perdidos e o estado do sistema pode ficar inconsistente.
Imagine um serviço de pagamentos: se o servidor for derrubado enquanto processa uma transação, o cliente pode ser debitado sem receber a confirmação, ou pior, o banco de dados pode ficar com um registro parcial. Bancos de dados, filas como RabbitMQ ou Kafka, e caches como Redis sofrem com encerramentos abruptos.
Para o usuário final, um downtime planejado com graceful shutdown é invisível — as requisições em andamento são concluídas e novas conexões são rejeitadas de forma educada. Já um shutdown forçado gera erros 500, timeouts e frustração.
2. Sinais do Sistema Operacional e Captura em Go
O sistema operacional envia sinais para notificar processos sobre eventos. Os mais relevantes para graceful shutdown são:
- SIGINT (Ctrl+C): interrupção do terminal
- SIGTERM: término solicitado (comum em Kubernetes e systemd)
- SIGHUP: hang up (reinicialização de configuração)
Em Go, capturamos esses sinais com o pacote os/signal:
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigCh
log.Printf("Sinal recebido: %v. Iniciando shutdown...", sig)
cancel()
}()
// Seu serviço aqui
<-ctx.Done()
log.Println("Serviço encerrado com elegância")
}
O canal sigCh recebe os sinais, e ao capturá-los, cancelamos o contexto, propagando o cancelamento para todas as goroutines que o escutam.
3. Estrutura Básica de um Shutdown Controlado
O padrão mais comum combina signal.Notify com http.Server.Shutdown(). Veja um exemplo completo:
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 processamento longo
w.Write([]byte("OK"))
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Goroutine para iniciar o servidor
go func() {
log.Println("Servidor iniciado na porta :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Erro no servidor: %v", err)
}
}()
// Canal de sinais
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Servidor está sendo desligado...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Erro ao desligar servidor: %v", err)
}
log.Println("Servidor encerrado com sucesso")
}
O laço principal com select aguarda o sinal, e Shutdown() bloqueia até que todas as requisições ativas sejam concluídas ou o timeout expire.
4. Gerenciando Múltiplos Serviços (HTTP, gRPC, Workers)
Em sistemas reais, você pode ter um servidor HTTP, um servidor gRPC e workers consumindo filas simultaneamente. O sync.WaitGroup é essencial para coordenar o shutdown:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func startHTTPServer(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
server := &http.Server{Addr: ":8080"}
go func() {
<-ctx.Done()
log.Println("Desligando servidor HTTP...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
}()
log.Println("Servidor HTTP iniciado")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("Erro HTTP: %v", err)
}
}
func startWorker(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
log.Println("Worker encerrando...")
return
default:
// Processa trabalho
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
wg.Add(2)
go startHTTPServer(ctx, &wg)
go startWorker(ctx, &wg)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Iniciando shutdown de todos os serviços...")
cancel() // Propaga cancelamento para todos
wg.Wait() // Aguarda todos os serviços encerrarem
log.Println("Todos os serviços encerrados")
}
A ordem importa: pare frontends (HTTP/gRPC) antes de backends (workers, bancos), e consumidores antes de produtores para evitar acúmulo de dados não processados.
5. Timeouts e Deadline para Evitar Shutdowns Infinitos
Um erro comum é um shutdown que nunca termina porque uma requisição ficou presa. Use context.WithTimeout para limitar a espera:
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("Shutdown forçado após timeout: %v", err)
server.Close() // Fecha conexões restantes abruptamente
}
A diferença entre Shutdown() e Close() é crítica:
- Shutdown(): espera requisições ativas terminarem, depois fecha
- Close(): fecha imediatamente, abandonando requisições
Configure ReadTimeout e WriteTimeout no servidor para evitar requisições infinitas:
server := &http.Server{
Addr: ":8080",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
6. Graceful Shutdown em Workers e Goroutines Assíncronas
Workers que processam filas precisam drenar seu trabalho antes de encerrar:
type WorkerPool struct {
jobs chan Job
done chan struct{}
wg sync.WaitGroup
}
func (wp *WorkerPool) Start(numWorkers int) {
for i := 0; i < numWorkers; i++ {
wp.wg.Add(1)
go wp.worker()
}
}
func (wp *WorkerPool) worker() {
defer wp.wg.Done()
for {
select {
case job, ok := <-wp.jobs:
if !ok {
return // Canal fechado, encerra worker
}
job.Process()
case <-wp.done:
return // Sinal de shutdown
}
}
}
func (wp *WorkerPool) Shutdown() {
close(wp.jobs) // Para de aceitar novos jobs
close(wp.done) // Notifica workers
wp.wg.Wait() // Aguarda workers terminarem
}
Use sync.Cond ou variáveis atômicas para notificações mais complexas, como drenagem parcial antes do shutdown completo.
7. Testando o Graceful Shutdown
Testes unitários podem simular sinais:
func TestGracefulShutdown(t *testing.T) {
// Configura servidor em background
server := &http.Server{Addr: ":0"}
// ... configura handlers
go server.ListenAndServe()
// Simula SIGTERM
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
// Aguarda shutdown
time.Sleep(100 * time.Millisecond)
// Verifica que servidor fechou
if err := server.ListenAndServe(); err != http.ErrServerClosed {
t.Errorf("Esperava servidor fechado, got: %v", err)
}
}
Para testes de integração, use ferramentas como timeout do sistema e scripts de stress (ex: hey ou wrk) para enviar requisições enquanto o servidor é encerrado, verificando se todas foram completadas.
8. Boas Práticas e Armadilhas Comuns
- Múltiplas chamadas a
signal.Notify: cada chamada adiciona um ouvinte. Usesignal.Resetse precisar reconfigurar. - Logging detalhado: registre cada etapa do shutdown (início, serviços encerrados, conclusão) para depuração.
recoverem goroutines: garanta que um panic não interrompa o shutdown:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered de panic: %v", r)
}
}()
// Código do worker
}()
- Ordem de shutdown: pare serviços que recebem requisições primeiro, depois os que processam dados.
- Timeout global: sempre tenha um timeout máximo para todo o shutdown (ex: 30 segundos) para evitar bloqueios eternos.
Referências
- Documentação oficial do pacote os/signal — Referência completa para captura de sinais em Go
- Documentação do http.Server.Shutdown — API oficial para shutdown elegante de servidores HTTP
- Go by Example: Signals — Tutorial prático sobre captura e tratamento de sinais
- Graceful shutdown in Go (Eli Bendersky) — Artigo técnico detalhado sobre shutdown de servidores TCP
- Kubernetes: Graceful Shutdown — Como o Kubernetes gerencia o ciclo de vida de pods, incluindo graceful shutdown
- Cloudflare: Graceful shutdown patterns — Padrões avançados de shutdown usados em produção na Cloudflare