← Voltar para o blog

go tool trace: Latência, Scheduler e Goroutines em Go

Aprenda go tool trace e runtime/trace para investigar picos de latência, scheduler, goroutines, GC, syscalls, regiões, tarefas, testes e produção em Go.

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.
FerramentaMelhor pergunta
MétricasO problema existe, quando começou e qual dimensão foi afetada?
pprofOnde CPU, heap, bloqueio ou contenção se concentram?
go tool traceComo os eventos se encadearam ao longo do tempo?
DelveQual é o valor desta variável neste ponto da execução?
Race detectorHouve acesso concorrente inseguro à memória?
OpenTelemetryComo 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.Mutex ou 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:

  1. a aplicação passou a alocar mais antes da cauda de latência?
  2. o heap cresceu por retenção ou por volume temporário?
  3. a CPU já estava saturada?
  4. o evento coincide repetidamente com o sintoma?
  5. um perfil de heap ou alloc_space confirma 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:

  1. os workers recebem jobs;
  2. todos chegam a uma seção protegida pelo mesmo mutex;
  3. uma goroutine faz I/O de rede enquanto segura o lock;
  4. as outras permanecem bloqueadas;
  5. 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:

  1. Existe uma regressão mensurável? Comece por métricas e logs.
  2. CPU ou memória está alta? Colete pprof primeiro.
  3. A latência cresce sem função dominante? Trace pode revelar espera e serialização.
  4. O problema atravessa serviços? Use OpenTelemetry para localizar o trecho e runtime trace no processo suspeito.
  5. Precisa examinar valor e fluxo de uma execução reproduzível? Use Delve.
  6. 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 ./... e go 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

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.