Compartir vía


Depurar alto uso de CPU en .NET Core

Este artículo se aplica a: ✔️ SDK de .NET Core 3.1 y versiones posteriores

En este tutorial, aprenderá a depurar un escenario de uso excesivo de CPU. Con el ejemplo proporcionado del repositorio de código fuente de la aplicación web ASP.NET Core, puede provocar un bloqueo intencionadamente. El extremo dejará de responder y experimentará la acumulación de hilos. Aprenderá a usar varias herramientas para diagnosticar este escenario con varios fragmentos clave de datos de diagnóstico.

En este tutorial, aprenderá a:

  • Investigación del uso elevado de CPU
  • Determinación del uso de CPU con dotnet-counters
  • Uso de dotnet-trace para la generación de seguimiento
  • Rendimiento del perfil en PerfView
  • Diagnóstico y resolución de un uso excesivo de CPU

Prerrequisitos

En el tutorial se usa:

Contadores de CPU

Antes de intentar este tutorial, instale la versión más reciente de dotnet-counters:

dotnet tool install --global dotnet-counters

Si la aplicación ejecuta una versión de .NET anterior a .NET 9, la interfaz de usuario de salida de dotnet-counters tendrá un aspecto ligeramente diferente; consulte dotnet-counters para obtener más información.

Antes de intentar recopilar datos de diagnóstico, debe observar un estado de alto uso de CPU. Ejecute la aplicación de ejemplo mediante el siguiente comando desde el directorio raíz del proyecto.

dotnet run

Para comprobar el uso actual de la CPU, use el comando dotnet-counters tool:

dotnet-counters monitor -n DiagnosticScenarios --showDeltas

La salida debe ser similar a la siguiente:

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

Al enfocar los Last Delta valores de dotnet.process.cpu.time, se indica cuántos segundos la CPU ha estado activa durante el período de actualización (actualmente establecido en 1 s por defecto). Con la aplicación web en ejecución, inmediatamente después del inicio, la CPU no está siendo consumida en absoluto, y estas diferencias son 0. Vaya a la api/diagscenario/highcpu ruta con 60000 como parámetro de ruta:

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

Ahora, vuelva a ejecutar el comando dotnet-counters .

dotnet-counters monitor -n DiagnosticScenarios --showDeltas

Debería ver un aumento en el uso de CPU, como se muestra a continuación (en función del equipo host, espere un uso de CPU variable):

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

A lo largo de la duración de la solicitud, el uso de la CPU se desplazará alrededor del valor aumentado.

Sugerencia

Para visualizar un uso de CPU aún mayor, puede ejercer este punto de conexión en varias pestañas del explorador simultáneamente.

En este punto, puede afirmar con seguridad que la CPU está funcionando a un nivel más alto de lo esperado. Identificar los efectos de un problema es clave para encontrar la causa. Usaremos el efecto del consumo elevado de CPU además de las herramientas de diagnóstico para encontrar la causa del problema.

Análisis de cpu alta con Profiler

Al analizar una aplicación con un uso elevado de CPU, use un generador de perfiles para comprender lo que hace el código. dotnet-trace collect funciona en todos los sistemas operativos, pero el bias de punto seguro y las pilas de llamadas solo administradas lo limitan a información más general que un generador de perfiles con conocimiento de kernel, como ETW para Windows o perf para Linux. Según el sistema operativo y la versión de .NET, es posible que estén disponibles las funcionalidades de generación de perfiles mejoradas; consulte las pestañas específicas de la plataforma que siguen para obtener instrucciones detalladas.

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

En .NET 10 y versiones posteriores, dotnet-trace collect-linux es el enfoque de generación de perfiles recomendado en Linux. Combina EventPipe con perf_events de nivel de sistema operativo para generar un único seguimiento unificado que incluya pila de llamadas administradas y nativas, todo ello sin necesidad de reiniciar el proceso. Esto requiere permisos de root y el kernel de Linux 6.4+ con CONFIG_USER_EVENTS=y. Consulte requisitos previos de collect-linux para conocer los requisitos completos.

Asegúrese de que el destino de depuración de ejemplo esté configurado para .NET 10 o posterior, luego ejecútelo y pruebe de nuevo el punto de conexión de alto consumo de CPU (https://localhost:5001/api/diagscenario/highcpu/60000). Mientras se ejecuta dentro de la solicitud de 1 minuto, ejecute dotnet-trace collect-linux para capturar un registro de todo el equipo:

sudo dotnet-trace collect-linux

Deje que se ejecute durante unos 20-30 segundos y presione Ctrl+C o Entrar para detener la colección. El resultado es un .nettrace archivo que incluye tanto las pilas de llamadas administradas como nativas.

Abra el .nettrace con PerfView y use la vista Pilas de CPU para identificar los métodos que consumen más tiempo de procesamiento.

Para obtener información sobre cómo resolver símbolos nativos en tiempo de ejecución en el seguimiento, consulte Obtener símbolos para marcos de tiempo de ejecución nativos.

Utilice perf

La perf herramienta también se puede usar para generar perfiles de aplicación de .NET Core. Salga de la instancia anterior del objetivo de depuración de muestra.

Establezca la DOTNET_PerfMapEnabled variable de entorno para que la aplicación .NET cree un map archivo en el /tmp directorio. Este map archivo lo usa perf para asignar direcciones de CPU a funciones generadas por JIT por nombre. Para obtener más información, consulte Exportación de mapas de rendimiento y volcados JIT.

Ejecute el destino de depuración de ejemplo en la misma sesión de terminal.

export DOTNET_PerfMapEnabled=1
dotnet run

Vuelva a utilizar el punto de conexión de alto uso de CPU de la API (https://localhost:5001/api/diagscenario/highcpu/60000). Mientras se lleva a cabo la solicitud de 1 minuto, ejecute el comando junto con su identificador de proceso perf:

sudo perf record -p 2266 -g

El perf comando inicia el proceso de recopilación de rendimiento. Deje que se ejecute durante unos 20-30 segundos y presione Ctrl+C para salir del proceso de recopilación. Puede usar el mismo perf comando para ver la salida del seguimiento.

sudo perf report -f

También puede generar un gráfico de llamas mediante los siguientes comandos:

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

Este comando genera un flamegraph.svg que puede ver en el explorador para investigar el problema de rendimiento:

Imagen SVG del grafo de llama

Análisis de datos de CPU elevados con Visual Studio

Todos los archivos *.nettrace se pueden analizar en Visual Studio. Para analizar un archivo *.nettrace de Linux en Visual Studio, transfiera el archivo *.nettrace, además de los demás documentos necesarios, a una máquina Windows y, a continuación, abra el archivo *.nettrace en Visual Studio. Para obtener más información, consulte Análisis de datos de uso de CPU.

Consulte también

Pasos siguientes