Profiling com pprof

1. Introdução ao Profiling em Go

Profiling é o processo de medição dinâmica do comportamento de um programa durante sua execução, coletando dados sobre uso de CPU, memória, concorrência e outros recursos. Em Go, o profiling é essencial para identificar gargalos de performance, vazamentos de memória e problemas de concorrência antes que eles afetem usuários em produção.

A ferramenta nativa pprof (profiling profiler) está integrada ao ecossistema Go através dos pacotes runtime/pprof e net/http/pprof. Ela permite coletar cinco tipos principais de perfis:

  • CPU: onde o programa gasta tempo de processamento
  • Memória (heap): alocações e retenção de objetos
  • Goroutine: número e estado de goroutines ativas
  • Block: operações que bloqueiam goroutines
  • Mutex: contenção em locks (mutexes)

Cada perfil responde a uma pergunta específica sobre o comportamento do programa, permitindo otimizações direcionadas.

2. Habilitando e Coletando Perfis

Para aplicações web

O pacote net/http/pprof expõe endpoints HTTP para coleta remota de perfis. Basta importá-lo silenciosamente:

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof" // registra handlers no DefaultServeMux
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, profiling!"))
    })

    log.Println("Servidor rodando em :8080 - pprof em /debug/pprof/")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Com o servidor rodando, colete perfis com:

# Perfil de CPU por 30 segundos
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

# Perfil de heap
go tool pprof http://localhost:8080/debug/pprof/heap

# Perfil de goroutines
go tool pprof http://localhost:8080/debug/pprof/goroutine

Para programas CLI

Use runtime/pprof para salvar perfis em arquivos:

package main

import (
    "os"
    "runtime/pprof"
)

func main() {
    // CPU profiling
    fCPU, _ := os.Create("cpu.pprof")
    pprof.StartCPUProfile(fCPU)
    defer pprof.StopCPUProfile()

    // Executa o trabalho pesado
    doWork()

    // Memória profiling
    fMem, _ := os.Create("mem.pprof")
    pprof.WriteHeapProfile(fMem)
    defer fMem.Close()
}

func doWork() {
    // Simula processamento intensivo
    sum := 0
    for i := 0; i < 100000000; i++ {
        sum += i
    }
}

3. Análise de Perfil de CPU

Para iniciar o CPU profiling, chamamos pprof.StartCPUProfile() e paramos com pprof.StopCPUProfile(). O perfil captura amostras periódicas do contador de programa.

Analise o perfil com go tool pprof:

go tool pprof cpu.pprof

Dentro do shell interativo do pprof, comandos úteis:

(pprof) top10          # Top 10 funções mais custosas
(pprof) top --cum      # Ordena por tempo cumulativo
(pprof) list minhasFuncao  # Mostra o código com tempos por linha
(pprof) web            # Gera gráfico SVG (requer Graphviz)
(pprof) peek           # Mostra callers e callees de uma função

Exemplo de saída top:

Showing nodes accounting for 80.50s, 85.11% of 94.58s total
Dropped 42 nodes (cum <= 0.47s)
      flat  flat%   sum%        cum   cum%
    30.20s 31.93% 31.93%     30.20s 31.93%  regexp.(*input).step
    25.40s 26.85% 58.78%     25.40s 26.85%  runtime.memmove
    15.10s 15.96% 74.74%     45.30s 47.89%  regexp.(*machine).add
     9.80s 10.36% 85.10%      9.80s 10.36%  runtime.memclrNoHeapPointers

A coluna flat mostra o tempo gasto diretamente na função, enquanto cum inclui chamadas internas. Funções com alta taxa flat são candidatas imediatas a otimização.

4. Análise de Perfil de Memória

O perfil de heap mostra alocações ativas. Os principais modos de visualização são:

  • inuse_space: quantidade de memória alocada e não liberada
  • inuse_objects: número de objetos alocados e não liberados
  • alloc_space: total de memória alocada desde o início
  • alloc_objects: total de objetos alocados

Para analisar:

go tool pprof -alloc_space mem.pprof
go tool pprof -inuse_objects mem.pprof

Identificando vazamentos

Execute o programa duas vezes (antes e depois de um intervalo) e compare:

# Primeira coleta
curl -o mem1.pprof http://localhost:8080/debug/pprof/heap

# Após algum tempo
curl -o mem2.pprof http://localhost:8080/debug/pprof/heap

# Comparação
go tool pprof -base mem1.pprof mem2.pprof

O comando -base mostra o delta entre os perfis. Funções que crescem consistentemente indicam vazamento.

5. Perfis de Concorrência: Goroutine, Block e Mutex

Perfil de goroutines

Revela goroutines que não estão progredindo:

# Lista todas as goroutines com suas pilhas
go tool pprof http://localhost:8080/debug/pprof/goroutine?debug=2

No modo interativo, use top para ver funções que criam muitas goroutines.

Perfil de block

Mostra operações que bloqueiam goroutines (channels, mutexes, I/O). Habilite antes:

runtime.SetBlockProfileRate(1) // 1 = amostrar todas as operações

Colete:

go tool pprof http://localhost:8080/debug/pprof/block

Perfil de mutex

Revela contenção em mutexes. Habilite com:

runtime.SetMutexProfileFraction(1) // 1 = amostrar todos os conflitos

Trace para visualização avançada

O pacote runtime/trace gera arquivos que podem ser abertos no navegador:

import "runtime/trace"

f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()

Visualize com:

go tool trace trace.out

6. Visualização e Ferramentas Avançadas

Gráficos com Graphviz

Instale o Graphviz e use:

go tool pprof -svg cpu.pprof > cpu.svg
go tool pprof -pdf cpu.pprof > cpu.pdf

Interface web interativa

Desde Go 1.11, o pprof tem servidor HTTP próprio:

go tool pprof -http=:8081 cpu.pprof

Isso abre uma interface web com flame graphs, gráficos de chama e visualização interativa do call graph.

Flame graphs

No servidor web do pprof, acesse a aba "Flame Graph" para ver uma representação hierárquica do uso de recursos. Cada retângulo representa uma função, com largura proporcional ao tempo consumido.

7. Boas Práticas e Armadilhas Comuns

Impacto na performance

Profiling adiciona overhead. CPU profiling reduz a taxa de execução em 5-10%. Use amostragem (padrão de 100 amostras/segundo) e evite profiling contínuo em produção.

Profiling em produção

  • Nunca exponha /debug/pprof/ publicamente sem autenticação
  • Use profiling apenas em ambientes de staging ou com tráfego controlado
  • Considere profiling amostral em produção com pprof.StartCPUProfile por curtos períodos

Limpeza entre execuções

Sempre feche arquivos de perfil e chame StopCPUProfile():

func collectProfile() {
    f, _ := os.Create("cpu.pprof")
    pprof.StartCPUProfile(f)
    defer func() {
        pprof.StopCPUProfile()
        f.Close()
    }()
    // trabalho...
}

Armadilhas comuns

  1. Perfil de memória sem GC: execute runtime.GC() antes de coletar para ver apenas objetos vivos
  2. Amostragem insuficiente: colete por pelo menos 30 segundos para CPU
  3. Comparações inválidas: sempre use a mesma duração e carga de trabalho ao comparar perfis

Referências