Profiling com pprof
Esta aula ensina a usar o pacote pprof de Go para realizar profiling de performance, cobrindo análise de CPU e memória, integração com servidores HTTP via net/http/pprof, interpretação de relatórios e otimização guiada por dados.
Bem-vindo à aula sobre profiling com pprof! Quando seus programas Go ficam lentos ou consomem muita memória, você precisa de ferramentas para descobrir onde estão os gargalos. O pacote runtime/pprof e o subpacote net/http/pprof fornecem funcionalidades poderosas para coletar e analisar dados de desempenho. Nesta aula, você aprenderá a gerar perfis de CPU e memória, integrar pprof a um servidor HTTP, interpretar os resultados e usar essas informações para otimizar seu código de forma direcionada.
Vamos explorar desde a instrumentação básica até a análise visual com ferramentas como go tool pprof e go tool trace. Ao final, você terá um fluxo de trabalho completo para identificar e corrigir problemas de performance em aplicações Go.
CPU e memória
O profiling de CPU e memória é essencial para entender o comportamento do seu programa. O pprof coleta amostras em intervalos regulares durante a execução, criando um retrato do que o programa está fazendo. Para CPU, ele captura a pilha de chamadas em cada amostra, permitindo identificar funções que consomem mais tempo de processamento. Para memória, ele rastreia alocações e retenções, mostrando onde a memória está sendo usada.
Em Go, você pode gerar perfis programaticamente usando runtime/pprof. O exemplo a seguir mostra como iniciar e parar um perfil de CPU, salvando os dados em um arquivo.
package main
import (
"os"
"runtime/pprof"
)
func main() {
f, err := os.Create("cpu.prof")
if err != nil {
panic(err)
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
panic(err)
}
defer pprof.StopCPUProfile()
// Seu código aqui
slowFunction()
}
Para memória, você pode obter uma amostra do heap em um ponto específico:
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
Esses arquivos podem ser analisados com go tool pprof ou visualmente com go tool pprof -http.
net/http/pprof
Em aplicações web, você pode expor endpoints de profiling diretamente no seu servidor HTTP usando net/http/pprof. Basta importar o pacote (com um underscore para registrar os handlers) e os endpoints como /debug/pprof/ estarão disponíveis.
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Seu servidor principal
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
Com isso, você pode acessar http://localhost:6060/debug/pprof/ para ver a lista de perfis disponíveis, como profile (CPU), heap (memória), goroutine, block, etc. Por exemplo, para baixar um perfil de CPU de 30 segundos, use curl http://localhost:6060/debug/pprof/profile?seconds=30.
É importante proteger esses endpoints em produção, pois eles podem expor informações sensíveis. Use autenticação ou restrinja o acesso a redes internas.
Interpretando
Depois de gerar um perfil, você precisa interpretá-lo. O comando go tool pprof oferece uma interface interativa para explorar os dados. Por exemplo, para analisar um perfil de CPU:
go tool pprof cpu.prof
Dentro do prompt, você pode usar comandos como top para listar as funções mais quentes, list para ver o código-fonte com anotações, e web para gerar um gráfico visual (requer Graphviz).
Vamos supor que o perfil mostre que a função slowFunction consome 80% do tempo. Você pode investigar com list slowFunction para ver exatamente onde o tempo é gasto.
(pprof) list slowFunction
Isso mostrará o código-fonte com contagens de amostras por linha, ajudando a identificar gargalos específicos.
Para memória, o comando go tool pprof -sample_index=alloc_objects mem.prof pode mostrar onde as alocações estão acontecendo. Você também pode usar go tool pprof -http=:8081 mem.prof para abrir uma interface gráfica no navegador, com gráficos e visualizações interativas.
Além disso, o pacote runtime/pprof permite criar perfis de goroutines, bloqueios e mutexes, que são úteis para diagnosticar problemas de concorrência.
Otimização guiada
Com os dados do pprof, você pode tomar decisões de otimização baseadas em evidências, em vez de suposições. O processo típico é:
- Medir: gere perfis de CPU e memória.
- Identificar: use
go tool pprofpara encontrar as funções mais caras. - Otimizar: aplique técnicas como redução de alocações, uso de concorrência, ou algoritmos mais eficientes.
- Verificar: repita o profiling para confirmar a melhoria.
Por exemplo, se o perfil mostra que uma função está fazendo muitas alocações, você pode otimizar reutilizando buffers ou usando tipos de valor em vez de ponteiros. Se há contenção de locks, você pode usar estruturas lock-free ou particionar dados.
Vamos ver um exemplo prático. Suponha que temos uma função que processa uma lista de strings e concatena repetidamente:
func joinStrings(parts []string) string {
result := ""
for _, p := range parts {
result += p
}
return result
}
Essa função é ineficiente porque cada concatenação cria uma nova string, resultando em muitas alocações. O profiling de memória mostraria isso. Uma otimização seria usar strings.Builder:
func joinStrings(parts []string) string {
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
Essa mudança reduz drasticamente as alocações e melhora o desempenho.
Lembre-se: otimize apenas o que é necessário. Use o profiling para encontrar os gargalos reais, não otimize código que não está causando problemas.
Boas práticas e observações finais
Aqui estão algumas recomendações ao trabalhar com pprof:
- Sempre faça profiling em um ambiente representativo de produção, com cargas de trabalho realistas.
- Execute perfis de CPU por um período significativo (pelo menos 30 segundos) para obter amostras estatisticamente relevantes.
- Compare perfis antes e depois de mudanças para quantificar melhorias.
- Use
go test -benchpara benchmarks unitários e-cpuprofile/-memprofilepara gerar perfis de testes. - Em produção, considere usar ferramentas como
net/http/pprofcom segurança, ou colete perfis em intervalos específicos.
O profiling é uma habilidade essencial para qualquer programador Go que deseja escrever software eficiente. Com o pprof, você tem uma ferramenta poderosa para entender o comportamento do seu código e otimizá-lo de forma guiada por dados.
Referências
- Documentação oficial do runtime/pprof
- Documentação oficial do net/http/pprof
- Blog do Go: Profiling Go Programs (artigo original)
- Diagnósticos em Go (documentação oficial)
- Repositório do pprof (ferramenta de visualização)
- Especificação da linguagem Go (para referência)
Exercícios
- Exercício 1: Escreva um programa Go que gere um perfil de CPU de 5 segundos e salve em
cpu.prof. Execute uma função simples (por exemplo, um loop de 1 milhão de iterações) durante esse tempo. - Exercício 2: Crie um servidor HTTP simples que exponha os endpoints de pprof na porta 6060 e um endpoint principal na porta 8080. Teste com
curlpara baixar um perfil de heap. - Exercício 3: Dado o seguinte código ineficiente, use pprof para identificar o gargalo de memória e otimize-o:
- Exercício 4: Explique como você interpretaria um perfil de CPU que mostra que a função
parseconsome 70% do tempo. Que comandos você usaria para investigar? - Exercício 5: Escreva um benchmark usando
testingque compare as duas versões da funçãoconcat(original e otimizada) e execute com-benchmempara ver as alocações.
package main
import (
"os"
"runtime/pprof"
"time"
)
func main() {
f, _ := os.Create("cpu.prof")
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Função simples
for i := 0; i < 1000000; i++ {
_ = i * i
}
time.Sleep(5 * time.Second)
}
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
// Endpoints de pprof em 6060
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Servidor principal
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
}
func concat(parts []string) string {
result := ""
for _, p := range parts {
result += p
}
return result
}
strings.Builder para evitar múltiplas alocações:import "strings"
func concat(parts []string) string {
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
go tool pprof cpu.prof e depois list parse para ver as linhas exatas onde o tempo é gasto. Também poderia usar top para ver o ranking geral e web para gerar um gráfico visual.package concat
import (
"strings"
"testing"
)
func concat(parts []string) string {
result := ""
for _, p := range parts {
result += p
}
return result
}
func concatBuilder(parts []string) string {
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
func BenchmarkConcat(b *testing.B) {
parts := []string{"a", "b", "c", "d", "e"}
for i := 0; i < b.N; i++ {
concat(parts)
}
}
func BenchmarkConcatBuilder(b *testing.B) {
parts := []string{"a", "b", "c", "d", "e"}
for i := 0; i < b.N; i++ {
concatBuilder(parts)
}
}
Execute com go test -bench=. -benchmem.