Conteúdo

Monitorar métricas de runtime do .NET é essencial para manter aplicações performáticas em produção. O runtime expõe dezenas de métricas através da API System.Diagnostics.Metrics, cobrindo Garbage Collector, Thread Pool, JIT, exceções e uso de recursos. Ferramentas como Datadog, Prometheus e Azure Monitor conseguem coletar e visualizar essas métricas automaticamente, permitindo identificar gargalos antes que afetem usuários.

Insights

  1. GC Pause Time é o vilão silencioso: Pausas frequentes do Garbage Collector podem causar latência perceptível ao usuário, mesmo com CPU e memória aparentemente saudáveis.
  2. Thread Pool Queue Length revela saturação antes do colapso: Quando a fila de work items cresce consistentemente, sua aplicação está prestes a sofrer degradação de performance.
  3. First Chance Exceptions nem sempre são problemas: Exceções capturadas em try/catch também são contabilizadas. O padrão de crescimento importa mais que o número absoluto.
  4. LOH e POH são coletados apenas na Gen2: Objetos grandes (>85KB) e objetos pinados têm custo de coleta maior. Monitore-os separadamente.
  5. Contention Count alto indica locks mal projetados: Contenção de locks é sintoma de código que não escala. Considere estruturas lock-free ou particionamento.

As métricas mais críticas para análise são: GC pause time (impacto direto na latência), thread pool queue length (saturação de processamento), heap size por geração (pressão de memória) e contention count (problemas de concorrência). Entender o que cada métrica representa e como interpretar seus valores transforma dados brutos em decisões de otimização concretas.

Por que monitorar métricas de runtime?

CPU e memória são métricas de infraestrutura. Elas dizem que algo está errado, mas não o quê. Métricas de runtime revelam o comportamento interno da aplicação: como o Garbage Collector está operando, se há contenção de threads, quantas exceções estão sendo lançadas.

Uma aplicação pode ter CPU em 30% e ainda assim apresentar latência alta por pausas excessivas do GC. Pode ter memória disponível e sofrer com thread pool starvation. Sem métricas de runtime, você está diagnosticando no escuro.

Anatomia das métricas de runtime do .NET

O .NET organiza métricas de runtime em categorias distintas. Vamos analisar cada uma:

1. Métricas de Garbage Collector

O GC é responsável por liberar memória automaticamente. Suas métricas são as mais críticas para performance:

MétricaTipoO que mede
dotnet.gc.collectionsCounterNúmero de coletas por geração (gen0, gen1, gen2)
dotnet.gc.heap.total_allocatedCounterTotal de bytes alocados desde o início
dotnet.gc.pause.timeCounterTempo total em pausas de GC
dotnet.gc.last_collection.heap.sizeGaugeTamanho do heap por geração após última coleta
dotnet.gc.last_collection.heap.fragmentation.sizeGaugeFragmentação do heap

Como interpretar:

  • Gen0 collections altas, Gen2 baixas: Comportamento saudável. Objetos de vida curta são coletados rapidamente.
  • Gen2 collections frequentes: Problema. Objetos estão sobrevivendo muito tempo ou há muitas alocações no Large Object Heap.
  • GC pause time crescendo: A aplicação está pausando cada vez mais para coletas. Impacta diretamente a latência.
  • Fragmentação alta: Memória está fragmentada. Pode causar alocações mais lentas e OutOfMemoryException mesmo com memória disponível.

2. Métricas de Thread Pool

O Thread Pool gerencia threads para operações assíncronas. Saturação aqui causa lentidão generalizada:

MétricaTipoO que mede
dotnet.thread_pool.thread.countGaugeThreads atualmente no pool
dotnet.thread_pool.queue.lengthGaugeWork items aguardando processamento
dotnet.thread_pool.work_item.countCounterWork items completados

Como interpretar:

  • Queue length consistentemente > 0: Work items estão acumulando. O pool não consegue processar na velocidade que chegam.
  • Thread count crescendo rapidamente: O runtime está criando threads para compensar demanda. Pode indicar bloqueio síncrono em código async.
  • Thread count no máximo e queue crescendo: Thread pool starvation. A aplicação vai degradar severamente.

3. Métricas de Exceções

Exceções têm custo de performance e indicam problemas na lógica:

MétricaTipoO que mede
dotnet.exceptionsCounterTotal de exceções lançadas, por tipo

Como interpretar:

  • Crescimento constante de um tipo específico: Investigue a causa raiz.
  • OperationCanceledException alta: Normal em aplicações com cancelamento. Não necessariamente um problema.
  • NullReferenceException ou ArgumentException: Bugs no código. Corrija imediatamente.

4. Métricas de Contenção

Contenção ocorre quando múltiplas threads tentam adquirir o mesmo lock:

MétricaTipoO que mede
dotnet.monitor.lock_contentionsCounterVezes que uma thread esperou por um lock

Como interpretar:

  • Contention count baixo e estável: Saudável.
  • Contention count crescendo com carga: Locks estão se tornando gargalo. Considere redesenhar para estruturas concorrentes ou lock-free.

Coletando Métricas com OpenTelemetry

A forma moderna de coletar métricas é via OpenTelemetry. O código abaixo configura coleta completa de métricas de runtime:

Pacotes NuGet necessários:

Coletando Métricas com Datadog

O Datadog coleta métricas de runtime automaticamente quando você habilita o tracer:

Variáveis de ambiente necessárias:

Dockerfile com Datadog:

Criando métricas customizadas

Além das métricas de runtime, você pode criar métricas específicas do seu domínio:

Dashboard do Datadog para referência

Baseado na imagem de referência do Datadog, estas são as métricas essenciais para um dashboard de runtime .NET:

.NET – Como Analisar e Interpretar as Métricas de Runtime do .NET

Painel 1 – Recursos do Processo:

  • .NET process CPU usage (user vs system)
  • .NET process memory usage (working set)
  • Memory load percentage

Painel 2 – Garbage Collector:

  • GC collections count (por geração)
  • GC heap size (por geração + LOH + POH)
  • GC pause time

Painel 3 – Threads e Concorrência:

  • Thread pool thread count
  • Thread pool queue length
  • Contention count
  • Contention time

Painel 4 – Exceções e Erros:

  • First chance exceptions (por tipo)
  • Unhandled exceptions

Painel 5 – ASP.NET Core (se aplicável):

  • Requests per second
  • Active connections
  • Request duration (p50, p95, p99)

Alertas Recomendados

Configure alertas para estas condições:

CondiçãoSeveridadeAção
GC pause time > 200ms em 5minWarningInvestigar alocações
GC pause time > 500ms em 5minCriticalEscalar imediatamente
Thread pool queue > 100 por 1minWarningVerificar bloqueios
Thread pool queue > 500 por 1minCriticalAdicionar capacidade
Gen2 collections > 10/minWarningRevisar ciclo de vida de objetos
Contention count crescendo 50%/horaWarningRevisar locks
Memory load > 80%WarningAvaliar memory leak
Memory load > 95%CriticalRisco de OOM

Diagnóstico

Exemplo Completo de uma API com Observabilidade

Conclusão

Métricas de runtime são a diferença entre reagir a problemas e preveni-los. Com as ferramentas certas e o conhecimento de como interpretar cada métrica, você transforma dados em ações concretas de otimização.

Comece habilitando a coleta de métricas em uma aplicação. Observe o comportamento normal por alguns dias. Depois, configure alertas para desvios. Em poucas semanas, você terá visibilidade completa do comportamento interno das suas aplicações .NET.

FAQ: Perguntas Frequentes

1. Qual a diferença entre GC pause time e GC collection count?

GC collection count indica quantas vezes o Garbage Collector executou. GC pause time indica quanto tempo a aplicação ficou pausada durante essas coletas. Você pode ter muitas coletas rápidas (bom) ou poucas coletas lentas (ruim). O pause time é mais relevante para latência.

2. Por que minha aplicação tem alto contention count mesmo com poucos usuários?

Contention ocorre quando threads disputam o mesmo lock. Pode acontecer com poucos usuários se houver um lock global (singleton mal implementado, cache com lock exclusivo, logging síncrono). Use o profiler para identificar qual lock está causando contenção.

3. O que significa thread pool starvation e como resolver?

Thread pool starvation ocorre quando todos os threads estão ocupados e novos work items ficam na fila. A causa mais comum é código que bloqueia threads async com .Wait() ou .Result. A solução é usar await consistentemente e evitar bloqueios síncronos em código assíncrono.

4. Devo me preocupar com First Chance Exceptions?

Depende. First Chance Exceptions incluem exceções tratadas em try/catch, o que é normal. Preocupe-se quando: (1) o número cresce sem motivo aparente, (2) há tipos de exceção inesperados como NullReferenceException, (3) exceções ocorrem em hot paths afetando performance.

5. Como saber se meu heap size está saudável?

Compare o heap size com a memória disponível e observe a tendência. Um heap que cresce constantemente indica possível memory leak. O ideal é que o heap oscile (cresce com uso, diminui após GC) em torno de um valor estável. Use o memory load percentage como indicador: abaixo de 70% geralmente é saudável.

Compartilhe:

Tiago Tartari

Tiago Tartari

Eu ajudo e capacito pessoas e organizações a transformar problemas complexos em soluções práticas usando a tecnologia para atingir resultados extraordinários.

Qual é o desafio
que você tem hoje?