Se uma aplicação Go apresenta picos de latência, goroutines paradas, CPU ociosa apesar de fila crescente ou pausas difíceis de explicar, use go tool trace quando a pergunta depender da ordem dos eventos. O execution trace registra uma linha do tempo do runtime: execução e bloqueio de goroutines, decisões do scheduler, garbage collection, syscalls, rede e tarefas instrumentadas pela aplicação.
A recomendação direta é: comece pelas métricas para delimitar o sintoma, use pprof para localizar onde os recursos se concentram e recorra ao trace para entender quando e por que o fluxo deixou de avançar. Para inspecionar variáveis e caminhar pelo código de uma execução específica, Delve continua sendo a ferramenta certa.
Este guia mostra como gerar traces em testes e serviços, abrir a interface, interpretar scheduler e goroutines, instrumentar operações com runtime/trace e usar o Flight Recorder com segurança.
O que o execution trace responde
Um perfil de CPU agrega amostras por função. Isso é excelente para descobrir que uma função consumiu 35% do processador, mas não preserva necessariamente a história completa de uma requisição. O trace mantém a dimensão temporal e pode revelar situações como:
- dezenas de goroutines prontas, mas sem tempo de CPU suficiente;
- uma operação serializando atrás de um mutex;
- workers ociosos porque a fila não entrega trabalho;
- goroutines bloqueadas em channel, rede ou syscall;
- períodos de garbage collection coincidindo com uma cauda de latência;
- baixa utilização dos processadores lógicos apesar de trabalho pendente;
- uma tarefa lógica migrando por várias goroutines;
- uma requisição rápida na média, mas lenta quando disputa um recurso específico.
| Ferramenta | Melhor pergunta |
|---|---|
| Métricas | O problema existe, quando começou e qual dimensão foi afetada? |
pprof | Onde CPU, heap, bloqueio ou contenção se concentram? |
go tool trace | Como os eventos se encadearam ao longo do tempo? |
| Delve | Qual é o valor desta variável neste ponto da execução? |
| Race detector | Houve acesso concorrente inseguro à memória? |
| OpenTelemetry | Como uma requisição atravessou serviços e dependências? |
O execution trace não substitui tracing distribuído. O OpenTelemetry em Go acompanha operações entre processos e serviços; runtime/trace detalha o comportamento do runtime dentro de um processo Go. Os dois podem investigar a mesma latência em escalas diferentes.
Como gerar um trace com go test
A forma mais simples de aprender é capturar um teste ou benchmark reproduzível. O suporte já faz parte de go test:
go test -trace=trace.out ./internal/checkout
Depois abra o arquivo:
go tool trace trace.out
O comando inicia um servidor local e exibe a URL da interface. Em uma máquina sem navegador automático, copie o endereço informado para o seu navegador.
Para reduzir ruído, execute somente o teste relevante:
go test \
-run '^TestCheckout_ReservaConcorrente$' \
-count=1 \
-trace=trace.out \
./internal/checkout
go tool trace trace.out
-count=1 evita reutilizar resultado do cache de testes. Se o problema aparece apenas com corrida ou carga, não assuma que um único caso pequeno reproduzirá o comportamento. Construa um teste que preserve a concorrência necessária, mas mantenha o período curto.
Para benchmark:
go test \
-run '^$' \
-bench '^BenchmarkDispatcher$' \
-benchtime=3s \
-trace=trace.out \
./internal/worker
A página de testes em Go explica seleção de casos, benchmarks e ferramentas auxiliares. O trace deve complementar um teste que já representa o defeito, não esconder uma reprodução instável.
Como coletar trace de um serviço
Se o serviço importa net/http/pprof em um servidor administrativo, o endpoint /debug/pprof/trace disponibiliza uma coleta ao vivo. Exemplo de captura curta:
curl --fail --show-error \
-o trace.out \
"http://127.0.0.1:6060/debug/pprof/trace?seconds=5"
go tool trace trace.out
Cinco segundos sob carga representativa costumam ensinar mais do que um arquivo enorme coletado fora da janela do problema. Se a ocorrência dura pouco, sincronize a captura com um teste de carga, um alerta ou uma operação conhecida.
A porta administrativa não deve compartilhar exposição com a API pública. Use 127.0.0.1, rede privada, VPN, túnel ou port-forward autenticado. O mesmo cuidado vale para endpoints de CPU, heap, mutex e goroutines descritos no guia de pprof seguro em produção.
Quando não houver HTTP administrativo, uma aplicação standalone pode iniciar e encerrar a coleta pelo pacote runtime/trace:
package main
import (
"log"
"os"
"runtime/trace"
)
func main() {
f, err := os.Create("trace.out")
if err != nil {
log.Fatal(err)
}
defer f.Close()
if err := trace.Start(f); err != nil {
log.Fatal(err)
}
defer trace.Stop()
runWorkload()
}
func runWorkload() {
// Execute aqui somente a janela que precisa ser investigada.
}
Não envolva toda a vida de um servidor nesse Start por padrão. Traces longos aumentam custo, tamanho do arquivo e dificuldade de análise.
Como ler a interface do go tool trace
A interface pode variar entre versões do toolchain, mas o raciocínio central permanece. Comece pelas visões de alto nível e só depois amplie uma faixa temporal específica.
Visão por processador lógico
A timeline por processador mostra quando cada P do scheduler executou trabalho. Procure:
- longos espaços vazios enquanto existe fila de trabalho;
- execução concentrada em poucos Ps;
- alternância excessiva entre goroutines;
- uma fase paralela que se torna serial;
- intervalos dominados por GC ou syscalls.
Um espaço vazio não prova defeito. Talvez o serviço estivesse realmente sem trabalho. Compare a timeline com throughput, tamanho da fila, latência e o gerador de carga.
Goroutines
A visão de goroutines permite acompanhar estados como execução, runnable e bloqueio. Uma goroutine runnable está pronta, mas aguarda escalonamento. Muitas goroutines runnable por tempo prolongado podem indicar saturação de CPU ou competição excessiva.
Já uma goroutine bloqueada pode estar esperando:
- recebimento ou envio em channel;
sync.Mutexou outra sincronização;- timer;
- syscall;
- rede;
- conclusão de outra tarefa.
O estado sozinho não basta. Um worker esperar no channel quando não há jobs é saudável; centenas de produtores bloqueados porque nenhum consumidor avança podem indicar backpressure ausente, deadlock ou capacidade insuficiente.
GC e heap
Eventos de garbage collection ajudam a correlacionar alocação, trabalho do coletor e pausas com a latência. Não conclua que “o GC é o problema” apenas porque ele aparece. Pergunte:
- a aplicação passou a alocar mais antes da cauda de latência?
- o heap cresceu por retenção ou por volume temporário?
- a CPU já estava saturada?
- o evento coincide repetidamente com o sintoma?
- um perfil de heap ou
alloc_spaceconfirma a hipótese?
O trace mostra o encadeamento; o pprof mostra quais funções geraram ou retiveram alocações. Use ambos para evitar uma otimização baseada em coincidência visual.
Syscalls e rede
Uma goroutine pode liberar o processador enquanto espera I/O, mas uma sequência de chamadas lentas ainda afeta a operação lógica. Quando a timeline aponta rede ou syscall, investigue timeout, DNS, pool de conexões, servidor remoto e cancelamento. Para detalhar DNS, conexão, TLS e obtenção de conexão em um cliente HTTP, use httptrace.
Instrumentando tarefas, regiões e logs
O runtime já produz muitos eventos, mas nomes de domínio tornam o trace mais fácil de interpretar. O pacote runtime/trace oferece tarefas, regiões e logs associados a context.Context.
Uma task representa uma operação lógica que pode atravessar várias goroutines. Uma region marca uma etapa dentro dessa operação. Como trace.WithRegion não retorna erro, capture explicitamente o resultado da função instrumentada:
package checkout
import (
"context"
"runtime/trace"
)
func Process(ctx context.Context, orderID string) error {
ctx, task := trace.NewTask(ctx, "checkout")
defer task.End()
trace.Log(ctx, "order_id", orderID)
var reserveErr error
trace.WithRegion(ctx, "reserve-stock", func() {
reserveErr = reserveStock(ctx, orderID)
})
if reserveErr != nil {
return reserveErr
}
var paymentErr error
trace.WithRegion(ctx, "authorize-payment", func() {
paymentErr = authorizePayment(ctx, orderID)
})
return paymentErr
}
A instrumentação não deve alterar o contrato da função nem esconder erros. Mantenha poucos nomes de tipo, como checkout, reserve-stock e authorize-payment. Não gere regionType ou taskType com ID de pedido; use trace.Log para o identificador. Tipos com cardinalidade ilimitada dificultam agregação e análise.
Para uma região cujo início e fim ficam no mesmo fluxo, também existe StartRegion:
region := trace.StartRegion(ctx, "encode-response")
defer region.End()
A região deve terminar na mesma goroutine em que começou, e regiões aninhadas precisam encerrar na ordem correta.
Estudo de caso: CPU ociosa e fila crescendo
Imagine um worker pool com 32 consumidores. O dashboard mostra fila crescente, mas CPU abaixo de 30%. Um perfil de CPU não apresenta função dominante. A timeline revela:
- os workers recebem jobs;
- todos chegam a uma seção protegida pelo mesmo mutex;
- uma goroutine faz I/O de rede enquanto segura o lock;
- as outras permanecem bloqueadas;
- os processadores ficam ociosos porque o trabalho foi serializado.
A hipótese deixa de ser “precisamos de mais workers”. Adicionar consumidores só aumentaria a fila no mutex. A correção provável é mover o I/O para fora da seção crítica, reduzir o estado protegido ou dividir o lock por chave.
Antes do deploy:
go test ./...
go test -race ./...
go test -bench '^BenchmarkDispatcher$' -benchmem ./internal/worker
Depois, repita a carga e gere outro trace com duração comparável. A validação não é “a timeline ficou bonita”; é throughput maior, latência menor e ausência de regressão funcional.
Estudo de caso: goroutine leak ou operação legítima?
Um contador crescente de goroutines pode significar leak, mas o trace ajuda a separar causas. Considere chamadas a uma dependência sem timeout. Cada requisição inicia uma goroutine que fica aguardando resposta. Se o cliente não propaga cancelamento, o número cresce durante a degradação remota.
O caminho de correção inclui:
- propagar
context.Context; - definir timeout no ponto que conhece o orçamento;
- interromper trabalho quando
ctx.Done()fechar; - limitar concorrência;
- evitar goroutine “fire and forget” sem proprietário;
- acompanhar quantidade e idade das operações.
Veja context, timeout e cancelamento em Go e o guia de concorrência com goroutines e channels para os padrões de ciclo de vida.
Flight Recorder: capturando o passado
Desde Go 1.25, runtime/trace inclui o Flight Recorder. Em vez de iniciar a coleta somente depois do alerta, ele mantém uma janela móvel dos eventos recentes e permite salvar um snapshot quando um gatilho dispara.
A API atual usa trace.NewFlightRecorder com trace.FlightRecorderConfig, Start, WriteTo e Stop:
package diagnostics
import (
"log"
"os"
"runtime/trace"
"sync"
"time"
)
var snapshotOnce sync.Once
func StartFlightRecorder() *trace.FlightRecorder {
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
MinAge: 2 * time.Second,
MaxBytes: 16 << 20,
})
if err := fr.Start(); err != nil {
log.Printf("flight recorder indisponível: %v", err)
}
return fr
}
func CaptureSlowRequest(fr *trace.FlightRecorder, elapsed time.Duration) {
if !fr.Enabled() || elapsed < 500*time.Millisecond {
return
}
snapshotOnce.Do(func() {
f, err := os.Create("slow-request.trace")
if err != nil {
log.Printf("criando snapshot: %v", err)
return
}
defer f.Close()
if _, err := fr.WriteTo(f); err != nil {
log.Printf("gravando snapshot: %v", err)
}
})
}
MinAge define uma retenção mínima desejada; MaxBytes limita a janela por tamanho e pode prevalecer sobre a idade. Ajuste os valores com teste de carga, porque um serviço movimentado produz dados mais rapidamente. Somente uma goroutine pode executar WriteTo por vez, e atualmente apenas um Flight Recorder pode estar ativo no processo.
Não crie um arquivo novo para toda requisição lenta. Use cooldown, amostragem, limite de snapshots, armazenamento controlado e rotação. O artigo sobre Flight Recorder no Go 1.25 apresenta o conceito; para detalhes de API e mudanças, consulte sempre a documentação da versão do seu toolchain.
Trace, pprof ou tracing distribuído: como escolher
Use este roteiro:
- Existe uma regressão mensurável? Comece por métricas e logs.
- CPU ou memória está alta? Colete pprof primeiro.
- A latência cresce sem função dominante? Trace pode revelar espera e serialização.
- O problema atravessa serviços? Use OpenTelemetry para localizar o trecho e runtime trace no processo suspeito.
- Precisa examinar valor e fluxo de uma execução reproduzível? Use Delve.
- O evento raro já terminou quando o alerta chega? Considere Flight Recorder.
A comparação com outras linguagens também ajuda a enxergar o método, não apenas a ferramenta. O guia de profiling de Rust em produção mostra como separar CPU, memória e latência em outro ecossistema de sistemas.
Erros comuns ao usar go tool trace
Coletar tempo demais
Arquivo maior não significa diagnóstico melhor. Capture uma janela curta e relevante. Traces extensos consomem mais recursos e dificultam encontrar o evento útil.
Interpretar correlação como causa
GC, syscall ou troca de goroutine perto de uma requisição lenta não prova causalidade. Repita a coleta, compare casos rápidos e lentos e confirme com pprof, métricas ou teste controlado.
Instrumentar IDs como tipos
trace.NewTask(ctx, "checkout-12345") cria cardinalidade desnecessária. Use tipo estável checkout e registre o ID com trace.Log.
Expor o endpoint administrativo
/debug/pprof/trace não deve ficar aberto na internet. O risco não é apenas consumo de recursos: artefatos de diagnóstico podem revelar estrutura interna e dados adicionados pela instrumentação.
Otimizar antes de reproduzir
Uma timeline complexa convida a mudanças intuitivas. Primeiro formule uma hipótese, depois altere uma variável e repita a medição sob cenário comparável.
Confundir trace do runtime com trace distribuído
runtime/trace não substitui propagação de spans entre serviços. Ele explica o processo Go; OpenTelemetry explica a jornada entre componentes.
Checklist de investigação
Antes da coleta:
- defina o sintoma e a métrica afetada;
- registre versão do Go, commit e configuração relevante;
- escolha uma janela curta;
- proteja o endpoint ou mecanismo de captura;
- reproduza carga representativa.
Durante a análise:
- compare timeline com métricas;
- diferencie runnable de bloqueada;
- procure transição de paralelo para serial;
- correlacione GC com perfil de alocação;
- identifique channels, locks, rede e syscalls;
- verifique tarefas e regiões instrumentadas.
Depois da hipótese:
- adicione ou preserve um teste de regressão;
- rode
go test ./...ego test -race ./...quando aplicável; - faça benchmark antes e depois;
- gere um segundo trace comparável;
- remova acessos temporários;
- armazene ou descarte o artefato conforme a política de segurança.
Perguntas frequentes
Para que serve o go tool trace?
Ele abre um execution trace do runtime e mostra eventos ao longo do tempo. É especialmente útil para latência, scheduler, goroutines, GC, syscalls e operações que atravessam várias goroutines.
Qual é a diferença entre go tool trace e pprof?
pprof agrega amostras para mostrar onde os recursos se concentram. O trace preserva a linha do tempo para mostrar como e quando os eventos se relacionaram. Muitas investigações usam os dois.
Como gerar um trace de um teste Go?
Use go test -trace=trace.out ./pacote e abra com go tool trace trace.out. Se possível, selecione um teste com -run e desative o cache com -count=1.
runtime/trace pode ser usado em produção?
Sim, com coleta curta, acesso protegido, gatilho controlado e validação de overhead. Trate o arquivo como potencialmente sensível e evite exposição pública do endpoint administrativo.
Quando usar o Flight Recorder?
Quando o problema é raro e só pode ser detectado depois que a janela crítica começou. O Flight Recorder mantém os eventos recentes e permite salvar o passado imediato quando um gatilho confiável dispara.
Referências oficiais
- Diagnostics — Go Documentation
- Pacote runtime/trace
- More powerful Go execution traces
- Flight Recorder in Go 1.25
Conclusão
go tool trace é a ferramenta certa quando a média e os perfis agregados não contam a história inteira. Ele mostra como goroutines, scheduler, GC, syscalls e regiões da aplicação se organizam no tempo — justamente a dimensão que desaparece quando você olha apenas para totais.
Use-o como parte de um método: métricas para detectar, pprof para localizar, trace para ordenar os eventos, testes e benchmarks para validar. Com coletas curtas, instrumentação de baixa cardinalidade e endpoints protegidos, problemas de latência deixam de ser uma sequência de palpites e viram hipóteses verificáveis.