Partager via


Déboguer le problème lié aux besoins de pools de threads

Cet article s’applique à : ✔️ .NET 9.0 et versions ultérieures

Dans ce tutoriel, vous allez apprendre à déboguer un scénario de famine de ThreadPool. La famine de ThreadPool se produit lorsque le pool n’a pas de threads disponibles pour traiter de nouveaux éléments de travail et qu’il provoque souvent une réponse lente des applications. À l’aide de l’exemple fourni application web ASP.NET Core, vous pouvez provoquer intentionnellement l'épuisement du ThreadPool et apprendre comment le diagnostiquer.

Dans ce tutoriel, vous allez :

  • Examiner une application qui répond aux demandes lentement
  • Utiliser l’outil dotnet-counters pour identifier si un épuisement du ThreadPool se produit probablement.
  • Utilisez les outils dotnet-stack et dotnet-trace pour déterminer quel travail consiste à maintenir les threads ThreadPool occupés

Conditions préalables

Le tutoriel utilise :

Exécuter l’exemple d’application

Téléchargez le code de l’exemple d’application et exécutez-le à l’aide du Kit de développement logiciel (SDK) .NET :

E:\demo\DiagnosticScenarios>dotnet run
Using launch settings from E:\demo\DiagnosticScenarios\Properties\launchSettings.json...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: E:\demo\DiagnosticScenarios

Si vous utilisez un navigateur web et envoyez des demandes à https://localhost:5001/api/diagscenario/taskwait, vous devez voir la réponse success:taskwait retournée après environ 500 ms. Cela montre que le serveur web sert le trafic comme prévu.

Observer la lenteur du système

Le serveur web de démonstration a plusieurs points de terminaison qui simulent une requête sur la base de données, puis retournent une réponse à l’utilisateur. Chacun de ces points de terminaison a un délai d’environ 500 ms lors du traitement des requêtes un par un, mais les performances sont beaucoup plus mauvaises lorsque le serveur web est soumis à une certaine charge. Téléchargez l’outil de test de charge Bombardier et observez la différence de latence lorsque 125 requêtes simultanées sont envoyées à chaque point de terminaison.

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait
Bombarding https://localhost:5001/api/diagscenario/taskwait for 10s using 125 connection(s)
[=============================================================================================] 10s
Done!
Statistics        Avg      Stdev        Max
  Reqs/sec        33.06     234.67    3313.54
  Latency         3.48s      1.39s     10.79s
  HTTP codes:
    1xx - 0, 2xx - 454, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    75.37KB/s

Ce deuxième point de terminaison utilise un modèle de code qui s’exécute encore plus mal :

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait
Bombarding https://localhost:5001/api/diagscenario/tasksleepwait for 10s using 125 connection(s)
[=============================================================================================] 10s
Done!
Statistics        Avg      Stdev        Max
  Reqs/sec         1.61      35.25     788.91
  Latency        15.42s      2.18s     18.30s
  HTTP codes:
    1xx - 0, 2xx - 140, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    36.57KB/s

Ces deux points de terminaison affichent considérablement plus que la latence moyenne de 500 ms lorsque la charge est élevée (3,48 s et 15,42 s respectivement). Si vous exécutez cet exemple sur une version antérieure de .NET Core, vous pouvez voir que les deux exemples fonctionnent de manière égale. .NET 6 a mis à jour les heuristiques du ThreadPool qui réduisent l’impact sur les performances de la mauvaise pratique de codage utilisée dans le premier exemple.

Détecter la faim de ThreadPool

Si vous avez observé le comportement ci-dessus sur un service réel, vous savez qu’il répond lentement sous charge, mais vous ne connaissez pas la cause. dotnet-counters est un outil qui peut afficher des compteurs de performances en direct. Ces compteurs peuvent fournir des indices sur certains problèmes et sont souvent faciles à obtenir. Dans les environnements de production, vous pouvez avoir des compteurs similaires fournis par les outils de supervision à distance et les tableaux de bord web. Installez dotnet-counters et commencez à surveiller le service web :

dotnet-counters monitor -n DiagnosticScenarios
Press p to pause, r to resume, q to quit.
    Status: Running

Name                                                       Current Value
[System.Runtime]
    dotnet.assembly.count ({assembly})                               115
    dotnet.gc.collections ({collection})
        gc.heap.generation
        gen0                                                           2
        gen1                                                           1
        gen2                                                           1
    dotnet.gc.heap.total_allocated (By)                       64,329,632
    dotnet.gc.last_collection.heap.fragmentation.size (By)
        gc.heap.generation
        gen0                                                     199,920
        gen1                                                      29,208
        gen2                                                           0
        loh                                                           32
        poh                                                            0
    dotnet.gc.last_collection.heap.size (By)
        gc.heap.generation
        gen0                                                     208,712
        gen1                                                   3,456,000
        gen2                                                   5,065,600
        loh                                                       98,384
        poh                                                    3,147,488
    dotnet.gc.last_collection.memory.committed_size (By)      31,096,832
    dotnet.gc.pause.time (s)                                           0.024
    dotnet.jit.compilation.time (s)                                    1.285
    dotnet.jit.compiled_il.size (By)                             565,249
    dotnet.jit.compiled_methods ({method})                         5,831
    dotnet.monitor.lock_contentions ({contention})                   148
    dotnet.process.cpu.count ({cpu})                                  16
    dotnet.process.cpu.time (s)
        cpu.mode
        system                                                         2.156
        user                                                           2.734
    dotnet.process.memory.working_set (By)                             1.3217e+08
    dotnet.thread_pool.queue.length ({work_item})                      0
    dotnet.thread_pool.thread.count ({thread})                         0
    dotnet.thread_pool.work_item.count ({work_item})              32,267
    dotnet.timer.count ({timer})                                       0

Les compteurs précédents sont un exemple alors que le serveur web ne servait aucune requête. Réexécutez Bombardier avec le api/diagscenario/tasksleepwait point de terminaison et la charge soutenue pendant 2 minutes afin qu’il y ait suffisamment de temps pour observer ce qui arrive aux compteurs de performances.

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s

La famine de ThreadPool se produit lorsqu’il n’y a pas de threads libres pour gérer les éléments de travail mis en file d’attente et que le runtime répond en augmentant le nombre de threads ThreadPool. La dotnet.thread_pool.thread.count valeur augmente rapidement à 2 à 3 fois le nombre de cœurs de processeur sur votre machine, puis d’autres threads sont ajoutés de 1 à 2 par seconde jusqu’à ce qu’ils se stabilisent quelque part au-dessus de 125. Les signaux clés indiquant que l'épuisement du ThreadPool constitue actuellement un goulot d’étranglement des performances sont l’augmentation lente et régulière des threads ThreadPool et une utilisation du processeur bien inférieure à 100%. L'augmentation du nombre de threads se poursuivra jusqu'à ce que le pool atteigne son nombre maximal de threads, que suffisamment de threads aient été créés pour satisfaire tous les éléments de travail entrants, ou que le processeur soit saturé. Souvent, mais pas toujours, la saturation du ThreadPool affiche également de grandes valeurs pour dotnet.thread_pool.queue.length et des valeurs faibles pour dotnet.thread_pool.work_item.count, ce qui signifie qu’il y a une grande quantité de travail en attente et peu de travail accompli. Voici un exemple de compteurs alors que le nombre de threads est toujours en hausse :

[System.Runtime]
    dotnet.assembly.count ({assembly})                               115
    dotnet.gc.collections ({collection})
        gc.heap.generation
        gen0                                                           5
        gen1                                                           1
        gen2                                                           1
    dotnet.gc.heap.total_allocated (By)                       1.6947e+08
    dotnet.gc.last_collection.heap.fragmentation.size (By)
        gc.heap.generation
        gen0                                                           0
        gen1                                                     348,248
        gen2                                                           0
        loh                                                           32
        poh                                                            0
    dotnet.gc.last_collection.heap.size (By)
        gc.heap.generation
        gen0                                                           0
        gen1                                                  18,010,920
        gen2                                                   5,065,600
        loh                                                       98,384
        poh                                                    3,407,048
    dotnet.gc.last_collection.memory.committed_size (By)      66,842,624
    dotnet.gc.pause.time (s)                                           0.05
    dotnet.jit.compilation.time (s)                                    1.317
    dotnet.jit.compiled_il.size (By)                             574,886
    dotnet.jit.compiled_methods ({method})                         6,008
    dotnet.monitor.lock_contentions ({contention})                   194
    dotnet.process.cpu.count ({cpu})                                  16
    dotnet.process.cpu.time (s)
        cpu.mode
        system                                                         4.953
        user                                                           6.266
    dotnet.process.memory.working_set (By)                             1.3217e+08
    dotnet.thread_pool.queue.length ({work_item})                      0
    dotnet.thread_pool.thread.count ({thread})                       133
    dotnet.thread_pool.work_item.count ({work_item})              71,188
    dotnet.timer.count ({timer})                                     124

Une fois le nombre de threads ThreadPool stabilisés, le pool n’est plus affamé. Mais s’il se stabilise à une valeur élevée (plus de trois fois le nombre de cœurs de processeur), cela indique généralement que le code de l’application bloque certains threads ThreadPool et que threadPool est compensé par l’exécution avec plus de threads. L’exécution stable au nombre élevé de threads n’aura pas nécessairement d’impact important sur la latence des requêtes, mais si la charge varie considérablement au fil du temps ou que l’application sera régulièrement redémarrée, chaque fois que le ThreadPool est susceptible d’entrer une période de faim où il augmente lentement les threads et fournit une latence de requête médiocre. Chaque thread consomme également de la mémoire, de sorte que la réduction du nombre total de threads nécessaires offre un autre avantage.

À compter de .NET 6, les heuristiques threadPool ont été modifiées pour augmenter le nombre de threads ThreadPool beaucoup plus rapidement en réponse à certaines API de tâche bloquantes. La famine de ThreadPool peut toujours se produire avec ces API, mais la durée est beaucoup plus courte que celle des versions plus anciennes de .NET, car le runtime répond plus rapidement. Réexécutez à nouveau Bombardier avec le point de terminaison api/diagscenario/taskwait.

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s

Sur .NET 6, vous devez observer que le pool augmente le nombre de threads plus rapidement qu’avant, puis stabiliser à un nombre élevé de threads. La famine de ThreadPool se produit pendant l’escalade du nombre de threads.

Résoudre la faim de ThreadPool

Pour éliminer la saturation du ThreadPool, les threads doivent rester débloqués afin d'être disponibles pour gérer les éléments de travail entrants. Il existe plusieurs façons de déterminer ce que faisait chaque thread. Si le problème se produit uniquement occasionnellement, la collecte d’une trace avec dotnet-trace est préférable pour enregistrer le comportement de l’application sur une période donnée. Si le problème se produit constamment, vous pouvez utiliser l’outil dotnet-stack ou capturer un vidage avec dotnet-dump qui peut être affiché dans Visual Studio. dotnet-stack peut être plus rapide, car il affiche les piles de threads immédiatement sur la console. Toutefois, le débogage de vidage dans Visual Studio offre de meilleures visualisations qui associent les cadres à la source, Juste mon code peut filtrer les cadres d'implémentation du runtime, et la fonctionnalité Stacks parallèles peut aider à regrouper de nombreux threads possédant des piles similaires. Ce tutoriel présente les options dotnet-stack et dotnet-trace. Pour obtenir un exemple d’examen des piles de threads à l’aide de Visual Studio, consultez la vidéo du didacticiel sur le diagnostic de la saturation du ThreadPool.

Diagnostiquer un problème continu avec dotnet-stack

Réexécutez Bombardier pour mettre le serveur web en charge :

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s

Exécutez ensuite dotnet-stack pour afficher les traces de la pile de threads :

dotnet-stack report -n DiagnosticScenarios

Vous devriez voir un long résultat contenant un grand nombre de piles, dont beaucoup ont une apparence similaire à celle-ci :

Thread (0x25968):
  [Native Frames]
  System.Private.CoreLib.il!System.Threading.ManualResetEventSlim.Wait(int32,value class System.Threading.CancellationToken)
  System.Private.CoreLib.il!System.Threading.Tasks.Task.SpinThenBlockingWait(int32,value class System.Threading.CancellationToken)
  System.Private.CoreLib.il!System.Threading.Tasks.Task.InternalWaitCore(int32,value class System.Threading.CancellationToken)
  System.Private.CoreLib.il!System.Threading.Tasks.Task`1[System.__Canon].GetResultCore(bool)
  DiagnosticScenarios!testwebapi.Controllers.DiagScenarioController.TaskWait()
  Anonymously Hosted DynamicMethods Assembly!dynamicClass.lambda_method1(pMT: 00007FF7A8CBF658,class System.Object,class System.Object[])
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+SyncObjectResultExecutor.Execute(class Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultTypeMapper,class Microsoft.Extensions.Internal.ObjectMethodExecutor,class System.Object,class System.Object[])
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeAsync()
  Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Routing.ControllerRequestDelegateFactory+<>c__DisplayClass10_0.<CreateRequestDelegate>b__0(class Microsoft.AspNetCore.Http.HttpContext)
  Microsoft.AspNetCore.Routing.il!Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
  Microsoft.AspNetCore.Authorization.Policy.il!Microsoft.AspNetCore.Authorization.AuthorizationMiddleware+<Invoke>d__6.MoveNext()
  System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
  Microsoft.AspNetCore.Authorization.Policy.il!Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
  Microsoft.AspNetCore.HttpsPolicy.il!Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
  Microsoft.AspNetCore.HttpsPolicy.il!Microsoft.AspNetCore.HttpsPolicy.HstsMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
  Microsoft.AspNetCore.HostFiltering.il!Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
  Microsoft.AspNetCore.Server.Kestrel.Core.il!Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol+<ProcessRequests>d__223`1[System.__Canon].MoveNext()
  System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
  System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.Threading.Tasks.VoidTaskResult,Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol+<ProcessRequests>d__223`1[System.__Canon]].MoveNext(class System.Threading.Thread)
  System.Private.CoreLib.il!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(class System.Runtime.CompilerServices.IAsyncStateMachineBox,bool)
  System.Private.CoreLib.il!System.Threading.Tasks.Task.RunContinuations(class System.Object)
  System.IO.Pipelines.il!System.IO.Pipelines.StreamPipeReader+<<ReadAsync>g__Core|36_0>d.MoveNext()
  System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
  System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.IO.Pipelines.ReadResult,System.IO.Pipelines.StreamPipeReader+<<ReadAsync>g__Core|36_0>d].MoveNext(class System.Threading.Thread)
  System.Private.CoreLib.il!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(class System.Runtime.CompilerServices.IAsyncStateMachineBox,bool)
  System.Private.CoreLib.il!System.Threading.Tasks.Task.RunContinuations(class System.Object)
  System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1[System.Int32].SetExistingTaskResult(class System.Threading.Tasks.Task`1<!0>,!0)
  System.Net.Security.il!System.Net.Security.SslStream+<ReadAsyncInternal>d__186`1[System.Net.Security.AsyncReadWriteAdapter].MoveNext()
  System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
  System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.Int32,System.Net.Security.SslStream+<ReadAsyncInternal>d__186`1[System.Net.Security.AsyncReadWriteAdapter]].MoveNext(class System.Threading.Thread)
  Microsoft.AspNetCore.Server.Kestrel.Core.il!Microsoft.AspNetCore.Server.Kestrel.Core.Internal.DuplexPipeStream+<ReadAsyncInternal>d__27.MoveNext()
  System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
  System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
  System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()

Les cadres en bas de ces piles indiquent que ces threads sont des threads du pool de threads :

  System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
  System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()

Et les trames situées près du haut révèlent que le thread est bloqué sur un appel à GetResultCore(bool) provenant de la fonction DiagnosticScenarioController.TaskWait() :

Thread (0x25968):
  [Native Frames]
  System.Private.CoreLib.il!System.Threading.ManualResetEventSlim.Wait(int32,value class System.Threading.CancellationToken)
  System.Private.CoreLib.il!System.Threading.Tasks.Task.SpinThenBlockingWait(int32,value class System.Threading.CancellationToken)
  System.Private.CoreLib.il!System.Threading.Tasks.Task.InternalWaitCore(int32,value class System.Threading.CancellationToken)
  System.Private.CoreLib.il!System.Threading.Tasks.Task`1[System.__Canon].GetResultCore(bool)
  DiagnosticScenarios!testwebapi.Controllers.DiagScenarioController.TaskWait()

Diagnostiquer un problème intermittent avec dotnet-trace

L’approche dotnet-stack est efficace uniquement pour les opérations de blocage évidentes et cohérentes qui se produisent dans chaque requête. Dans certains scénarios, le blocage se produit sporadiquement toutes les quelques minutes, ce qui rend dotnet-stack moins utile pour diagnostiquer le problème. Dans ce cas, vous pouvez utiliser dotnet-trace pour collecter des événements sur une période donnée et les enregistrer dans un fichier nettrace qui peut être analysé ultérieurement.

Il existe un événement particulier qui permet de diagnostiquer la saturation du pool de threads : l’événement WaitHandleWait, qui a été introduit dans .NET 9. Il est émis lorsqu'un thread devient bloqué par des opérations telles que des appels synchrones sur appels asynchrones (par exemple, Task.Result, Task.Wait, et Task.GetAwaiter().GetResult()) ou par d'autres opérations de verrouillage telles que lock, Monitor.Enter, ManualResetEventSlim.Wait, et SemaphoreSlim.Wait.

Réexécutez Bombardier pour mettre le serveur web en charge :

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s

Exécutez ensuite dotnet-trace pour collecter les événements d’attente :

dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30

Cela doit générer un fichier nommé DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace contenant les événements. Ce nettrace peut être analysé à l’aide de deux outils différents :

Les sections suivantes montrent comment utiliser chaque outil pour lire le fichier nettrace.

Analyser un nettrace avec Perfview

  1. Téléchargez PerfView et exécutez-le.

  2. Ouvrez le fichier nettrace en double-cliquant dessus.

    Capture d’écran de l’ouverture d’un nettrace dans PerfView

  3. Double-cliquez sur Advanced Group>Any Stacks. Une nouvelle fenêtre s’ouvre.

    Capture d’écran de l’affichage de toutes les piles dans PerfView.

  4. Double-cliquez sur la ligne « Événement Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start ».

    Vous devriez maintenant voir les traces de pile où les événements WaitHandleWait ont été émis. Ils sont divisés par « WaitSource ». Actuellement, il existe deux sources : MonitorWait pour les événements émis via Monitor.Wait et Unknown pour tous les autres.

    Capture d’écran de l’affichage des piles pour les événements d’attente dans PerfView.

  5. Commencez par MonitorWait, car il représente 64,8% des événements. Vous pouvez cocher les cases pour développer les traces de pile responsables de l’émission de cet événement.

    Capture d’écran de l’affichage de toutes les piles développées pour les événements d’attente dans PerfView.

    Cette trace de pile peut être interprétée comme suit : Task<T>.Result a émis un événement WaitHandleWait avec un WaitSource MonitorWait (Task<T>.Result utilise Monitor.Wait pour effectuer une attente). Il a été appelé par DiagScenarioController.TaskWait, qui a été appelé par une fonction lambda, qui a été appelée par du code ASP.NET

Analyser un nettrace avec l’observateur d’événements .NET

  1. Accédez à verdie-g.github.io/dotnet-events-viewer.

  2. Faites glisser-déplacer le fichier nettrace.

    Capture d’écran de l’ouverture d’un nettrace dans l’Observateur d'événements .NET.

  3. Accédez à la page Arborescence des événements , sélectionnez l’événement « WaitHandleWaitStart », puis sélectionnez Exécuter la requête.

    Capture d’écran d’une requête d’événements dans la visionneuse d’événements .NET.

  4. Vous devez voir les traces de pile où les événements WaitHandleWait ont été émis. Cliquez sur les flèches pour développer les traces de pile responsables de l’émission de cet événement.

    Capture d’écran de l’arborescence dans la visionneuse d’événements .NET.

    Cette trace de pile peut être lue comme suit : ManualResetEventSlim.Wait a émis un événement WaitHandleWait. Il a été appelé par Task.SpinThenBlockWait, qui a été appelé par Task.InternalWaitCore, qui a été appelé par Task<T>.Result, qui a été appelé par DiagScenario.TaskWait, qui a été appelé par une fonction lambda, qui a été appelé par du code ASP.NET

Dans les scénarios réels, vous pouvez trouver beaucoup d’événements d’attente émis à partir de threads en dehors du pool de threads. Dans ce contexte, vous examinez un épuisement du pool de threads, donc les attentes sur un thread dédié en dehors du pool de threads ne sont pas pertinentes. Vous pouvez déterminer si une trace de pile d'exécution provient d'un fil de pool de threads en examinant les premières méthodes, qui devraient mentionner le pool de threads (par exemple, WorkerThread.WorkerThreadStart ou ThreadPoolWorkQueue).

En haut d’une trace de pile de threads de pool de threads.

Correction de code

Vous pouvez maintenant accéder au code de ce contrôleur dans le fichier Contrôleurs/DiagnosticScenarios.cs de l’exemple d’application pour voir qu’il appelle une API asynchrone sans utiliser await. Il s'agit du modèle de code sync-over-async, qui est connu pour bloquer les threads et est la cause la plus courante de l'épuisement du ThreadPool.

public ActionResult<string> TaskWait()
{
    // ...
    Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
    return "success:taskwait";
}

Dans ce cas, le code peut être facilement modifié pour utiliser l’async/await à la place, comme indiqué dans le TaskAsyncWait() point de terminaison. L’utilisation d’await permet au thread actuel de traiter d’autres éléments de travail pendant que la requête de base de données est en cours. Une fois la recherche de base de données terminée, un thread ThreadPool reprend l’exécution. De cette façon, aucun thread n’est bloqué dans le code pendant chaque requête.

public async Task<ActionResult<string>> TaskAsyncWait()
{
    // ...
    Customer c = await PretendQueryCustomerFromDbAsync("Dana");
    return "success:taskasyncwait";
}

L’exécution de Bombadier pour envoyer la api/diagscenario/taskasyncwait charge au point de terminaison montre que le nombre de threads ThreadPool reste beaucoup plus faible et que la latence moyenne reste proche de 500 ms lors de l’utilisation de l’approche asynchrone/await :

>bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskasyncwait
Bombarding https://localhost:5001/api/diagscenario/taskasyncwait for 10s using 125 connection(s)
[=============================================================================================] 10s
Done!
Statistics        Avg      Stdev        Max
  Reqs/sec       227.92     274.27    1263.48
  Latency      532.58ms    58.64ms      1.14s
  HTTP codes:
    1xx - 0, 2xx - 2390, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    98.81KB/s