Compartilhar via


Depurar alto uso de CPU do .NET Core

Este artigo se aplica a: ✔️ SDK do .NET Core 3.1 e versões posteriores

Neste tutorial, você aprenderá a depurar um cenário de uso excessivo de CPU. Usando o repositório de código-fonte do exemplo fornecido do aplicativo web ASP.NET Core, você pode causar um deadlock intencionalmente. O ponto de extremidade deixará de responder e sofrerá acúmulo de threads. Você aprenderá como usar várias ferramentas para diagnosticar esse cenário com diversos dados de diagnóstico essenciais.

Neste tutorial, você irá:

  • Investigar o alto uso da CPU
  • Determinar o uso da CPU com dotnet-counters
  • Usar o dotnet-trace para a geração de rastreamento
  • Desempenho do perfil no PerfView
  • Diagnosticar e resolver uso excessivo de CPU

Pré-requisitos

O tutorial usa:

Contadores de CPU

Antes de tentar este tutorial, instale a versão mais recente dos contadores dotnet:

dotnet tool install --global dotnet-counters

Se o aplicativo estiver executando uma versão do .NET mais antiga que o .NET 9, a interface do usuário de saída do dotnet-counters terá uma aparência ligeiramente diferente; consulte dotnet-counters para obter detalhes.

Antes de tentar coletar dados de diagnóstico, você precisa observar uma alta utilização de CPU. Execute o aplicativo de exemplo usando o comando a seguir do diretório raiz do projeto.

dotnet run

Para verificar o uso atual da CPU, use o comando da ferramenta dotnet-counters :

dotnet-counters monitor -n DiagnosticScenarios --showDeltas

A saída deve ser semelhante à seguinte:

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

Name                                                            Current Value      Last Delta
[System.Runtime]
    dotnet.assembly.count ({assembly})                               111               0
    dotnet.gc.collections ({collection})
        gc.heap.generation
        ------------------
        gen0                                                           8               0
        gen1                                                           1               0
        gen2                                                           0               0
    dotnet.gc.heap.total_allocated (By)                        4,042,656          24,512
    dotnet.gc.last_collection.heap.fragmentation.size (By)
        gc.heap.generation
        ------------------
        gen0                                                     801,728               0
        gen1                                                       6,048               0
        gen2                                                           0               0
        loh                                                            0               0
        poh                                                            0               0
    dotnet.gc.last_collection.heap.size (By)
        gc.heap.generation
        ------------------
        gen0                                                     811,512               0
        gen1                                                     562,024               0
        gen2                                                   1,095,056               0
        loh                                                       98,384               0
        poh                                                       24,528               0
    dotnet.gc.last_collection.memory.committed_size (By)       5,623,808               0
    dotnet.gc.pause.time (s)                                           0.019           0
    dotnet.jit.compilation.time (s)                                    0.582           0
    dotnet.jit.compiled_il.size (By)                             138,895               0
    dotnet.jit.compiled_methods ({method})                         1,470               0
    dotnet.monitor.lock_contentions ({contention})                     4               0
    dotnet.process.cpu.count ({cpu})                                  22               0
    dotnet.process.cpu.time (s)
        cpu.mode
        --------
        system                                                         0.109           0
        user                                                           0.453           0
    dotnet.process.memory.working_set (By)                    65,515,520               0
    dotnet.thread_pool.queue.length ({work_item})                      0               0
    dotnet.thread_pool.thread.count ({thread})                         0               0
    dotnet.thread_pool.work_item.count ({work_item})                   6               0
    dotnet.timer.count ({timer})                                       0               0

Concentrando-se nos Last Delta valores de dotnet.process.cpu.time, estes nos dizem quantos segundos dentro do período de atualização (atualmente definido como o padrão de 1 s) a CPU esteve ativa. Com o aplicativo Web em execução, imediatamente após a inicialização, a CPU não está sendo utilizada de forma alguma, e ambos os deltas são 0. Navegue até a api/diagscenario/highcpu rota com 60000 como o parâmetro de rota.

https://localhost:5001/api/diagscenario/highcpu/60000

Agora, execute novamente o comando dotnet-counters .

dotnet-counters monitor -n DiagnosticScenarios --showDeltas

Você deverá ver um aumento no uso da CPU, conforme mostrado abaixo (dependendo do computador host, espere um uso variável da CPU):

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

Name                                                            Current Value      Last Delta
[System.Runtime]
    dotnet.assembly.count ({assembly})                               111               0
    dotnet.gc.collections ({collection})
        gc.heap.generation
        ------------------
        gen0                                                           8               0
        gen1                                                           1               0
        gen2                                                           0               0
    dotnet.gc.heap.total_allocated (By)                        4,042,656          24,512
    dotnet.gc.last_collection.heap.fragmentation.size (By)
        gc.heap.generation
        ------------------
        gen0                                                     801,728               0
        gen1                                                       6,048               0
        gen2                                                           0               0
        loh                                                            0               0
        poh                                                            0               0
    dotnet.gc.last_collection.heap.size (By)
        gc.heap.generation
        ------------------
        gen0                                                     811,512               0
        gen1                                                     562,024               0
        gen2                                                   1,095,056               0
        loh                                                       98,384               0
        poh                                                       24,528               0
    dotnet.gc.last_collection.memory.committed_size (By)       5,623,808               0
    dotnet.gc.pause.time (s)                                           0.019           0
    dotnet.jit.compilation.time (s)                                    0.582           0
    dotnet.jit.compiled_il.size (By)                             138,895               0
    dotnet.jit.compiled_methods ({method})                         1,470               0
    dotnet.monitor.lock_contentions ({contention})                     4               0
    dotnet.process.cpu.count ({cpu})                                  22               0
    dotnet.process.cpu.time (s)
        cpu.mode
        --------
        system                                                         0.344           0.013
        user                                                          14.203           0.963
    dotnet.process.memory.working_set (By)                    65,515,520               0
    dotnet.thread_pool.queue.length ({work_item})                      0               0
    dotnet.thread_pool.thread.count ({thread})                         0               0
    dotnet.thread_pool.work_item.count ({work_item})                   6               0
    dotnet.timer.count ({timer})                                       0               0

Durante toda a duração da solicitação, o uso da CPU se manterá próximo do valor aumentado.

Dica

Para visualizar um alto uso da CPU, você pode testar esse endpoint em várias guias do navegador simultaneamente.

Neste ponto, você pode dizer com segurança que a CPU está funcionando a uma frequência maior do que você esperava. Identificar os efeitos de um problema é fundamental para encontrar a causa. Usaremos o efeito do alto consumo de CPU além das ferramentas de diagnóstico para encontrar a causa do problema.

Analisar alto uso de CPU com o Perfilador

Ao analisar um aplicativo com alto uso de CPU, use um perfilador para compreender as ações do código. dotnet-trace collect funciona em todos os sistemas operacionais, mas o viés de ponto seguro e as pilhas de chamadas somente gerenciadas limitam-no a informações mais gerais do que um profiler com reconhecimento de kernel, como ETW para Windows ou perf para Linux. Dependendo do sistema operacional e da versão do .NET, os recursos aprimorados de criação de perfil podem estar disponíveis. Consulte as guias específicas da plataforma que seguem para obter diretrizes detalhadas.

Usar dotnet-trace collect-linux (.NET 10+)

No .NET 10 e posterior, dotnet-trace collect-linux é a abordagem de criação de perfil recomendada no Linux. Ele combina o EventPipe com perf_events no nível do sistema operacional para produzir um único rastreamento unificado que inclua as chamadas gerenciadas e nativas, tudo sem a necessidade de uma reinicialização do processo. Isso requer permissões de root e o kernel Linux 6.4+ com CONFIG_USER_EVENTS=y. Consulte os pré-requisitos do collect-linux para obter requisitos completos.

Verifique se o destino de depuração de exemplo está configurado para atingir o .NET 10 ou posterior, em seguida, execute-o e exercite o ponto de extremidade de CPU alta (https://localhost:5001/api/diagscenario/highcpu/60000) novamente. Enquanto o processo estiver em execução dentro do prazo de 1 minuto, execute dotnet-trace collect-linux para capturar um rastreamento em nível de máquina:

sudo dotnet-trace collect-linux

Deixe que ele seja executado por cerca de 20 a 30 segundos e pressione Ctrl+C ou Enter para interromper a coleção. O resultado é um .nettrace arquivo que inclui as pilhas de chamadas gerenciadas e nativas.

Abra o .nettrace com PerfView e use a visualização Pilhas de CPU para identificar os métodos que consomem mais tempo de processamento da CPU.

Para obter informações sobre como solucionar símbolos nativos de tempo de execução no rastreamento, consulte Obter símbolos para quadros nativos de tempo de execução.

Utilize perf

A perf ferramenta também pode ser usada para gerar perfis de aplicativo do .NET Core. Saia da instância prévia do alvo de depuração de teste.

Defina a DOTNET_PerfMapEnabled variável de ambiente para fazer com que o aplicativo .NET crie um map arquivo no /tmp diretório. Esse map arquivo é usado pelo perf a fim de mapear endereços de CPU para funções geradas pelo JIT por nome. Para obter mais informações, consulte Exportar mapas perf e despejos de jit.

Execute o alvo de depuração de exemplo na mesma sessão de terminal.

export DOTNET_PerfMapEnabled=1
dotnet run

Teste o endpoint da API de alto uso de CPU (https://localhost:5001/api/diagscenario/highcpu/60000) novamente. Enquanto ele estiver em execução dentro da solicitação de 1 minuto, execute o comando com a perf ID do processo:

sudo perf record -p 2266 -g

O perf comando inicia o processo de coleta de desempenho. Deixe que ele seja executado por cerca de 20 a 30 segundos e pressione Ctrl+C para sair do processo de coleção. Você pode usar o mesmo perf comando para ver a saída do rastreamento.

sudo perf report -f

Você também pode gerar um grafo de chama usando os seguintes comandos:

git clone --depth=1 https://github.com/BrendanGregg/FlameGraph
sudo perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > flamegraph.svg

Esse comando gera um flamegraph.svg que você pode exibir no navegador para investigar o problema de desempenho:

Imagem SVG de Flame Graph

Analisando dados altos da CPU com o Visual Studio

Todos os arquivos *.nettrace podem ser analisados no Visual Studio. Para analisar um arquivo *.nettrace do Linux no Visual Studio, transfira o arquivo *.nettrace, além dos outros documentos necessários, para um computador Windows e abra o arquivo *.nettrace no Visual Studio. Para obter mais informações, consulte Analisar dados de uso da CPU.

Consulte também

Próximas etapas