Truques para debugar memory leaks em aplicações Go com pprof
1. Introdução ao Memory Leak em Go e ao pprof
Memory leaks em Go são mais sutis do que em linguagens sem garbage collector. O garbage collector (GC) gerencia a memória automaticamente, mas não consegue liberar objetos que ainda possuem referências ativas. Os padrões mais comuns de vazamento incluem:
- Goroutines órfãs: goroutines iniciadas mas que nunca finalizam, retendo memória e recursos
- Slices e maps com capacidade excedida: estruturas que crescem sem limite e nunca são limpas
- Referências persistentes: variáveis globais ou closures que mantêm referências a objetos que deveriam ser coletados
- Canais sem consumidores: goroutines bloqueadas em operações de envio/recebimento
O pacote net/http/pprof expõe endpoints HTTP com perfis de runtime, e o comando go tool pprof permite analisar esses perfis de forma interativa. Juntos, formam a principal ferramenta para diagnosticar problemas de memória em Go.
2. Configurando o pprof na sua aplicação
Para habilitar o pprof, importe o pacote e registre os handlers HTTP:
import (
"net/http"
_ "net/http/pprof"
)
func main() {
// Inicia servidor HTTP com pprof em porta dedicada
go func() {
http.ListenAndServe(":6060", nil)
}()
// Sua aplicação continua normalmente
// ...
}
Por padrão, o pprof expõe vários endpoints:
- /debug/pprof/heap — perfil de heap (objetos vivos)
- /debug/pprof/goroutine — stack traces de todas as goroutines
- /debug/pprof/allocs — histórico de alocações
- /debug/pprof/profile — perfil de CPU
Dica importante: nunca exponha o pprof em produção sem autenticação ou em redes públicas. Use middleware de autenticação ou escute apenas em localhost:
http.ListenAndServe("127.0.0.1:6060", nil)
3. Capturando e comparando snapshots de heap
Para capturar um snapshot do heap:
# Captura o perfil de heap atual
curl -s http://localhost:6060/debug/pprof/heap > heap_antes.pprof
# Após executar operações que podem causar vazamento
curl -s http://localhost:6060/debug/pprof/heap > heap_depois.pprof
Análise interativa com go tool pprof:
# Análise básica
go tool pprof heap_depois.pprof
# Comandos dentro do pprof interativo:
# top - mostra as funções que mais alocam
# list NomeFuncao - detalha alocações por linha
# web - gera gráfico SVG das chamadas
Truque essencial: comparar snapshots para ver o que cresceu:
go tool pprof -base heap_antes.pprof heap_depois.pprof
Isso mostra apenas as alocações que ocorreram entre os dois snapshots, destacando exatamente onde a memória está vazando.
4. Identificando padrões comuns de vazamento
Goroutines sem controle de cancelamento
// PROBLEMA: goroutine nunca finaliza
func processItems(items []Item) {
for _, item := range items {
go func(i Item) {
process(i) // se process nunca retorna ou trava
}(item)
}
}
// SOLUÇÃO: usar context com timeout
func processItems(ctx context.Context, items []Item) {
for _, item := range items {
go func(i Item) {
select {
case <-ctx.Done():
return
default:
process(i)
}
}(item)
}
}
Slices e maps globais que crescem indefinidamente
var cacheGlobal map[string]*Dados
func addToCache(key string, dados *Dados) {
cacheGlobal[key] = dados // nunca é limpo
}
// SOLUÇÃO: usar cache com TTL ou LRU
Closures que retêm variáveis
// PROBLEMA: closure retém referência ao slice
func criarProcessadores(items []Item) []func() {
processadores := make([]func(), 0, len(items))
for _, item := range items {
processadores = append(processadores, func() {
processar(item) // item é capturado por referência
})
}
return processadores
}
5. Analisando alocações suspeitas com alocação detalhada
O profile de alocações (/debug/pprof/allocs) mostra o histórico de todas as alocações, não apenas os objetos vivos:
# Captura perfil de alocações
curl -s http://localhost:6060/debug/pprof/allocs > allocs.pprof
# Análise
go tool pprof allocs.pprof
Diferença crucial: o heap profile mostra objetos que ainda estão na memória (vivos), enquanto o allocs profile mostra todas as alocações já feitas (incluindo as já coletadas pelo GC).
Truque avançado com -diff_base:
# Captura baseline
curl -s http://localhost:6060/debug/pprof/allocs > baseline.pprof
# Executa operação suspeita
curl -s http://localhost:6060/debug/pprof/allocs > apos_operacao.pprof
# Compara
go tool pprof -diff_base baseline.pprof apos_operacao.pprof
6. Depurando vazamentos de goroutines
Para listar todas as goroutines com detalhes:
curl -s http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines.txt
O modo debug=2 mostra o stack trace completo de cada goroutine, permitindo identificar:
- Goroutines presas em
selectsem caso pronto - Goroutines bloqueadas em canais sem receptor
- Goroutines em loops infinitos
Ferramenta complementar: monitore runtime.NumGoroutine() periodicamente:
go func() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
log.Printf("Goroutines ativas: %d", runtime.NumGoroutine())
}
}()
Um crescimento contínuo no número de goroutines (sem picos esperados) é sinal claro de vazamento.
7. Automatizando a detecção com scripts e integração contínua
Script para coleta automatizada:
#!/bin/bash
# collect_profiles.sh
HOST="http://localhost:6060"
OUTPUT_DIR="./profiles/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$OUTPUT_DIR"
echo "Coletando perfis em $OUTPUT_DIR"
# Perfil de heap
curl -s "$HOST/debug/pprof/heap" > "$OUTPUT_DIR/heap.pprof"
# Perfil de alocações
curl -s "$HOST/debug/pprof/allocs" > "$OUTPUT_DIR/allocs.pprof"
# Goroutines detalhadas
curl -s "$HOST/debug/pprof/goroutine?debug=2" > "$OUTPUT_DIR/goroutines.txt"
# Número de goroutines
echo "Goroutines: $(curl -s "$HOST/debug/pprof/goroutine?debug=1" | wc -l)" > "$OUTPUT_DIR/status.txt"
echo "Coleta concluída"
Integração com testes de carga usando vegeta:
# Simula carga enquanto coleta perfis
echo "GET http://localhost:8080/api/endpoint" | vegeta attack -rate=100 -duration=30s | vegeta report
# Coleta perfil após carga
curl -s http://localhost:6060/debug/pprof/heap > heap_pos_carga.pprof
Para CI/CD, crie um script que compara perfis e falha se detectar crescimento anormal:
#!/bin/bash
# check_memory_leak.sh
BASELINE="baseline.pprof"
CURRENT="current.pprof"
go tool pprof -top -diff_base=$BASELINE $CURRENT | grep -E "^[[:space:]]*[0-9]+\.[0-9]+%" | head -5
# Se alguma função mostrou crescimento > 10%, falha
if go tool pprof -top -diff_base=$BASELINE $CURRENT | awk '{if ($1 > 10) exit 1}'; then
echo "Memory leak detectado!"
exit 1
fi
Conclusão
Debuggar memory leaks em Go requer uma combinação de ferramentas e boas práticas. O pprof é seu principal aliado, mas é preciso saber interpretar os dados que ele fornece. Lembre-se:
- Sempre compare snapshots (antes/depois) para isolar vazamentos
- Monitore goroutines ativas como indicador precoce
- Automatize a coleta de perfis em pipelines de CI
- Use contextos e canais com cuidado para evitar goroutines órfãs
Com essas técnicas, você conseguirá identificar e corrigir memory leaks de forma sistemática, mantendo suas aplicações Go eficientes e estáveis.
Referências
- Documentação oficial do pprof em Go — Referência completa sobre os endpoints e funcionalidades do pacote pprof
- Profiling Go programs (Blog oficial Go) — Artigo detalhado sobre profiling de aplicações Go com exemplos práticos
- Debugging performance issues in Go programs — Wiki oficial com guias de performance e debugging de memory leaks
- Understanding Go memory usage — Artigo técnico da Ardan Labs sobre alocação de memória e garbage collection em Go
- Go Memory Leak Detection with pprof — Tutorial prático com exemplos reais de detecção de vazamentos usando pprof
- Profiling and optimizing Go programs — Palestra da GopherCon sobre técnicas avançadas de profiling com pprof
- Go tool pprof documentation — Repositório oficial do pprof com documentação detalhada de todos os comandos e opções