ASP.NET Troubleshooting – Identificando a causa de um alto e crescente consumo de memória em produção

Olá pessoal.

Neste post descrevo uma forma de identificar a causa de um consumo crescente de memória (possívelmente memory-leak) em um aplicativo ASP.NET em produção, sendo que não tenho acesso ao código-fonte do aplicativo e tão pouco contato com a equipe de desenvolvimento.

Exemplifiquei aqui em ASP.NET, mas o procedimento pode ser para qualquer aplicativo .NET, seja ele, WinForms, WPF, Windows Services, etc.

Cenário

O administrador da infraestrutura percebeu um alto e crescente consumo de memória no processo w3wp.exe (IIS – servidor web), sendo que neste IIS está hospedado um aplicativo web ASP.NET (MVC 4).

Investigação

Passo 1 – Confirmar o alto consumo de memória e o crescimento constante

Uma das formas fáceis de confirmar de que está realmente havendo um crescente consumo de memória é utilizar os contadores de performance (perfmon.exe).

Neste caso os seguintes contadores podem ser úteis:

  • Memória do .NET CLR
    • # Bytes in all Heaps para o processo w3wp.exe (IIS) – este contador irá nos mostrar quanto de memória está sendo consumido ao longo do tempo e se é crescente.
  • Aplicativos ASP.NET v4.0.30319 – é interessante para confrontar a quantidade de memória consumida em relação a quantidade de usuários/uso da aplicação
    • Número Total de Sessões – em uma aplicação não stateless é possível ter uma noção da quantidade de usuário que utilizaram a aplicação até o momento
    • Sessões ativas – em uma aplicação não stateless é possível ter uma noção da quantidade de usuário que estão utilizando neste momento
    • Solicitações em Execução – é possível ter uma noção do quanto a aplicação está sendo utilizada (requisições)
    • Solicitações / segundo – é possível ter uma noção de quantas requisições estão sendo processadas por segundo

Na primeira amostragem dos contadores (Figura 1), é possível identificar que o w3wp.exe está consumindo cerca de 260MB de memória com um baixo uso do aplicativo (apenas duas sessões e requisições por segundo quase zero). Ou seja, muita memória mas pouca atividade dos usuários.

Figura 1 – Primeira amostragem dos contadores

Em uma segunda amostragem dos contadores (Figura 2), poucos minutos depois, é possível identificar que o w3wp.exe teve um salto de consumo de memória, indo a próximo de 742MB de memória mas ainda com baixo acesso ao servidor web (apenas três sessões e número de requisições por segundo quase zero). Ou seja, incremento grande no consumo de memória mas ainda pouco uso do aplicativo.

Figura 2 – Segunda amostragem dos contadores

A Figura 3 mostra o incremento de memória em uma linha do tempo.

 Figura 3- Linha do tempo dos contadores

A suspeita do administrador de infraestrutura está se confirmando, pois aparentemente o consumo de memória apenas cresceu, sem ter um grande aumento no uso do aplicativo web.
O próximo passo é tentar descobrir o que está consumindo tanta memória.

Passo 2 – Colher informações do ambiente de produção

Há várias formas e fontes de informações que nos podem ser úteis em uma investigação de problema em produção. Neste caso de alto consumo de memória um recurso muito útil é o DUMP do processo w3wp.exe.

Este dump é uma espécie de fotografia do que o processo está fazendo neste momento e os recursos alocados por ele, por exemplo, quais objetos estão em memória neste instante, quais DLLs foram carregadas, etc.

Há formas variadas de gerar um dump, as mais comuns são através dos aplicativos adplus.exe, Debug Diag, Task Manager e WindDbg.
Para este artigo utilizei uma das formas mais simples de gerar um dump, usar o gerenciador de tarefas do Windows, porém esta forma pode não servir para outras investigações mais detalhadas.

Selecionei o processo w3wp.exe e solicitei “Criar arquivo de despejo”, conforme mostrado na Figura 4.

 Figura 4 – Criar um arquivo dump básico

O tempo para gerar o arquivo de despejo e o tamanho final do arquivo de dump varia conforme a quantidade de memória alocada, podendo a ficar com alguns gigas de tamanha.

Concluído a geração do arquivo de despejo (dump), me é informado onde está o arquivo criado (Figura 5).

 Figura 5 – Caminho do arquivo dump

Este arquivo é o dump – fotografia do processo em um determinado momento. Analisando este arquivo poderemos descobrir a causa do alto consumo de memória.

Passo 3 – Analisar o dump

Irei utilizar o ntsd.exe (utilitário da Microsoft disponível no Windows SDK  – download) disponível na pasta C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x64.

Usando o PowerShell aciono o ntsd.exe informando que quero analisar um arquivo de dump (parâmetro –z) e o local do arquivo de dump.

Figura 6 – Iniciar o debugger ntsd.exe para análise do dump

Então é aberto uma tela do aplicativo console ntsd.exe.

Figura 7 – ntsd.exe em execução

Antes de começar a analisar o dump, é necessário fazer alguns ajustes para informar ao ntsd.exe que ele deve carregar algumas informações para analisar um aplicativo .NET.

Ajustar o símbolos de depuração com .symfix e .reload

Figura 8 – Correção para os símbolos de depuração

Carregar as extensões para análise de aplicativos .NET: sos.dll e clr.dll

Figura 9 – Carregando o sos.dll e clr.dll

Tudo preparado, hora de começar a investigar.

Uma primeira ação é dar uma olhada na heap de memória do aplicativo para ver se identificamos algo suspeito.

Para isso utilizo o comando: !DumpHeap -stat, que fornece estatísticas dos objetos alocados pelo aplicativo, ordenados pelo consumo de memória.

 Figura 10 – Estatísticas da heap

Já podemos notar que dos 260MB ocupados na primeira amostragem (momento que foi tirado o dump), há 482 objetos do tipo array de bytes ocupando 168MB, representando cerca de 64% de toda a memória ocupada.
Bastante suspeito, então vamos dar uma olhada mais a fundo nisso, listando detalhes de todos os 482 objetos array de bytes, para depois investigar um deles aleatoriamente.

Para isso, executo o comando “!DumpHeap -mt 000007f92c88f490“, onde o endereço informado é o Method Table (-mt) do System.Byte[] mostrado na figura anterior.

 Figura 11 – Listar os 482 objetos do tipo System.Byte[]

Dos 482 objetos listados, escolho um quase aleatoriamente, digo quase pois opto por um que está com uma quantidade maior de memória (última coluna exibida). Escolhido qual objeto investigar, utilizo o comando “!do endereço”, por exemplo, escolhi o último listado (!do 000000ee92121090) pois ocupa 10MB.

Figura 12 – Detalhes de um objeto específico

Nesta imagem podemos perceber que é um objeto Array de uma dimensão (Rank 1) com 10MB de tamanho (elementos).

Se o conteúdo fosse string, poderíamos saber o que tem nele na propriedade Content, mas como é bytes, preciso investigar quem é que está usando este objeto, quem é o “root” do objeto.

Para isso, utilizo o comando “!gcroot endereço”, por exemplo, !gcroot 000000ee92121090.

 Figura 13 – GCRoot do objeto investigado

Eis que a Figura 13 nos dá algumas informações interessantes, sendo elas:

  1. Found 1 unique roots” – só há uma referência para este objeto array.
  2. MvcApplication7.MyObject” é quem está apontando para este objeto. Com esse nome, provavelmente é um código do fornecedor do aplicativo em execução. Ao acionar a equipe de desenvolvimento, é possível dizer para eles que a classe MyObject está alocando um array de bytes de 10MB e aparentemente causando o alto consumo de memória.
  3. System.Web.SessionState.SessionStateItemCollection” – indicador que o MyObject está sendo mantido na sessão do usuário do ASP.NET, ou seja, aparentemente ficará com a memória alocada durante toda a vida útil da sessão, que por padrão é 20 minutos.
  4. System.Web.SessionState.InProcSessionState” – indica que a sessão do ASP.NET está InProc, ou seja, sendo mantida no próprio processo w3wp.exe, confirmando que o armazenamento do MyObject na sessão irá impactar diretamente no consumo de memória do processo w3wp.exe.

Feito! Conseguimos identificar de forma específica uma classe que é a possível causadora do problema, isso sem ter acesso ao código fonte do aplicativo.
Agora então bastaria passar essas informações para a equipe de desenvolvimento do aplicativo para que eles avaliem se é um bug ou uma característica do aplicativo.

A título de curiosidade, a imagem abaixo representa o “culpado” pelo alto consumo de memória no código-fonte do aplicativo analisado em produção.

Figura 14 – Código fonte do aplicativo executado em produção

Observação: este código não é de um aplicativo real, foi construído para exemplificar o problema de memória.

Por hoje era isso.

Fonte:Rafael Leonhardt

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: