pprof é uma das ferramentas mais úteis do ecossistema Go porque transforma suspeita em evidência. Em vez de discutir se a API está lenta por causa de JSON, banco, garbage collector, locks ou excesso de goroutines, você coleta um perfil e olha onde o programa realmente gasta CPU, aloca memória ou fica bloqueado.
O erro comum é tratar pprof como recurso de laboratório: algo que só aparece em benchmark local, fora do caminho real de produção. Em serviços Go, muitos problemas só aparecem com tráfego real, dados reais, cache quente, limites de conexão, filas, concorrência e deploy em container. Por isso vale aprender a expor profiling com segurança, coletar perfis curtos, interpretar sinais e fechar o ciclo com correção, benchmark e rollout.
Este guia mostra como usar pprof em Go para investigar CPU, heap, goroutines, mutex, block profile e traces sem abrir um painel perigoso na internet. Ele complementa PGO em Go, OpenTelemetry em Go, health checks em Go, context e timeouts e graceful shutdown.
O que é pprof
pprof é o formato e o conjunto de ferramentas usados pelo Go para coletar e analisar perfis de execução. A biblioteca padrão oferece dois pacotes principais:
runtime/pprof, para gerar perfis programaticamente;net/http/pprof, para expor endpoints HTTP de profiling.
Na prática, a maioria dos serviços começa com net/http/pprof em uma porta administrativa interna. A partir dela você coleta perfis com go tool pprof, curl ou ferramentas de automação.
Os perfis mais usados são:
| Perfil | Pergunta que responde |
|---|---|
| CPU | Onde o processo gasta tempo de CPU? |
| Heap | Quais caminhos alocam ou retêm memória? |
| Goroutine | Quais goroutines existem e onde estão paradas? |
| Mutex | Onde goroutines esperam por locks? |
| Block | Onde goroutines bloqueiam em channel, select, cond ou sync? |
| Trace | Como scheduler, GC, syscalls e goroutines interagem ao longo do tempo? |
pprof não substitui métricas, logs ou tracing distribuído. Ele é uma lupa. Métricas dizem que CPU subiu, latência piorou ou goroutines cresceram. pprof ajuda a explicar por quê.
Segurança: nunca exponha pprof publicamente
Endpoints de profiling podem revelar nomes de funções, caminhos internos, padrões de tráfego, labels, bibliotecas, endpoints administrativos e até detalhes sensíveis dependendo do código. Além disso, alguns perfis custam CPU e memória. Expor /debug/pprof na internet é convite para vazamento e abuso.
Regra prática: pprof deve ficar atrás de pelo menos uma dessas proteções:
- Porta ligada apenas em
127.0.0.1. - Porta administrativa acessível só pela rede interna ou VPN.
- Autenticação forte no proxy interno.
- Port-forward temporário via Kubernetes.
- Feature flag operacional com janela curta.
Evite colocar pprof no mesmo servidor público da API sem controle de rota. O exemplo inseguro abaixo é comum em tutoriais, mas não deve ir cru para produção:
import _ "net/http/pprof"
func main() {
http.ListenAndServe(":8080", nil) // ruim se esse mux também é público
}
Prefira separar o servidor administrativo:
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", http.DefaultServeMux.ServeHTTP)
mux.HandleFunc("/debug/pprof/cmdline", http.DefaultServeMux.ServeHTTP)
mux.HandleFunc("/debug/pprof/profile", http.DefaultServeMux.ServeHTTP)
mux.HandleFunc("/debug/pprof/symbol", http.DefaultServeMux.ServeHTTP)
mux.HandleFunc("/debug/pprof/trace", http.DefaultServeMux.ServeHTTP)
srv := &http.Server{
Addr: "127.0.0.1:6060",
Handler: mux,
}
log.Printf("pprof admin server: %v", srv.ListenAndServe())
}()
// servidor público da aplicação em outra porta/mux
}
Em Kubernetes, esse desenho permite usar kubectl port-forward para uma coleta temporária:
kubectl port-forward deploy/minha-api 6060:6060
Depois colete do seu terminal local usando localhost.
Perfil de CPU
O CPU profile responde onde o programa gastou tempo executando. É o primeiro perfil a coletar quando há consumo alto de CPU, queda de throughput ou aumento de latência sem sinal claro de banco ou rede.
Com o servidor administrativo ativo:
go tool pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"
Dentro do console:
(pprof) top
(pprof) list MinhaFuncao
(pprof) web
(pprof) png > cpu.png
Também dá para salvar o arquivo bruto:
curl -o cpu.pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=60"
go tool pprof ./bin/app cpu.pprof
Dicas para um perfil de CPU útil:
- Colete durante o problema ou durante carga representativa.
- Use 30 a 120 segundos; perfis muito curtos podem mostrar ruído.
- Compare antes/depois da correção.
- Não tire conclusão de uma coleta isolada se o tráfego varia muito.
- Guarde o commit, versão da imagem e janela de coleta junto do arquivo.
Se top mostra muito tempo em serialização JSON, compressão, regex, template, hashing, parse de data ou reflection, há chance de otimização direta. Se mostra chamadas de runtime, GC ou locks, combine com heap, mutex e block profiles antes de mexer.
Heap profile: alocação versus retenção
O heap profile ajuda a entender memória. Mas existe uma pegadinha: você precisa separar “quem aloca muito” de “quem mantém memória viva”.
Coleta básica:
go tool pprof "http://127.0.0.1:6060/debug/pprof/heap"
No console, veja memória em uso:
(pprof) top
(pprof) top -cum
Para investigar alocações acumuladas, use alloc_space:
(pprof) sample_index=alloc_space
(pprof) top
Sinais comuns:
inuse_spacealto: objetos ainda vivos, possível cache, mapa global, fila, buffer ou leak.alloc_spacealto masinuse_spacebaixo: muita alocação temporária, pode pressionar GC sem reter memória.- Crescimento contínuo de heap junto com goroutines: investigue goroutine leak, canais e contextos sem cancelamento.
- Muito
[]byteoustring: revise cópias, buffers, parsing, compressão e payloads.
Antes de “otimizar” alocação, confirme impacto em métrica. Reduzir alocação em código frio raramente paga. Reduzir alocação em hot path de API ou worker de alto volume pode melhorar CPU, latência e pausas de GC.
Goroutine profile
O goroutine profile mostra stacks das goroutines existentes. Ele é valioso quando a métrica go_goroutines cresce sem estabilizar, quando há shutdown travado, deadlock parcial, consumo de memória inesperado ou workers parados.
go tool pprof "http://127.0.0.1:6060/debug/pprof/goroutine"
Para ver stacks legíveis pelo endpoint direto:
curl "http://127.0.0.1:6060/debug/pprof/goroutine?debug=2" > goroutines.txt
Procure padrões:
- Muitas goroutines presas em receive de channel sem produtor.
- Muitas goroutines esperando
time.Afterem loop. - Chamadas HTTP sem timeout.
- Workers aguardando fila que nunca fecha.
- Goroutines por request que não respeitam
context.Context. - Leitores de body, rows ou streams que não são fechados.
Um exemplo clássico de vazamento:
func handler(w http.ResponseWriter, r *http.Request) {
ch := make(chan Result)
go func() {
ch <- callSlowDependency() // pode bloquear para sempre se o handler retornar
}()
select {
case result := <-ch:
writeResult(w, result)
case <-time.After(200 * time.Millisecond):
http.Error(w, "timeout", http.StatusGatewayTimeout)
}
}
O timeout encerra o handler, mas a goroutine pode ficar presa tentando enviar no channel. Melhor usar contexto, buffer pequeno ou desenho em que a goroutine consegue desistir:
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond)
defer cancel()
result, err := callSlowDependency(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusGatewayTimeout)
return
}
writeResult(w, result)
}
Mutex e block profiles
Por padrão, Go não coleta todos os detalhes de mutex e bloqueios porque isso tem custo. Para investigar contenção, habilite amostragem explicitamente:
import "runtime"
func enableProfiles() {
runtime.SetMutexProfileFraction(10) // 1 em cada 10 eventos de contenção
runtime.SetBlockProfileRate(10000) // amostragem em nanossegundos
}
Depois colete:
go tool pprof "http://127.0.0.1:6060/debug/pprof/mutex"
go tool pprof "http://127.0.0.1:6060/debug/pprof/block"
Use esses perfis quando CPU não explica a lentidão, mas requisições ficam paradas, workers não avançam ou a latência cresce com concorrência.
Causas comuns:
sync.Mutexglobal protegendo cache grande.- Logger síncrono ou writer compartilhado.
- Pool pequeno demais para banco ou HTTP.
- Channel usado como fila sem consumidores suficientes.
singleflightsegurando trabalho pesado por chave ampla demais.- Seção crítica fazendo I/O.
A correção raramente é “remover lock”. Normalmente é reduzir escopo da seção crítica, separar sharding por chave, trocar estrutura de dados, mover I/O para fora do lock ou controlar concorrência de forma explícita.
Trace: quando pprof não basta
O execution trace mostra scheduler, goroutines, syscalls, rede, GC e eventos ao longo do tempo. Ele é mais pesado e mais detalhado do que os perfis tradicionais, mas ajuda em problemas de latência, starvation, pausas e interação entre goroutines.
Coleta curta:
curl -o trace.out "http://127.0.0.1:6060/debug/pprof/trace?seconds=5"
go tool trace trace.out
Use trace com parcimônia. Cinco a dez segundos sob carga representativa costumam ser suficientes. Trace longo vira arquivo grande e difícil de analisar.
Um fluxo de investigação saudável
Um bom fluxo evita mudanças aleatórias:
- Comece pela métrica: CPU, memória, latência, goroutines, erros ou throughput.
- Defina uma hipótese simples.
- Colete o perfil certo durante uma janela representativa.
- Salve comando, horário, versão e arquivo.
- Analise
top,top -cum,liste gráfico. - Faça uma mudança pequena.
- Rode testes e benchmark.
- Recolete perfil no mesmo cenário.
- Publique com rollout controlado.
- Remova ou feche acesso temporário se ele foi aberto só para a investigação.
Exemplo: a API está com CPU alta. Você coleta CPU profile e vê 35% em uma função que monta resposta JSON com reflection e conversão repetida de datas. Corrige cache local do formato, adiciona benchmark do encoder, mede redução de alocação e recolhe perfil. Esse é um ciclo fechado.
Contraexemplo: a API está lenta, alguém troca framework, muda pool, ativa cache global e aumenta CPU limit no mesmo deploy. Se melhorar, ninguém sabe por quê. Se piorar, ninguém sabe o que reverter.
pprof e PGO
Desde Go 1.21, perfis de CPU podem alimentar PGO, a otimização guiada por perfil. O fluxo é parecido, mas a exigência de qualidade é maior: o perfil usado no build precisa representar tráfego real e estável.
curl -o default.pgo "http://127.0.0.1:6060/debug/pprof/profile?seconds=60"
go build -pgo=default.pgo -o bin/app ./cmd/app
Não use um perfil coletado durante incidente, carga artificial ruim ou endpoint administrativo raro para otimizar o binário inteiro. Para detalhes de validação, veja o guia de PGO em Go.
Checklist de produção
Antes de deixar pprof disponível em um serviço Go, revise:
/debug/pprofnão está exposto publicamente.- A porta administrativa usa
127.0.0.1, rede interna, VPN ou proxy autenticado. - Coletas têm duração limitada.
- Perfis salvos não vão para repositório público.
- O time sabe associar perfil a versão/commit.
- Mutex e block profiles só são habilitados quando necessários.
- A coleta não compete com incidentes críticos sem coordenação.
- Perfis usados para PGO passam por revisão.
- Dashboards acompanham CPU, heap, GC, goroutines e latência.
- Correções são validadas com teste, benchmark ou nova coleta.
Erros comuns
Expor pprof junto da API pública. Mesmo que não exista link, scanners encontram endpoints conhecidos.
Coletar perfil fora do problema. Perfil local em laptop não explica necessariamente produção em container.
Confundir alocação acumulada com memória retida. alloc_space alto e inuse_space alto contam histórias diferentes.
Ignorar contexto e timeout. Muitos leaks de goroutine vêm de chamadas sem cancelamento.
Otimizar função fria. Uma função feia mas rara pode aparecer no código e não aparecer no perfil. O perfil manda.
Mudar muita coisa de uma vez. Profiling bom reduz incerteza; deploy caótico aumenta.
Conclusão
pprof é uma vantagem prática de Go: está na biblioteca padrão, conversa com o runtime e funciona em serviços reais. O ganho vem de usar a ferramenta com disciplina operacional. Proteja o endpoint, colete perfis curtos e representativos, interprete CPU, heap e goroutines no contexto das métricas e valide cada correção.
Quando esse hábito entra no time, performance deixa de ser debate abstrato. Você passa a ter evidência para decidir se o gargalo está em alocação, lock, goroutine vazando, JSON, banco, GC ou simplesmente em uma hipótese errada.