Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Dieser Artikel bezieht sich auf: ✔️ .NET 9.0 und höhere Versionen
In diesem Lernprogramm erfahren Sie, wie Sie ein ThreadPool-Starvation-Szenario debuggen. ThreadPool-Starvation tritt auf, wenn der Pool keine verfügbaren Threads zum Verarbeiten neuer Arbeitsaufgaben hat und häufig dazu führt, dass Anwendungen langsam reagieren. Mithilfe des bereitgestellten Beispiels ASP.NET Core Web App können Sie ThreadPool-Verhungern absichtlich verursachen und lernen, wie man sie diagnostiziert.
In diesem Tutorial werden Sie Folgendes lernen:
- Untersuchen einer App, die langsam auf Anforderungen reagiert
- Verwenden Sie das Tool dotnet-counters, um zu identifizieren, dass wahrscheinlich eine ThreadPool-Starvation auftritt.
- Verwenden Sie die dotnet-stack- und dotnet-trace-Tools, um zu bestimmen, welche Arbeit die ThreadPool-Threads beschäftigt.
Voraussetzungen
Das Tutorial verwendet:
- .NET 9 SDK zum Erstellen und Ausführen der Beispiel-App
- Beispiel-Web-App zur Demonstration des Verhaltens von ThreadPool bei Ressourcenmangel
- Bombardier zum Generieren der Last für die Beispielweb-App
- dotnet-Counter zur Beobachtung von Leistungsindikatoren
- dotnet-stack um Thread-Stacks zu untersuchen
- dotnet-trace um Warteereignisse zu sammeln
- Optional: PerfView zum Analysieren der Warteereignisse
Führen Sie die Beispielanwendung aus
Laden Sie den Code für die Beispiel-App herunter, und führen Sie ihn mit dem .NET SDK aus:
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
Wenn Sie einen Webbrowser verwenden und Anforderungen an https://localhost:5001/api/diagscenario/taskwait
senden, dann sollte die Antwort success:taskwait
nach ca. 500 ms zurückgegeben wird. Dies zeigt, dass der Webserver Datenverkehr erwartungsgemäß bedient.
Langsame Leistung bemerken
Der Demowebserver verfügt über mehrere Endpunkte, die eine Datenbankanforderung simuliert und dann eine Antwort an den Benutzer zurückgeben. Jeder dieser Endpunkte hat eine Verzögerung von ca. 500 ms, wenn Anforderungen einzeln bedient werden, aber die Leistung ist viel schlechter, wenn der Webserver einer Last ausgesetzt ist. Laden Sie das Bombardier Load Testing Tool herunter, und beobachten Sie den Unterschied bei der Latenz, wenn 125 gleichzeitige Anforderungen an jeden Endpunkt gesendet werden.
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
Dieser zweite Endpunkt verwendet ein Codemuster, das noch schlechter abschneidet.
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
Beide Endpunkte zeigen deutlich mehr als die durchschnittliche Latenz von 500 ms, wenn die Last hoch ist (3,48 s bzw. 15,42 s). Wenn Sie dieses Beispiel auf einer älteren Version von .NET Core ausführen, werden wahrscheinlich beide Beispiele gleichermaßen schlecht ausgeführt. .NET 6 hat ThreadPool-Heuristiken aktualisiert, die die Leistungseinbußen des im ersten Beispiel verwendeten fehlerhaften Codierungsmusters verringern.
Erkennung der ThreadPool-Verhungerung
Wenn Sie das oben beschriebene Verhalten in einem Dienst in der realen Welt beobachtet hätten, würden Sie wissen, dass er unter Last langsam reagiert, aber Sie würden die Ursache nicht kennen. dotnet-counters ist ein Tool, mit dem Liveleistungsindikatoren angezeigt werden können. Diese Indikatoren können Hinweise auf bestimmte Probleme liefern und sind oft leicht zugänglich. In Produktionsumgebungen verfügen Sie möglicherweise über ähnliche Zähler, die von Remote-Überwachungstools und Web-Dashboards bereitgestellt werden. Installieren Sie dotnet-Counter, und beginnen Sie mit der Überwachung des Webdiensts:
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
Die nachfolgenden Zähler sind ein Beispiel, während der Webserver keine Anfragen verarbeitet hat. Führen Sie Bombardier erneut mit dem api/diagscenario/tasksleepwait
Endpunkt und einer dauerhaften Last für 2 Minuten aus, damit es viel Zeit gibt, zu beobachten, was mit den Leistungsindikatoren passiert.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
ThreadPool-Starvation tritt auf, wenn keine freien Threads zum Behandeln der in die Warteschlange eingereihten Arbeitsaufgaben vorhanden sind und die Laufzeit reagiert, indem die Anzahl der ThreadPool-Threads erhöht wird. Der dotnet.thread_pool.thread.count
Wert erhöht sich schnell auf 2-3x die Anzahl der Prozessorkerne auf Ihrem Computer, und dann werden weitere Threads 1-2 pro Sekunde hinzugefügt, bis sie irgendwo über 125 stabilisiert werden. Die wichtigsten Anzeichen dafür, dass die Ressourcenknappheit des ThreadPools derzeit ein Leistungsengpass ist, sind die langsame und stetige Zunahme der ThreadPool-Threads und eine CPU-Auslastung, die deutlich unter 100%liegt. Die Threadanzahl wird erhöht, bis entweder der Pool auf die maximale Anzahl von Threads trifft, genügend Threads erstellt wurden, um alle eingehenden Arbeitsaufgaben zu erfüllen, oder die CPU wurde gesättigt. Häufig, aber nicht immer, werden bei einer ThreadPool-Starvation große Werte für dotnet.thread_pool.queue.length
und niedrige Werte für dotnet.thread_pool.work_item.count
angezeigt, was bedeutet, dass eine große Menge an anstehender Arbeit und wenig Arbeit abgeschlossen wird. Hier ist ein Beispiel der Zähler, während die Anzahl der Threads weiterhin steigt.
[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
Sobald sich die Anzahl der ThreadPool-Threads stabilisiert, leidet der Pool nicht mehr unter Ressourcenknappheit. Wenn sie jedoch mit einem hohen Wert (mehr als etwa dreimal die Anzahl der Prozessorkerne) stabilisiert wird, bedeutet dies in der Regel, dass der Anwendungscode einige ThreadPool-Threads blockiert, und der ThreadPool wird durch Ausführen mit mehr Threads kompensiert. Wenn bei konstant hohen Thread-Anzahlen keine großen Auswirkungen auf die Anforderungslatenz auftreten, aber die Auslastung im Laufe der Zeit stark schwankt oder die Anwendung regelmäßig neu gestartet wird, gerät der ThreadPool wahrscheinlich in eine Phase der Verknappung, in der die Anzahl der Threads langsam erhöht wird und eine schlechte Anforderungslatenz verursacht. Jeder Thread verbraucht außerdem Arbeitsspeicher, sodass das Reduzieren der Gesamtanzahl der benötigten Threads einen zusätzlichen Vorteil bietet.
Ab .NET 6 wurden ThreadPool-Heuristiken geändert, um die Anzahl der ThreadPool-Threads als Reaktion auf bestimmte blockierende Aufgaben-APIs viel schneller zu skalieren. ThreadPool-Überlastung kann immer noch mit diesen APIs auftreten, aber die Dauer ist viel kürzer als bei älteren .NET-Versionen, da die Runtime schneller reagiert. Führen Sie Bombardier erneut mit dem api/diagscenario/taskwait
Endpunkt aus:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
In .NET 6 sollten Sie beobachten, dass der Pool die Threadanzahl schneller erhöht als zuvor und dann bei einer hohen Anzahl von Threads stabilisiert wird. Eine Verknappung der ThreadPool-Ressourcen tritt auf, während die Anzahl der Threads zunimmt.
ThreadPool-Engpass beheben
Um ThreadPool-Starvation zu vermeiden, müssen ThreadPool-Threads nicht blockiert bleiben, damit sie zur Bearbeitung eingehender Aufgaben verfügbar sind. Es gibt mehrere Möglichkeiten, um zu bestimmen, was jeder Thread ausgeführt hat. Wenn das Problem nur gelegentlich auftritt, empfiehlt es sich, eine Ablaufverfolgung mit dotnet-trace zu erfassen, um das Anwendungsverhalten über einen bestimmten Zeitraum aufzuzeichnen. Wenn das Problem ständig auftritt, können Sie das Dotnet-Stack-Tool verwenden oder ein Dump mit dotnet-dump erfassen, das in Visual Studio angezeigt werden kann. dotnet-stack kann schneller sein, da sie die Threadstapel sofort auf der Konsole anzeigt. Das Visual Studio-Dumpdebugging bietet jedoch bessere Visualisierungen, die Frames der Quelle zuordnen, Just My Code kann Laufzeitimplementierungsframes herausfiltern, und das Feature "Parallel Stacks" kann dazu beitragen, große Anzahl von Threads mit ähnlichen Stapeln zu gruppieren. Dieses Tutorial zeigt die dotnet-stack- und dotnet-trace-Optionen. Ein Beispiel dafür, wie Sie die Threadstapel mithilfe von Visual Studio untersuchen können, finden Sie im Tutorial "Diagnosing ThreadPool starvation".
Diagnostizieren eines fortlaufenden Problems mit dotnet-stack
Führen Sie Bombardier erneut aus, um den Webserver unter Last zu setzen:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Führen Sie dann dotnet-stack aus, um die Thread-Stack-Traces anzuzeigen.
dotnet-stack report -n DiagnosticScenarios
Es sollte eine lange Ausgabe mit einer großen Anzahl von Stapeln angezeigt werden, von denen viele wie folgt aussehen:
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()
Die Frames am unteren Rand dieser Stapel geben an, dass es sich bei diesen Threads um ThreadPool-Threads handelt:
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
Und die Frames am oberen Rand zeigen, dass der Thread bei einem Aufruf von GetResultCore(bool)
innerhalb der DiagnosticScenarioController.TaskWait()-Funktion blockiert wird.
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()
Diagnostizieren eines zeitweiligen Problems mit dotnet-trace
Der Dotnet-Stack-Ansatz ist nur für offensichtliche, konsistente Blockierungsvorgänge wirksam, die in jeder Anforderung auftreten. In einigen Szenarien geschieht die Blockierung sporadisch nur alle paar Minuten, wodurch dotnet-stack weniger nützlich für die Diagnose des Problems ist. In diesem Fall können Sie dotnet-trace verwenden, um Ereignisse über einen bestimmten Zeitraum zu sammeln und in einer Nettrace-Datei zu speichern, die später analysiert werden kann.
Es gibt ein bestimmtes Ereignis, das hilft, Threadpool-Starvation zu diagnostizieren: das WaitHandleWait-Ereignis, das in .NET 9 eingeführt wurde. Er wird ausgegeben, wenn ein Thread durch Vorgänge blockiert wird, wie etwa sync-over-async-Aufrufe (z. B. Task.Result
, Task.Wait
und Task.GetAwaiter().GetResult()
) oder durch andere Sperrvorgänge wie lock
, Monitor.Enter
, ManualResetEventSlim.Wait
und SemaphoreSlim.Wait
.
Führen Sie Bombardier erneut aus, um den Webserver unter Last zu setzen:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Starten Sie dann dotnet-trace, um Wartezeitereignisse zu sammeln:
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
Dies sollte eine Datei mit dem Namen DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace
erzeugen, die die Ereignisse enthält. Dieser Netzwerkpfad kann mit zwei verschiedenen Tools analysiert werden.
- PerfView: Ein von Microsoft für Windows entwickeltes Leistungsanalysetool.
- .NET Events Viewer: Ein von der Community entwickeltes Blazor-Analyse-Webtool zur Nettrace-Analyse.
In den folgenden Abschnitten wird gezeigt, wie Sie jedes Tool zum Lesen der Nettrace-Datei verwenden.
Analysieren eines Nettraces mit Perfview
Laden Sie PerfView herunter, und führen Sie es aus.
Öffnen Sie die Nettrace-Datei, indem Sie darauf doppelklicken.
Doppelklicken Sie auf Advanced Group>Any Stacks. Ein neues Fenster wird geöffnet.
Doppelklicken Sie auf die Zeile "Ereignis Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".
Nun sollten die Stack-Traces angezeigt werden, in denen die WaitHandleWait-Ereignisse erschienen sind. Sie werden nach "WaitSource" aufgeteilt. Derzeit gibt es zwei Quellen:
MonitorWait
für Ereignisse, die über Monitor.Wait ausgegeben werden, undUnknown
für alle anderen.Beginnen Sie mit MonitorWait, wie sie 64.8% der Ereignisse darstellt. Sie können die Kontrollkästchen aktivieren, um die Stapelablaufverfolgungen zu erweitern, die für das Auslösen dieses Ereignisses verantwortlich sind.
Diese Stapelablaufverfolgung kann gelesen werden als:
Task<T>.Result
gab ein WaitHandleWait-Ereignis mit einem WaitSource MonitorWait aus (Task<T>.Result
verwendetMonitor.Wait
zum Durchführen einer Warteschleife). Es wurde aufgerufen vonDiagScenarioController.TaskWait
, das von einem Lambda aufgerufen wurde, das von einem ASP.NET-Code aufgerufen wurde.
Analysieren eines Nettrace mit .NET-Ereignisanzeige
Wechseln Sie zu verdie-g.github.io/dotnet-events-viewer.
Ziehen und ablegen Sie die Nettrace-Datei.
Wechseln Sie zur Seite " Ereignisstruktur ", wählen Sie das Ereignis "WaitHandleWaitStart" aus, und wählen Sie dann "Abfrage ausführen" aus.
Sie sollten die Stapelablaufverfolgungen sehen, in denen die Ereignisse vom Typ WaitHandleWait ausgegeben wurden. Klicken Sie auf die Pfeile, um die Stapelspuren zu erweitern, die für das Erzeugen dieses Ereignisses verantwortlich sind.
Diese Stack-Trace kann gelesen werden als:
ManualResetEventSlim.Wait
hat ein WaitHandleWait-Ereignis ausgegeben. Es wurde vonTask.SpinThenBlockWait
aufgerufen, das vonTask.InternalWaitCore
aufgerufen wurde, das vonTask<T>.Result
aufgerufen wurde, das vonDiagScenario.TaskWait
aufgerufen wurde, das von einem Lambda aufgerufen wurde, das von einem ASP.NET-Code aufgerufen wurde.
In realen Szenarien finden Sie möglicherweise viele Wartezeitereignisse, die von Threads außerhalb des Threadpools ausgegeben werden. Hier untersuchen Sie eine Threadpool-Verknappung, sodass alle Wartezeiten bei dedizierten Threads außerhalb des Threadpools nicht relevant sind. Sie können feststellen, ob ein Stack Trace aus einem Threadpool-Thread stammt, indem Sie sich die ersten Methoden ansehen, die eine Erwähnung des Threadpools enthalten sollten (z. B. WorkerThread.WorkerThreadStart
oder ThreadPoolWorkQueue
).
Codekorrektur
Jetzt können Sie zum Code für diesen Controller in der Controller/DiagnosticScenarios.cs-Datei der Beispiel-App navigieren, um zu sehen, dass eine asynchrone API ohne Verwendung await
aufgerufen wird. Dies ist das sync-over-async-Codemuster, das dafür bekannt ist, Threads zu blockieren und die häufigste Ursache für ThreadPool-Erschöpfung ist.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
In diesem Fall kann der Code problemlos geändert werden, um stattdessen die async/await zu verwenden, wie im TaskAsyncWait()
Endpunkt dargestellt. Die Verwendung von Await ermöglicht es dem aktuellen Thread, andere Arbeitselemente zu warten, während die Datenbankabfrage ausgeführt wird. Wenn die Datenbanksuche abgeschlossen ist, wird die Ausführung eines ThreadPool-Threads fortgesetzt. Auf diese Weise wird während jeder Anforderung kein Thread im Code blockiert.
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
Das Ausführen von Bombadier zum Senden der Last an den api/diagscenario/taskasyncwait
Endpunkt zeigt, dass die Threadpool-Threadanzahl viel niedriger bleibt und die durchschnittliche Latenz bei Verwendung des async/await-Ansatzes bei nahezu 500 ms bleibt:
>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