Os três pilares tradicionais de observabilidade são logs, métricas e traces. O profiling contínuo emerge como um quarto pilar — e é o único sinal que responde à pergunta "qual linha de código está consumindo CPU ou memória em produção, agora?". Métricas dizem "CPU está alta". Traces dizem "este request demorou 800ms". Profiling diz "68% do CPU está sendo consumido pela função serialize_response na linha 142 do arquivo handlers.go". A diferença entre profiling ad-hoc e profiling contínuo é temporal: profiling ad-hoc é executado manualmente quando você suspeita de um problema; profiling contínuo coleta dados permanentemente em produção, com histórico — você pode responder "a regressão que apareceu após o deploy das 14:30 está em qual função?" sem precisar reproduzir o problema.
Como Funciona o Sampling Profiler
Um sampling profiler interrompe a execução do programa em intervalos regulares (tipicamente 100-500 vezes por segundo), captura o stack trace do thread ou goroutine naquele instante, e agrega essas amostras ao longo do tempo. Funções que aparecem com mais frequência nos stack traces consomem mais CPU — a frequência de aparição é o estimador de custo.
Tipos de profiling
- CPU profiling: qual função está consumindo tempo de CPU? Captura stack traces durante execução ativa do thread.
- Wall-clock profiling: qual função está bloqueando o thread? Inclui espera em I/O, locks e sleep — diferente do CPU profiling que só conta tempo de CPU ativo. Mais relevante para latência de endpoint.
- Heap / Memory profiling: quais funções estão alocando memória? Identifica memory leaks e alocações desnecessárias que causam pressão de GC.
- Goroutine / thread profiling: estado de todas as goroutines — goroutines bloqueadas em lock ou I/O, útil para diagnosticar deadlocks e starvation.
- Mutex / lock profiling: onde há contenção de mutex? Identifica hot locks que serializam execução paralela.
- Alloc profiling (.NET/Java): objetos alocados com maior frequência — auxilia tuning de GC e identificação de boxing desnecessário.
Um sampling profiler bem implementado tem overhead de 1-3% de CPU — aceitável em produção para a maioria dos workloads. Profilers instrumentados (que injetam código em cada entrada/saída de função) têm overhead de 5-30% e não são adequados para produção contínua. A escolha de sampling rate importa: 100 amostras/s é padrão; aumentar para 500 captura mais granularidade mas aumenta o overhead linearmente.
Flame Graphs
Flame graphs são a visualização padrão de profiling, criada por Brendan Gregg em 2011. Cada caixa representa uma função na call stack. A largura é proporcional ao número de vezes que a função apareceu nas amostras. As caixas são empilhadas: a função mais abaixo é o ponto de entrada (ex: main), as funções acima são chamadas subsequentes.
Como ler um flame graph
- Eixo X: proporção de tempo de CPU — não é ordem temporal. Funções à esquerda não executaram antes das da direita — a ordem horizontal é apenas alfabética ou aleatória por conveniência visual.
- Eixo Y: profundidade da call stack — mais alto significa função mais profunda na cadeia de chamadas.
- Largura: quanto mais larga a caixa, mais CPU aquela função consumiu diretamente ou por suas callees.
- Platôs (flat tops): uma caixa larga no topo da stack sem filhos de largura significativa — essa é a função que está efetivamente consumindo CPU. É o candidato primário de otimização.
# Exemplo de análise de flame graph — identificando hot path
#
# processRequest [████████████████████████████████████████] 100%
# ├─ parseJSON [██████████████████] 45%
# │ └─ unmarshal [████████████████] 40% ← PLATÔ — hot function
# ├─ queryDB [████████████] 30%
# │ └─ sql.Query [███████████] 28% ← gasta tempo em I/O (wall-clock)
# └─ renderHTML [████████] 20%
# └─ template.Execute [███████] 18% ← platô secundário
#
# Ação: unmarshal() consome 40% do CPU — candidato a otimização.
# Opções: JSON parsing customizado, caching de schemas parseados,
# ou substituir JSON por protobuf para reduzir deserialização.
Differential flame graphs
Um differential flame graph compara dois profiles — tipicamente antes e depois de um deploy. Caixas vermelhas: mais CPU no profile novo (regressão). Caixas azuis: menos CPU (melhoria). Permite identificar regressões introduzidas por uma mudança de código sem análise manual comparativa — você vê imediatamente qual função ficou mais cara após o deploy das 14:30.
Pyroscope (Grafana)
Pyroscope (adquirido pela Grafana em 2023) é o backend open source de continuous profiling mais adotado. Integra-se nativamente ao stack Grafana, suporta múltiplas linguagens via SDKs ou eBPF, e armazena profiles em object storage (S3/GCS) — custo de storage muito baixo.
# docker-compose.yml — Pyroscope standalone
services:
pyroscope:
image: grafana/pyroscope:latest
ports:
- "4040:4040"
command:
- "-config.file=/etc/pyroscope/config.yaml"
volumes:
- ./pyroscope.yaml:/etc/pyroscope/config.yaml
# pyroscope.yaml
storage:
backend: s3
s3:
bucket_name: my-profiles
region: us-east-1
# Grafana datasource: adicione Pyroscope como datasource
# No Grafana Explore: selecione Pyroscope, query por service_name
# + time range → flame graph interativo do período
# Com Grafana 10+: correlação spans (Tempo) → profiles (Pyroscope)
# via service.name como chave de correlação
eBPF Profiling — Sem Modificar a Aplicação
eBPF permite executar programas sandboxed no kernel Linux. Para profiling, isso significa capturar stack traces de qualquer processo sem modificar código, sem SDK, e sem reiniciar o processo — o profiler opera no nível do kernel observando todos os processos simultaneamente.
- Zero instrumentação: profila processos existentes sem mudança de código ou deploy.
- Cross-language: um único agente eBPF profila processos em Go, Python, C++, Node.js e Java simultaneamente no mesmo host.
- Sistema completo: captura tempo em kernel-space (syscalls, I/O, network) além de user-space — revela onde o processo realmente gasta tempo, incluindo chamadas de sistema.
- Overhead muito baixo: ~1% ou menos de overhead de CPU.
Requer kernel Linux 4.9+ (preferencialmente 5.8+). Para linguagens com JIT (Java, .NET, Node.js): stack traces podem ser incompletos sem integração com o runtime para resolver símbolos JIT — o profiler vê endereços de memória, não nomes de função. Para Python/Ruby: interpretadores gerenciados precisam de suporte extra (frame pointer restoration). Não funciona em Windows. Ferramentas: Grafana Beyla (eBPF auto-instrumentação), Parca (CNCF, eBPF-first), Cilium/Pixie (K8s networking + profiling).
Profiling por Linguagem
// Package: Pyroscope.Profiler
// Program.cs — profiling contínuo com Pyroscope
using Pyroscope;
PyroscopeAgent.Start(new PyroscopeConfig
{
ApplicationName = "order-service",
ServerAddress = "http://pyroscope:4040",
Tags = new Dictionary<string, string>
{
{ "environment", "production" },
{ "version", "1.2.0" },
{ "region", "us-east-1" },
},
ProfilingTypes = new[]
{
ProfileType.Cpu,
ProfileType.Alloc, // alocações — auxilia GC tuning
ProfileType.Exception, // exceções como eventos de profiling
},
SampleRate = 100, // 100 amostras por segundo
});
// Anotar spans de negócio para segmentar o flame graph
// Permite ver o perfil apenas durante uma operação específica
using (Profiler.NewSpan("process-order"))
{
await ProcessOrderAsync(request);
}
// Analisar heap manualmente (dev/staging):
// dotnet-dump collect --process-id $(pgrep dotnet)
// dotnet-dump analyze <dumpfile>
// > dumpheap -stat ← objetos por tipo com tamanho total
// > gcroot <address> ← por que este objeto não foi coletado?
O Pyroscope .NET SDK usa o profiler nativo do CLR com baixo overhead. Tags permitem differential profiling entre versões — compare o flame graph de "version=1.1.0" com "version=1.2.0" diretamente no Grafana para identificar regressões.
# Package: pyroscope-io
import pyroscope
pyroscope.configure(
application_name="order-service",
server_address="http://pyroscope:4040",
tags={
"environment": "production",
"version": "1.2.0",
},
oncpu=True, # CPU profiling
native=False, # True = profila extensões C nativas também
)
# Context manager para anotar seções de código
with pyroscope.tag_wrapper({"endpoint": "/api/orders"}):
result = process_order(request)
# py-spy — profiling sem modificar código (externo ao processo)
# Útil para debugging ad-hoc em produção sem redeploy:
#
# py-spy top --pid 12345
# → top-like de funções (atualiza em tempo real)
#
# py-spy record -o profile.svg --pid 12345 --duration 30
# → grava 30s e exporta flame graph SVG interativo
#
# Em Kubernetes (requer shareProcessNamespace: true):
# kubectl exec -it <pod> -- py-spy top --pid 1
#
# Diagnosticar deadlock ou hang:
# py-spy dump --pid 12345
# → dump de todos os threads com stack trace atual
py-spy não requer modificação do código e pode profilear qualquer processo Python em execução — útil para incidentes em produção sem redeploy. O Pyroscope SDK é para profiling contínuo automatizado.
import (
_ "net/http/pprof" // registra handlers /debug/pprof/*
"github.com/grafana/pyroscope-go"
)
func main() {
// pprof em porta interna — nunca expor publicamente
go http.ListenAndServe("localhost:6060", nil)
profiler, _ := pyroscope.Start(pyroscope.Config{
ApplicationName: "order-service",
ServerAddress: "http://pyroscope:4040",
Tags: map[string]string{
"environment": os.Getenv("ENV"),
"version": version,
},
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects, // objetos alocados
pyroscope.ProfileAllocSpace, // bytes alocados
pyroscope.ProfileInuseObjects, // objetos vivos no heap
pyroscope.ProfileInuseSpace, // bytes vivos no heap
pyroscope.ProfileGoroutines,
pyroscope.ProfileMutexCount,
pyroscope.ProfileMutexDuration,
pyroscope.ProfileBlockCount,
pyroscope.ProfileBlockDuration,
},
})
defer profiler.Stop()
// Anotar spans de negócio
pyroscope.TagWrapper(ctx,
pyroscope.Labels("operation", "process-order"),
func() { processOrder(ctx, req) },
)
}
// Profiling manual via CLI:
// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
// → CPU profile 30s, abre UI interativa
//
// go tool pprof -http=:8080 profile.pb.gz
// → UI web com flame graph, top, tree, source view
//
// Diagnosticar goroutine leak:
// curl localhost:6060/debug/pprof/goroutine?debug=2
// → dump de todas as goroutines com stack trace
Go tem o pprof mais maduro de qualquer linguagem mainstream. ProfileInuseSpace vs ProfileAllocSpace: InuseSpace mostra o heap atual (útil para memory leaks), AllocSpace mostra alocações totais (útil para otimizar pressão de GC).
Casos de Uso Reais
Detectar regressão de performance após deploy
Com profiling contínuo, você compara o differential flame graph antes e depois de um deploy. Sem profiling contínuo, você detecta a regressão via métricas (CPU aumentou 30%) mas não sabe onde no código — precisa reproduzir localmente com profiling ad-hoc, o que raramente replica o workload de produção.
Memory leaks em produção
Memory profiling captura quais funções estão alocando mais memória ao longo do tempo. Com profiling contínuo histórico, você identifica quando o leak começou (após qual deploy o heap cresceu linearmente) e quais allocators são responsáveis — a correlação temporal com deploys é a chave do diagnóstico.
Otimização de custo de infraestrutura
Profiling em toda a frota revela ineficiências de CPU que se multiplicam por centenas de instâncias. Exemplo real: função de serialização JSON desnecessariamente cara em Go consumindo 15% de CPU em todos os pods. Substituindo por uma biblioteca mais eficiente, a frota precisou de 15% menos CPUs — economia identificada por profiling, não por revisão de código especulativa.
GC tuning
Em linguagens com GC (Java, .NET, Go), o profiling de alocação revela padrões que causam pressão de GC: alocações frequentes de objetos de curta vida, boxing de value types desnecessários, slices crescendo com realloc frequente. Reduzir alocações é frequentemente mais eficaz que ajustar parâmetros do GC.
Decisões de engenharia
Pyroscope: melhor integração com Grafana stack (Tempo, Loki, Mimir), SDKs maduros para Go, Python e .NET, UI com differential flame graphs. Parca (CNCF): eBPF-first, sem SDK necessário, armazena em Parquet — boa opção para profiling de infra sem tocar código. eBPF direto (perf, bpftrace): para debugging avançado de kernel e situações onde SDKs não atendem. Para a maioria dos times com stack Grafana: Pyroscope. Para Kubernetes sem mudança de código: Parca ou Beyla.
Não é para todos os sistemas. Faz sentido quando: CPU ou memória é custo significativo de infra (profiling pode identificar 10-20% de economia), você tem problemas de performance recorrentes difíceis de reproduzir, ou o workload de produção é muito diferente do de staging. Para sistemas pequenos com CPU idle > 50%, o custo de setup supera o benefício imediato. Comece habilitando apenas CPU profiling — é o mais fácil de interpretar. Adicione heap profiling quando investigar memory leaks ou pressão de GC.
Sim, mas protegido. Exponha em porta interna (localhost:6060) ou behind authn middleware — nunca publicamente. O pprof endpoint em si não é perigoso, mas expõe nomes de funções e estrutura interna do código (information disclosure) e pode ser usado para consumir CPU ao disparar um profile de 60 segundos. Em Kubernetes, acesse via kubectl port-forward para profiling manual. O Pyroscope faz pull do pprof endpoint internamente na rede do cluster.
Pyroscope suporta span-level profiling: passe o trace context para o profiler (via TagWrapper em Go, NewSpan em .NET) e ele associa amostras ao trace ID ativo no momento. No Grafana, o datasource Pyroscope está integrado ao datasource Tempo — você vê o flame graph do processo durante um span específico clicando em "Profiles for this span" na UI do trace. A correlação usa service.name como chave entre os dois datastores.
Como praticar
-
Configure profiling contínuo com Pyroscope em um serviço Go ou Python: instale o SDK, configure push para um Pyroscope local (Docker), e visualize o flame graph no Grafana. Crie uma função artificialmente cara (loop desnecessário, serialização ineficiente) e confirme que ela aparece como platô no flame graph após alguns segundos de tráfego.
Critério: o flame graph aparece no Grafana com dados reais do serviço; a função cara é identificável como platô com largura proporcional ao overhead introduzido; o setup está em docker-compose reproduzível; a função cara foi otimizada e o differential flame graph mostra a melhora (caixas azuis na função modificada). -
Diagnostique um memory leak simulado usando heap profiling: crie um leak intencional (acumular objetos em uma cache global sem limite de tamanho), execute por alguns minutos, e use memory profiling para identificar exatamente qual função está alocando os objetos que não são coletados. Compare o heap antes e depois do leak ser introduzido.
Critério: o profiling de heap mostra crescimento linear de InuseSpace ao longo do tempo; a função responsável pelo acúmulo é identificada pelo profiling (não por revisão de código especulativa); após o fix (adicionar limite de tamanho à cache), o InuseSpace estabiliza; o exercício documenta o processo de diagnóstico como seria feito em produção. -
Use
go tool pprof(ou py-spy para Python) para analisar um processo ao vivo: capture um CPU profile de 30 segundos de uma aplicação sob carga (use um gerador de carga simples —heyouwrk), abra a UI web interativa (-http=:8080), e identifique os 3 maiores consumidores de CPU usando as views "Top", "Flame Graph" e "Source".
Critério: o profile foi capturado com carga real no serviço; as 3 views (Top, Flame Graph, Source) foram exploradas e os 3 maiores consumidores documentados com % de CPU; a view Source mostra as linhas de código específicas onde o CPU é consumido; pelo menos uma função identificada é um candidato óbvio de otimização (ex: alocação desnecessária, I/O síncrono em hot path). -
Compare CPU profiling vs wall-clock profiling no mesmo serviço: escolha um endpoint que faz uma query de banco de dados e mede ambos os tipos de profile. Documente a diferença — quantos % do CPU profile mostra código de aplicação vs quantos % do wall-clock profile está em espera de I/O.
Critério: o CPU profile mostra os hotspots de processamento (parse, serialização, cálculos); o wall-clock profile mostra que a maior parte do tempo está emsql.Queryou equivalente (I/O wait); a documentação explica por que wall-clock é mais útil para diagnosticar latência de endpoint do que CPU profiling; a diferença entre os dois tipos é clara o suficiente para explicar a um colega. -
Gere e analise um differential flame graph entre duas versões do mesmo código: crie uma regressão de performance deliberada em uma função (ex: mudar de map lookup para linear search), profile a versão antes e depois da regressão, e use o Pyroscope ou Grafana para gerar o differential. Identifique e corrija a regressão usando apenas o differential como guia, sem ler o código.
Critério: o differential flame graph mostra claramente caixas vermelhas na função regressada; a regressão é identificada e corrigida usando apenas o visual do differential (sem revisão de código); um segundo differential (antes da regressão vs após o fix) confirma que a melhora é visível; o exercício documenta o processo como seria realizado em um incidente real pós-deploy.
Perguntas de entrevista
Como você lê um flame graph? O que é um "platô" e o que ele indica?
No flame graph, o eixo Y representa profundidade da call stack (mais alto = mais profundo na cadeia de chamadas), e o eixo X representa proporção de CPU — não é temporal, é proporção de amostras coletadas. A largura de cada caixa indica quanto CPU aquela função consumiu, incluindo todas as funções que ela chamou.
Um platô é uma caixa larga que não tem filhos de largura significativa — ela é a função que efetivamente está consumindo CPU, não delegando para outras. Exemplo: se processRequest ocupa 80% da largura mas chama parseJSON que ocupa 70%, e parseJSON chama unmarshal que ocupa 60% sem filhos de destaque — então unmarshal é o platô e é o candidato primário de otimização. O erro comum ao ler flame graphs é focar em funções largas no meio do stack que têm filhos igualmente largos — essas são passagem, não o problema.
Qual a diferença entre CPU profiling e wall-clock profiling?
CPU profiling captura amostras apenas quando o thread está em execução ativa (user-space ou kernel-space em nome do thread). Tempo bloqueado em I/O, sleep, ou esperando lock não aparece. Ideal para identificar hotspots de processamento puro — parse, cálculos, serialização.
Wall-clock profiling captura amostras a cada intervalo de tempo real, independente de o thread estar executando ou bloqueado. Um thread bloqueado em I/O por 2 segundos aparece como 2 segundos no wall-clock profile. Ideal para identificar onde o programa está realmente demorando do ponto de vista do usuário — incluindo latência de banco de dados, espera em locks, e chamadas de rede.
Exemplo prático: um endpoint HTTP que demora 500ms total, sendo 50ms de CPU e 450ms de espera pelo banco de dados. CPU profiling mostra 50ms de processamento — não revela o bottleneck real. Wall-clock profiling mostra que 90% do tempo está em sql.Query — o bottleneck é imediatamente evidente.
Por que profiling contínuo é o "quarto pilar" de observabilidade? O que ele revela que logs, métricas e traces não revelam?
Logs, métricas e traces respondem ao "quê" e "quando": "o request X falhou às 14:30 com timeout" (log), "a latência P99 subiu 40%" (métrica), "o span de banco de dados demorou 800ms" (trace). Eles não respondem ao "onde no código" — qual função, qual linha, qual hot path está causando o problema.
Profiling contínuo responde ao "onde": dado que a latência P99 subiu 40%, o profiling diz "a função deserialize_payload que antes consumia 5% de CPU agora consome 35% — o commit de 14:18 adicionou uma chamada de regex desnecessária nessa função". Sem profiling, você teria que fazer revisão de código especulativa, profiling manual em staging (que não replica o workload de produção), ou adicionar logs em cada função suspeita e redeployar. Com profiling contínuo histórico e differential flame graphs, a causa raiz é encontrada em minutos.
Como você diagnosticaria um memory leak em produção usando profiling?
O processo em etapas: (1) Confirmar o leak via métricas: crescimento linear de uso de memória ao longo do tempo, sem platôs, que não é explicado por crescimento de tráfego. Se o uso de memória cresce mesmo com tráfego estável, é leak. (2) Correlacionar com deploys: usando o profiling contínuo histórico, identificar após qual deploy o crescimento começou — isso aponta para o commit responsável. (3) Analisar heap profiling: comparar o InuseSpace profile (objetos vivos no heap) de uma hora atrás com o atual. A diferença são os objetos que não estão sendo coletados. A view "Top" por InuseSpace mostra quais tipos de objetos ocupam mais memória. (4) Rastrear a alocação: AllocObjects profile mostra quais funções estão alocando mais objetos — frequentemente a função que aloca não é a que retém. Ferramentas como dotnet-dump (GC roots), go tool pprof heap, ou jmap/jstack (Java) mostram por que os objetos não estão sendo coletados (qual referência os está retendo). (5) Validar o fix: após o fix, o InuseSpace deve estabilizar — monitorar por algumas horas para confirmar que não volta a crescer linearmente.
O que é um differential flame graph e como você o usaria para investigar uma regressão de performance após um deploy?
Um differential flame graph compara dois profiles coletados em momentos diferentes — tipicamente antes e depois de um deploy, ou com e sem uma funcionalidade específica. A diferença de largura de cada função é codificada em cor: vermelho significa que a função ficou mais cara no profile novo (mais amostras, mais CPU), azul significa que ficou mais barata. A intensidade da cor é proporcional à magnitude da diferença.
Workflow de investigação de regressão: (1) o alerta de CPU alta ou P99 elevado dispara após o deploy das 14:30; (2) no Grafana/Pyroscope, selecione "Diff" e compare o período antes do deploy (ex: 13:00-14:00) com o período depois (14:30-15:30); (3) o differential flame graph mostra imediatamente quais funções ficaram mais caras em vermelho — essas são as candidatas; (4) a função com mais vermelho no nível mais profundo da stack (sem filhos vermelhos expressivos) é o platô da regressão; (5) inspecione a função no código — frequentemente é uma mudança de algoritmo O(n) para O(n²), alocação desnecessária adicionada, ou lock de escopo ampliado. O processo inteiro tipicamente leva 5-15 minutos, vs horas de revisão de código especulativa sem profiling.
Referências para aprofundar
- artigo Flame Graphs — Brendan Gregg (brendangregg.com, 2011).
- artigo The Flame Graph — Brendan Gregg (ACM Queue, 2016).
- livro Systems Performance: Enterprise and the Cloud — Brendan Gregg (Prentice Hall, 2ª ed. 2020).
- docs Grafana Pyroscope — Documentação oficial — Grafana Labs (2024).
- docs Parca — Continuous Profiling (CNCF) — Parca Project (2024).
- docs Go — net/http/pprof — Go Project (2024).
- docs py-spy — Sampling Profiler for Python — Ben Frederickson (2024).
- docs Profiling C# .NET Applications — Microsoft Learn (2024).
- artigo Continuous Profiling for Production Go Applications — Grafana Labs Blog (2022).
- artigo eBPF — What is eBPF? — eBPF Foundation (2024).
- artigo Off-CPU Analysis — Brendan Gregg (2015).
- docs OpenTelemetry Profiling Signal — OpenTelemetry Project (2024).