Pular para o conteúdo

Trilha de Go · parte 37 de 37

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

Tamanho da fonte:
Largura da coluna:

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:

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:

Ciclo de Vida do Graceful Shutdown no Kubernetes
figura 1 · Ciclo de Vida do Graceful Shutdown no Kubernetes

As Fases do Processo:

  1. Remoção do Tráfego: O pod é marcado como Terminating e retirado da lista de Endpoints ativos dos Services (o Ingress/Load Balancer para de enviar novos clientes).
  2. Envio do SIGTERM: O Kubelet envia o sinal SIGTERM para o processo principal (PID 1) do container Go.
  3. 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ódigo 0.
  4. 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çado SIGKILL que 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:

Ordem Estrita de Desligamento de Recursos
figura 2 · Ordem Estrita de Desligamento de Recursos
  1. 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.
  2. Camada de Processamento Assíncrono (Workers / Filas): Notifica os workers de background para encerrarem e aguarda a conclusão com sync.WaitGroup.
  3. Camada de Persistência (Bancos e Caches): Fecha com segurança pools do PostgreSQL, conexões com Redis e brokers RabbitMQ/Kafka.
  4. 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 502 em produção.
  • Use signal.NotifyContext para associar sinais SIGINT e SIGTERM ao ciclo de vida do context.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 do SIGKILL forçado do Kubernetes.
  • Adicione um hook preStop: sleep 5 nos manifestos Kubernetes para permitir a propagação das tabelas de roteamento antes de fechar a porta HTTP da aplicação.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?