Site icon TheWindowsUpdate.com

Troubleshooting ASP.NET Core com alto consumo de CPU em contêineres

This post has been republished via RSS; it originally appeared at: Microsoft Tech Community - Latest Blogs - .

A conteinerização é uma abordagem de desenvolvimento de software em que uma aplicação é empacotada juntamente com suas dependências e configurações, resultando em uma imagem. Essa imagem pode ser testada e implantada de maneira isolada no sistema operacional (SO) do host por meio de um contêiner.

Com a crescente adoção dos contêineres, muitas aplicações ASP.NET Core estão sendo implantadas nesse tipo de ambiente. No entanto, essa abordagem pode apresentar desafios, especialmente na identificação e solução de problemas de desempenho.

Este artigo aborda os problemas comuns de desempenho relacionados a esse tipo de aplicação, incluindo as razões pelas quais ela pode estar consumindo uma quantidade excessiva de CPU e como identificá-los. Se você está enfrentando problemas de desempenho nesse tipo de aplicação, este artigo pode ser útil para ajudá-lo a identificar e resolver esses problemas de maneira rápida e eficiente.

 

Observabilidade e monitoração

 

O primeiro passo para este troubleshooting, é identificar que a sua aplicação está enfrentando um problema de desempenho, para isto, existem diversos serviços de monitoração e observabilidade que podem te auxiliar, variando de acordo com a plataforma que você escolheu para hospedar seus contêineres. Entre eles, os mais comuns utilizados com os serviços do Azure são os componentes do Azure Monitor:

Com o bom uso de uma estratégia de observabilidade você será capaz de diagnosticar que aplicação está com problemas de desempenho, e será capaz de analisar o comportamento dos contêineres em relação ao consumo de CPU.

Neste artigo não vou focar nessas ferramentas, pois como pontuado, existem diversas, e cada uma delas vai nos ajudar a monitorar os comportamentos das aplicações, dentro de um ambiente complexo. Entretanto, focarei nas técnicas que ajudam a entender o porquê um contêiner está consumindo muito CPU.

 

Conexão com um contêiner

 

Para realizar o troubleshooting precisamos ter acesso e conectar remotamente à um contêiner através de um terminal de linha de comando.

O estabelecimento dessa conexão pode variar de acordo com o tipo de plataforma escolhido para hospedar seu contêiner, neste artigo estou rodando um contêiner localmente usando o Docker.

Pretendo dedicar um novo artigo com este tipo de análise porém em aplicações publicadas no Serviço de Kubernetes do Azure. Entretanto tenho uma palestra no canal no YouTube do Microsoft Reactor em conjunto com o Lucas Souza, onde previamente mostramos como realizar uma análise semelhante a demonstrada neste artigo, porém no AKS: Problemas de consumo de memória em Containers no Azure Kubernetes Services

Para visualizar todos os contêineres rodando localmente no Docker, rode o comando:

docker ps

 

No meu caso o resultado foi:

CONTAINER ID   IMAGE              COMMAND                  CREATED          STATUS         PORTS                            NAMES
858a1dd123a3   poc-trace:latest   "dotnet perfview-tra…"   10 seconds ago   Up 8 seconds   443/tcp, 0.0.0.0:42098->80/tcp   nostalgic_joliot

 

Para acessar o contêiner rode o comando:

docker exec -it <NOME-DO-CONTÊINER> bash

 

Análise

 

Para realizar esta análise usaremos três ferramentas:

Dentro do contêiner, rode a seguinte sequência de comandos para instalar as ferramentas:

apt-get update

apt-get install curl

curl -L https://aka.ms/dotnet-counters/linux-x64 -o ./dotnet-counters

chmod +x ./dotnet-counters

curl -L https://aka.ms/dotnet-trace/linux-x64 -o ./dotnet-trace

chmod +x ./dotnet-trace

 

O próximo passo é verificar o consumo de CPU dentro do contêiner, para isso rode o comando:

./dotnet-counters monitor --counters System.Runtime[cpu-usage,time-in-gc] -p 1

Você também pode salvar o resultado em um arquivo.

 

Esse comando mostrará os contadores que passamos como parâmetro, a janela de atualização é de um segundo, portanto você verá um resultado parecido com:

Press p to pause, r to resume, q to quit.
    Status: Running

[System.Runtime]
    % Time in GC since last GC (%)                                         3
    CPU Usage (%)                                                         91.409

 

No caso esse contador ficou variando entre 86% e 96%, portanto de todo limite de CPU do contêiner. Outro ponto importante é o % Time in GC since last GC (%), que representa o impacto do Garbage Collector no consumo, no meu caso ele foi inexpressivo.

Com esse indicador, podemos concluir que nossa aplicação está com problemas de desempenho em relação a alto consumo de CPU.

O próximo passo, é entender o porquê esta aplicação está com esse padrão de consumo elevado, para isso usaremos a ferramenta que dotnet-trace, com ela, realizaremos uma coleta da qual nos mostrará quais métodos foram executados no decorrer de cada milissegundo.

Caso tenha curiosidade em entender como as minúcias dessa coleta, sugiro uma leitura nas documentações:

 

Rode o comando:

./dotnet-trace collect --duration 00:00:01:00 -p 1

 

Você verá um resultado parecido com:

Provider Name                           Keywords            Level               Enabled By
Microsoft-DotNETCore-SampleProfiler     0x0000F00000000000  Informational(4)    --profile
Microsoft-Windows-DotNETRuntime         0x00000014C14FCCBD  Informational(4)    --profile

Process        : /usr/share/dotnet/dotnet
Output File    : /app/dotnet_20230312_165127.nettrace

[00:00:00:30]   Recording trace 1.5548   (MB)
Press <Enter> or <Ctrl+C> to exit...

 

Como resultado, este comando irá gerar um arquivo com a extensão .nettrace, o próximo passo é copiarmos o arquivo de dentro do contêiner, para nossa máquina, a fim de o manipularmos com o programa PerfView.

Faça isso através do comando:

docker cp <NOME-DO-CONTÊINER>:/app/<NOME-DO-ARQUIVO>.nettrace .

 

Esse comando copiará o arquivo de dentro do contêiner para o diretório onde o comando foi executado.

 

PerfView

 

O PerfView é uma ferramenta gratuita de análise de desempenho que ajuda a isolar problemas relacionados à CPU e à memória. Possui vários recursos especiais para investigações no .NET ou .NET Core Runtime.

Faça download da ferramenta em: https://www.microsoft.com/en-us/download/details.aspx?id=28567

 

Para abrir o arquivo .nettrace que geramos e copiamos anteriormente no PerfView, o .exe do programa precisa estar no mesmo diretório do arquivo .nettrace.

Com o programa aberto, no meu lateral esquerdo, selecione o arquivo com a extensão .nettrace, capturado anteriormente, em seguida selecione o item Thread Time (with StartStop Activities) Stack, conforme a imagem abaixo:

 

Essa tela tem a capacidade de rastrear as pilhas de execução de cada thread, e consegue relacioná-las a algumas ações específicas com: CPU, disco, rede, execução de código não gerenciado, entre outros. Em resumo, nesta tela conseguiremos identificar as pilhas de execução de métodos que mais consumiram CPU. Ela também consegue relacionar as threads que processaram alguma Task específica, à sua threads de origem.

Também tem a capacidade de procurar eventos do EventSource como por exemplo: HTTP, ASP.NET e WCF, e cria 'atividades' para cada um deles, entretanto, essa feature só está disponível em coletas realizadas no Windows.

 

Navegue até a aba ByName, localizada logo abaixo dos controles do menu superior:

 

No quadrante central, você verá uma tabela, na coluna Name, procure pela linha CPU_TIME, selecione-a e no seu teclado pressione Alt-I:

 

Repare que foi realizado um filtro sobre a tabela anterior, e a linha CPU_TIME aparece na primeira posição.

Em seguida, clique na aba Callers, novamente abaixo dos controles do menu superior:

 

Ela mostra todos os possíveis chamadores de um método ou recurso (nesse caso o CPU_TIME). É uma exibição em forma de árvore, mas os 'filhos' dos nós são os 'chamadores' (assim, é 'invertido' em relação à exibição de árvore de chamadas).

No meu caso, a tabela ficou a seguinte:

 

A tabela é ordenada conforme a coluna Inc %, que representa o custo inclusivo expresso como uma porcentagem do total de todas as amostras. Através dessa coluna, podemos métodos que mais chamaram o CPU_TIME.

Nesse caso, o método System.Private.CoreLib!DateTime.get_Now, representou 99,6% de todo o consumo de CPU. Esse método é a representação do seguimento de código Date.Now, cujo desenvolvedores C#, estão extremamente habituados.

Agora precisamos encontrar quais métodos chamam esse Date.Now no nosso código, para fazer isso, vamos clicar no CheckBox na lateral esquerda da tabela, para expandir a arvore de chamadores.

 

Ainda com o custo inclusivo igual a 99,6%, podemos observar o método perfview-trace!perfview_trace.Controllers.WeatherForecastController+<DoWork>d__4.MoveNext(), esse nó representa o registro de uma chamada assíncrona ao método DoWork dentro da classe WeatherForecastController. Observando os próximos chamadores, podemos observar que o custo inclusivo foi divido entre as quatro threads:

|  + Thread (744) (.NET ThreadPool)
|  + Thread (741) (.NET ThreadPool)
|  + Thread (740) (.NET ThreadPool)
|  + Thread (667) (.NET ThreadPool)

 

Isso significa que havia quatro threads processando uma task assíncrona, contendo o método DoWork, no qual esse método realiza chamadas ao DateTime.Now, onde esse último está consumindo 99,6% do total de CPU_TIME.

Agora que sabemos exatamente o método que está gerando esse consumo desenfreado de CPU, precisamos analisá-lo a fim de entender com mais detalhes o motivo pelo qual isso acontece, e se possível, planejar uma otimização. Nesse caso, o método em questão era:

    public static async Task DoWork() {
        await Task.Delay(200);
        for (int i = 0; i< 100000; i++) {
            string a = DateTime.Now.ToString();
        }
    }

 

Conclusão

 

Identificar o método problemático é apenas o primeiro passo para resolver problemas de alto consumo de CPU. Para realmente solucionar o problema, é importante analisar o método em detalhes para entender a causa raiz do problema. Embora em aplicações complexas a tarefa possa ser desafiadora, as técnicas apresentadas neste artigo podem ajudar a identificar e solucionar esses problemas de maneira eficiente. Ao implementar essas técnicas, é possível melhorar significativamente a eficiência e a estabilidade da sua aplicação, proporcionando uma experiência mais agradável para o usuário final. Em resumo, a identificação e solução de problemas de alto consumo de CPU é fundamental para garantir o bom desempenho da aplicação como um todo, e o uso de técnicas eficientes é essencial para alcançar esse objetivo.

Exit mobile version