Graceful Shutdown em Go: Por que seu serviço não deveria morrer sem aviso

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:

  1. O servidor continua aceitando requisições até o Shutdown.
  2. Shutdown bloqueia novas conexões e aguarda as existentes terminarem (ou o timeout).
  3. 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

ErroConsequênciaSolução
Não capturar SIGTERMPod morre na hora, requisições perdidasUse signal.Notify
Timeout muito curtoShutdown não completa, pod morto à forçaAumente terminationGracePeriodSeconds e timeout
Não fechar conexões com bancoConexões vazamFeche no defer ou no shutdown handler
Travar em goroutines que nunca terminamShutdown nunca completaUse canais done ou context cancel
Não tratar http.ErrServerClosedLog de erro falsoVerifique 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.

👉 Fale com a Jacobus Software

Rolar para cima