Graceful shutdown
LidoEsta aula ensina como implementar o graceful shutdown em aplicações Go, cobrindo o tratamento de sinais do sistema operacional, o encerramento elegante de servidores HTTP e o uso de context para coordenar a finalização. Inclui exemplos práticos, boas práticas e exercícios com respostas.
Em aplicações de produção, especialmente servidores web, é crucial encerrar o processo de forma controlada, permitindo que requisições em andamento sejam concluídas e recursos sejam liberados corretamente. O graceful shutdown (encerramento gracioso) é uma técnica que garante que o programa finalize sua execução de maneira ordenada ao receber sinais de término do sistema operacional, como SIGINT (Ctrl+C) ou SIGTERM.
Nesta aula, você aprenderá a implementar graceful shutdown em Go, utilizando o pacote os/signal para capturar sinais, o http.Server para encerrar servidores HTTP de forma controlada e o pacote context para coordenar o tempo limite da finalização. Ao final, você terá um conhecimento sólido para construir aplicações robustas e resilientes.
Sinais (os/signal)
O sistema operacional envia sinais para os processos para indicar eventos, como interrupção ou término. Em Go, o pacote os/signal permite que você capture esses sinais e execute ações em resposta. Os sinais mais comuns são os.Interrupt (SIGINT, enviado ao pressionar Ctrl+C) e syscall.SIGTERM (enviado por gerenciadores de processos, como o Docker ou Kubernetes).
Para capturar sinais, você usa a função signal.Notify, que registra um canal para receber os sinais especificados. O programa então fica aguardando nesse canal. É importante usar um canal com buffer de tamanho 1 para não perder sinais que chegam enquanto o programa está ocupado. Veja um exemplo básico:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// Canal para receber os sinais
sigs := make(chan os.Signal, 1)
// Registra os sinais de interesse
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
fmt.Println("Aguardando sinal...")
// Aguarda um sinal
sig := <-sigs
fmt.Printf("Sinal recebido: %v\n", sig)
fmt.Println("Encerrando aplicação...")
}
Nesse exemplo, o programa fica bloqueado no recebimento do canal até que um dos sinais seja enviado. Depois, imprime qual sinal foi recebido e termina. Em uma aplicação real, você chamaria a função de encerramento gracioso nesse ponto.
Encerrando o servidor
Para encerrar um servidor HTTP de forma graciosa, o Go fornece o método Shutdown(ctx) no tipo http.Server. Esse método bloqueia até que todas as conexões ativas sejam concluídas ou até que o contexto seja cancelado (por exemplo, após um tempo limite). Diferente do método Close(), que encerra imediatamente todas as conexões, o Shutdown permite que as requisições em andamento terminem.
O método Shutdown não fecha conexões TLS ativas, mas você pode usar Close após um tempo limite se necessário. Aqui está um exemplo de como usar o Shutdown em resposta a um sinal:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}),
}
// Inicia o servidor em uma goroutine
go func() {
log.Println("Servidor iniciado na porta 8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Erro ao iniciar servidor: %v", err)
}
}()
// Canal para sinais
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
// Aguarda sinal
sig := <-sigs
log.Printf("Sinal recebido: %v", sig)
// Cria contexto com tempo limite de 10 segundos
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Tenta encerrar graciosamente
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Erro no shutdown: %v", err)
// Se o shutdown falhar, força o fechamento
srv.Close()
}
log.Println("Servidor encerrado com sucesso")
}
Nesse código, quando um sinal é recebido, criamos um contexto com timeout de 10 segundos. O método Shutdown bloqueia até que todas as conexões sejam concluídas ou até o timeout. Se o timeout expirar, retorna um erro e podemos chamar Close() para forçar o encerramento. Isso garante que a aplicação não fique presa indefinidamente.
context na finalização
O pacote context é fundamental no graceful shutdown, pois permite definir um prazo para a finalização. Você pode usar context.WithTimeout ou context.WithCancel para controlar o tempo máximo que o shutdown pode levar. O contexto também pode ser propagado para outras partes da aplicação, como operações de banco de dados ou chamadas externas, permitindo que elas sejam canceladas quando o shutdown é iniciado.
Por exemplo, se sua aplicação possui goroutines que processam tarefas, você pode passar o contexto para elas e verificar ctx.Done() para saber quando deve parar. Veja um exemplo com um worker:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker finalizado")
return
default:
fmt.Println("Trabalhando...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
fmt.Println("Cancelando...")
cancel()
// Aguarda um pouco para o worker terminar
time.Sleep(1 * time.Second)
}
No graceful shutdown, você pode combinar o contexto do servidor com um contexto global que é cancelado quando o sinal chega. Assim, todas as operações que dependem desse contexto são interrompidas de forma coordenada. O exemplo a seguir mostra como usar um contexto base que é cancelado no sinal, e passá-lo para o Shutdown com um timeout adicional:
// Contexto global que é cancelado no sinal
ctxGlobal, cancelGlobal := context.WithCancel(context.Background())
defer cancelGlobal()
// Captura de sinal
sig := <-sigs
cancelGlobal() // Cancela todas as operações que usam ctxGlobal
// Cria um contexto com timeout para o shutdown
ctxShutdown, cancelShutdown := context.WithTimeout(ctxGlobal, 10*time.Second)
defer cancelShutdown()
srv.Shutdown(ctxShutdown)
Dessa forma, se o Shutdown demorar muito, o timeout é acionado, e o Shutdown retorna. Note que o ctxGlobal já está cancelado, então o WithTimeout herda esse cancelamento.
Exemplos
Vamos consolidar o conhecimento com um exemplo completo de um servidor que implementa graceful shutdown com logs e tempo limite. Este exemplo inclui um endpoint que simula uma operação demorada, para que você possa ver o shutdown aguardando a conclusão da requisição.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Configura o servidor
srv := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simula uma operação que leva 5 segundos
time.Sleep(5 * time.Second)
w.Write([]byte("Requisição concluída"))
}),
}
// Inicia o servidor em goroutine
go func() {
log.Println("Servidor iniciado na porta 8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Erro ao iniciar servidor: %v", err)
}
}()
// Canal de sinais
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
// Aguarda sinal
sig := <-sigs
log.Printf("Sinal recebido: %v. Iniciando shutdown...", sig)
// Contexto com timeout de 15 segundos (maior que o tempo da requisição)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Executa shutdown
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Erro no shutdown: %v", err)
srv.Close()
}
log.Println("Servidor encerrado graciosamente")
}
Nesse exemplo, se você enviar uma requisição e, durante o processamento, pressionar Ctrl+C, o servidor aguardará a conclusão da requisição (5 segundos) antes de encerrar. O timeout de 15 segundos é suficiente. Se a requisição demorasse mais que 15 segundos, o shutdown seria forçado.
Outro exemplo útil é o uso de um contexto global para cancelar operações em andamento em goroutines. Suponha que você tenha um worker que processa mensagens de uma fila. Ao receber o sinal, você cancela o contexto e o worker para de processar novas mensagens e finaliza as atuais.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
// Inicia 3 workers
for i := 0; i < 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
// Canal de sinais
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
// Aguarda sinal
sig := <-sigs
fmt.Printf("Sinal recebido: %v. Cancelando workers...\n", sig)
cancel() // Cancela o contexto
// Aguarda todos os workers terminarem
wg.Wait()
fmt.Println("Todos os workers finalizados")
}
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d finalizado\n", id)
return
default:
fmt.Printf("Worker %d processando...\n", id)
time.Sleep(1 * time.Second)
}
}
}
Nesse exemplo, ao receber o sinal, o contexto é cancelado e os workers param de processar, saindo do loop. O sync.WaitGroup garante que o programa principal espere todos terminarem antes de encerrar.
Boas práticas e observações finais
Ao implementar graceful shutdown, considere as seguintes boas práticas:
- Defina um tempo limite adequado: O timeout deve ser maior que o tempo máximo esperado para concluir as operações em andamento. Se for muito curto, você pode forçar o encerramento de requisições importantes.
- Use
http.ErrServerClosed: Ao verificar o erro doListenAndServe, ignorehttp.ErrServerClosed, pois ele é retornado quando oShutdowné chamado. - Propague o contexto: Passe o contexto de shutdown para todas as operações que podem ser canceladas, como chamadas a bancos de dados ou APIs externas. Use
context.WithTimeoutpara cada operação individual, se necessário. - Feche recursos: Após o shutdown, feche conexões com bancos de dados, arquivos, etc., para evitar vazamentos.
- Teste o shutdown: Simule sinais em testes para garantir que o encerramento funciona como esperado.
O graceful shutdown é um aspecto essencial para a confiabilidade de serviços em produção. Com as técnicas apresentadas, você pode garantir que sua aplicação encerre de forma limpa, minimizando impactos para os usuários.
Referências
- Documentação oficial do pacote os/signal
- Documentação do método Shutdown em http.Server
- Documentação do pacote context
- Go by Example: Signals
- Blog oficial: Go Concurrency Patterns: Context
- Referência da função signal.Notify
Exercícios
Escreva um programa Go que capture o sinal SIGINT e imprima "Sinal recebido" antes de encerrar. Use o pacote os/signal.
✓ Resposta:package main import ( "fmt" "os" "os/signal" ) func main() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt) <-sigs fmt.Println("Sinal recebido") }Considerando o código do exercício 1, adicione o tratamento do sinal SIGTERM. O que você precisa modificar?
✓ Resposta: Basta adicionarsyscall.SIGTERMna chamada designal.Notify. Ex.:signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)Explique a diferença entre
Server.Shutdown(ctx)eServer.Close().✓ Resposta:Shutdownaguarda as conexões ativas terminarem, respeitando o contexto de timeout.Closefecha imediatamente todas as conexões, sem esperar.Escreva um programa que inicia um servidor HTTP na porta 8080 e implementa graceful shutdown com timeout de 5 segundos. Ao receber SIGTERM, o servidor deve tentar encerrar graciosamente e, se o timeout expirar, forçar o fechamento.
✓ Resposta:package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" ) func main() { srv := &http.Server{Addr: ":8080", Handler: http.DefaultServeMux} go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Erro: %v", err) } }() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGTERM) <-sigs ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("Erro no shutdown: %v", err) srv.Close() } log.Println("Servidor encerrado") }Como você usaria um contexto global para cancelar goroutines em um graceful shutdown? Descreva a abordagem e dê um exemplo de código.
✓ Resposta: Crie um contexto base comcontext.WithCancel. Passe esse contexto para as goroutines. Ao receber o sinal, chame a função de cancelamento. As goroutines devem verificar<-ctx.Done()em um select para sair. Exemplo:ctx, cancel := context.WithCancel(context.Background()) // ... inicia goroutines com ctx // ao receber sinal: cancel() // aguarde as goroutines terminarem