이 문서는 .NET 9.0 이상 버전에 적용됩니다 ✔️.
이 자습서에서는 ThreadPool 고갈 시나리오를 디버그하는 방법을 알아봅니다. ThreadPool 고갈은 풀에 새 작업 항목을 처리하는 데 사용할 수 있는 스레드가 없고 애플리케이션이 느리게 응답하는 경우가 많을 때 발생합니다. 제공된 예제 ASP.NET Core 웹앱을 사용하여 ThreadPool을 의도적으로 고갈시키고 진단하는 방법을 알아볼 수 있습니다.
이 자습서에서는 다음을 수행합니다.
- 요청에 느리게 응답하는 앱 조사
- dotnet-counters 도구를 사용하여 ThreadPool 고갈이 발생할 가능성이 있는지 식별합니다.
- dotnet-stack 및 dotnet-trace 도구를 사용하여 ThreadPool 스레드를 사용 중인 상태로 유지하는 작업을 결정합니다.
필수 조건
이 자습서에서는 다음을 사용합니다.
- 샘플 앱을 빌드하고 실행하는 .NET 9 SDK
- ThreadPool 고갈 동작을 보여 주는 샘플 웹앱
- 샘플 웹앱의 부하를 생성하기 위한 Bombardier
- dotnet-counters로 성능 카운터 관찰하기
- 스레드 스택을 검사하는 dotnet-stack
- 대기 이벤트를 수집하는 dotnet-trace
- 선택 사항: 대기 이벤트를 분석하는 PerfView
샘플 앱 실행
샘플 앱에 대한 코드를 다운로드하고 .NET SDK를 사용하여 실행합니다.
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
웹 브라우저를 사용하여 https://localhost:5001/api/diagscenario/taskwait에 요청을 보내는 경우, 약 500 ms 후에 success:taskwait의 응답이 반환되는 것을 확인할 수 있습니다. 웹 서버가 예상대로 트래픽을 처리하고 있음을 보여 줍니다.
성능 저하 관찰
데모 웹 서버에는 데이터베이스 요청을 모의한 다음 사용자에게 응답을 반환하는 여러 엔드포인트가 있습니다. 이러한 각 엔드포인트는 요청을 한 번에 하나씩 처리할 때 약 500ms의 지연이 발생하지만 웹 서버에서 부하가 발생할 경우 성능이 훨씬 저하됩니다. Bombardier 부하 테스트 도구를 다운로드하고 각 엔드포인트에 125개의 동시 요청이 전송될 때 대기 시간의 차이를 관찰합니다.
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
이 두 번째 엔드포인트는 더 나쁜 작업을 수행하는 코드 패턴을 사용합니다.
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
이러한 두 엔드포인트는 부하가 높을 때 평균 대기 시간(각각 3.48초 및 15.42초)보다 훨씬 더 많이 표시됩니다. 이전 버전의 .NET Core에서 이 예제를 실행하면 두 예제가 모두 똑같이 잘못 수행되는 것을 볼 수 있습니다. .NET 6은 첫 번째 예제에서 사용되는 잘못된 코딩 패턴의 성능 영향을 줄이는 ThreadPool 추론을 업데이트했습니다.
ThreadPool 고갈 감지
실제 서비스에서 위의 동작을 관찰한 경우 부하가 느려지는 것을 알 수 있지만 원인을 알 수 없습니다. dotnet-counters 는 라이브 성능 카운터를 표시할 수 있는 도구입니다. 이러한 카운터는 특정 문제에 대한 단서를 제공할 수 있으며 종종 쉽게 얻을 수 있습니다. 프로덕션 환경에서는 원격 모니터링 도구 및 웹 대시보드에서 제공하는 유사한 카운터가 있을 수 있습니다. dotnet-counters를 설치하고 웹 서비스 모니터링을 시작합니다.
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
앱이 .NET 9보다 오래된 .NET 버전을 실행하는 경우 dotnet-counters의 출력 UI는 약간 다르게 표시됩니다. 자세한 내용은 dotnet-counters를 참조하세요 .
앞의 카운터는 웹 서버가 요청을 처리하지 않은 경우의 예입니다. 엔드포인트를 사용하여 Bombardier를 api/diagscenario/tasksleepwait 다시 실행하고 2분 동안 부하를 유지하므로 성능 카운터에 어떤 일이 일어나는지 관찰할 충분한 시간이 있습니다.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
ThreadPool 고갈은 대기 중인 작업 항목을 처리할 자유 스레드가 없고 ThreadPool 스레드 수를 늘려 런타임이 응답할 때 발생합니다. 이 값은 dotnet.thread_pool.thread.count 컴퓨터의 프로세서 코어 수의 2~3배로 빠르게 증가한 다음, 125 이상에서 안정화될 때까지 추가 스레드가 초당 1-2개 추가됩니다. ThreadPool 고갈이 현재 성능 병목 상태라는 주요 신호는 ThreadPool 스레드의 느리고 꾸준한 증가와 CPU 사용량이%100개 미만이라는 것입니다. 풀이 최대 스레드 수에 도달하거나, 들어오는 모든 작업 항목을 충족하기에 충분한 스레드가 만들어졌거나, CPU가 포화될 때까지 스레드 수 증가가 계속됩니다. ThreadPool 고갈은 흔히, 그러나 항상 그런 것은 아니지만, dotnet.thread_pool.queue.length에 큰 값이 나타나고 dotnet.thread_pool.work_item.count에 낮은 값이 나타납니다. 이는 보류 중인 작업이 많은 반면, 완료되는 작업이 거의 없음을 의미합니다. 다음은 스레드 수가 계속 증가하는 동안 카운터의 예입니다.
[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
ThreadPool 스레드 수가 안정화되면 풀이 더 이상 부족하지 않습니다. 그러나 높은 값(프로세서 코어 수의 약 3배 이상)으로 안정화되는 경우 일반적으로 애플리케이션 코드가 일부 ThreadPool 스레드를 차단하고 ThreadPool이 더 많은 스레드로 실행하여 보정됨을 나타냅니다. 높은 스레드 수에서 안정적으로 실행하는 것은 요청 대기 시간에 큰 영향을 미치지는 않지만 시간이 지남에 따라 부하가 크게 달라지거나 앱이 주기적으로 다시 시작되는 경우 ThreadPool이 스레드를 느리게 증가시키고 요청 대기 시간을 저하시키는 고갈의 기간에 들어갈 가능성이 높습니다. 또한 각 스레드는 메모리를 사용하므로 필요한 총 스레드 수를 줄이면 또 다른 이점이 있습니다.
.NET 6부터 ThreadPool 추론은 특정 차단 작업 API에 대한 응답으로 ThreadPool 스레드 수를 훨씬 더 빠르게 확장하도록 수정되었습니다. ThreadPool 고갈은 이러한 API에서 계속 발생할 수 있지만 런타임이 더 빠르게 응답하기 때문에 이전 .NET 버전보다 기간이 훨씬 짧습니다.
api/diagscenario/taskwait 엔드포인트를 사용하여 Bombardier를 다시 실행합니다.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
.NET 6에서는 풀이 이전보다 더 빠르게 스레드 수를 증가시키고 많은 수의 스레드에서 안정화되는 것을 관찰해야 합니다. 스레드 수가 상승하는 동안 ThreadPool 고갈이 발생합니다.
ThreadPool 고갈 해결
ThreadPool 고갈을 제거하려면 들어오는 작업 항목을 처리할 수 있도록 ThreadPool 스레드가 차단 해제된 상태로 유지되어야 합니다. 각 스레드가 수행한 작업을 확인하는 방법에는 여러 가지가 있습니다. 문제가 가끔 발생하는 경우 dotnet-trace 를 사용하여 추적을 수집하는 것이 일정 기간 동안 애플리케이션 동작을 기록하는 것이 가장 좋습니다. 문제가 지속적으로 발생하는 경우 dotnet-stack 도구를 사용하거나 Visual Studio에서 볼 수 있는 dotnet-dump가 있는 덤프를 캡처할 수 있습니다. dotnet-stack은 콘솔에 스레드 스택을 즉시 표시하므로 더 빠를 수 있습니다. 그러나 Visual Studio 덤프 디버깅은 프레임을 원본에 매핑하는 더 나은 시각화를 제공하고, 내 코드만 실행 프레임을 필터링할 수 있으며, 병렬 스택 기능은 유사한 스택을 사용하여 많은 수의 스레드를 그룹화하는 데 도움이 될 수 있습니다. 이 자습서에서는 dotnet-stack 및 dotnet-trace 옵션을 보여줍니다. Visual Studio를 사용하여 스레드 스택을 조사하는 예제는 ThreadPool 고갈 진단 자습서 비디오를 참조하세요.
dotnet-stack을 사용하여 연속 문제 진단
Bombardier를 다시 실행하여 웹 서버를 로드합니다.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
그런 다음 dotnet-stack을 실행하여 스레드 스택 추적을 확인합니다.
dotnet-stack report -n DiagnosticScenarios
많은 수의 스택이 포함된 긴 출력이 표시되어야 하며, 그 중 상당수는 다음과 같습니다.
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()
이러한 스택의 맨 아래에 있는 프레임은 이러한 스레드가 ThreadPool 스레드임을 나타냅니다.
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
위쪽에 있는 프레임은 DiagnosticScenarioController.TaskWait() 함수에서 호출할 GetResultCore(bool) 때 스레드가 차단됨을 표시합니다.
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()
dotnet-trace를 사용하여 일시적인 문제 진단
dotnet-stack 접근 방식은 모든 요청에서 발생하는 명백하고 일관된 차단 작업에만 적용됩니다. 일부 시나리오에서는 차단이 몇 분마다 산발적으로 발생하므로 dotnet-stack은 문제를 진단하는 데 덜 유용합니다. 이 경우 dotnet 추적을 사용하여 일정 기간 동안 이벤트를 수집하고 나중에 분석할 수 있는 nettrace 파일에 저장할 수 있습니다.
스레드 풀 고갈을 진단하는 데 도움이 되는 특정 이벤트 중 하나는 .NET 9에서 도입된 WaitHandleWait 이벤트입니다. 스레드가 동기화-비동기 호출(예: Task.Result, Task.Wait, Task.GetAwaiter().GetResult())이나 lock, Monitor.Enter, ManualResetEventSlim.Wait, SemaphoreSlim.Wait와 같은 다른 잠금 작업으로 인해 차단될 때 내보내집니다.
Bombardier를 다시 실행하여 웹 서버를 로드합니다.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
그런 다음, dotnet-trace를 실행하여 대기 이벤트를 수집합니다.
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
이벤트를 포함하는 이름이 지정된 DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace 파일을 생성해야 합니다. 이 nettrace는 다음 두 가지 도구를 사용하여 분석할 수 있습니다.
- PerfView: Windows용 Microsoft에서 개발한 성능 분석 도구입니다.
- .NET 이벤트 뷰어: 커뮤니티에서 개발한 Nettrace 분석 Blazor 웹 도구입니다.
다음 섹션에서는 각 도구를 사용하여 nettrace 파일을 읽는 방법을 보여줍니다.
Perfview를 사용하여 nettrace 분석
PerfView를 다운로드하고 실행합니다.
nettrace 파일을 두 번 클릭하여 엽니다.
고급 그룹>임의의 스택을 두 번 클릭합니다. 새 창이 열립니다.
"Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start" 줄을 두 번 클릭합니다.
이제 WaitHandleWait 이벤트가 내보내진 스택 추적이 표시됩니다. "WaitSource"에 의해 분할됩니다. 현재
MonitorWait를 통해 내보내는 이벤트와Unknown다른 모든 소스의 두 가지 원본이 있습니다.이벤트의 64.8%을 나타내므로 MonitorWait부터 시작하세요. 확인란을 선택하여 이 이벤트를 내보내는 스택 추적을 확장할 수 있습니다.
이 스택 추적을 다음과 같이 읽을 수 있습니다:
Task<T>.Result이(가)Task<T>.ResultMonitor.Wait를 사용하여 WaitSource MonitorWait으로 WaitHandleWait 이벤트를 발생시켰습니다. 일부 ASP.NET 코드에 의해 호출된 람다에 의해 호출된DiagScenarioController.TaskWait에 의해 호출되었다.
.NET 이벤트 뷰어를 사용하여 nettrace 분석
nettrace 파일을 드래그 앤 드롭합니다.
이벤트 트리 페이지로 이동하여 "WaitHandleWaitStart" 이벤트를 선택한 다음 쿼리 실행을 선택합니다.
당신은 WaitHandleWait 이벤트가 발생한 스택 추적을 확인해야 합니다. 화살표를 클릭하여 이 이벤트를 내보내는 스택 추적을 확장합니다.
이 스택 추적은 WaitHandleWait 이벤트를 내보낸 것으로
ManualResetEventSlim.Wait읽을 수 있습니다.Task.SpinThenBlockWait에 의해 호출된Task.InternalWaitCore에 의해 호출된Task<T>.Result에 의해 호출된DiagScenario.TaskWait에 의해 호출된 일부 람다에 의해 호출된 일부 ASP.NET 코드에 의해 호출되었습니다.
실제 시나리오에서는 스레드 풀 외부의 스레드에서 내보내는 대기 이벤트를 많이 찾을 수 있습니다. 여기서는 스레드 풀의 고갈을 조사하고 있으므로 스레드 풀 외부의 전용 스레드에 대한 모든 대기는 관련이 없습니다. 스레드 풀에 대한 멘션을 포함해야 하는 첫 번째 메서드(예 WorkerThread.WorkerThreadStart : 또는 ThreadPoolWorkQueue)를 확인하여 스레드 풀 스레드에서 스택 추적이 있는지 확인할 수 있습니다.
코드 수정
이제 샘플 앱의 Controllers/DiagnosticScenarios.cs 파일에서 이 컨트롤러에 대한 코드로 이동하여 사용하지 await않고 비동기 API를 호출하고 있는지 확인할 수 있습니다. 이는 스레드를 차단하는 것으로 알려져 있으며 ThreadPool 고갈의 가장 일반적인 원인인 동기화 오버 비동기 코드 패턴입니다.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
이 경우 엔드포인트에 표시된 TaskAsyncWait() 대로 비동기/await를 대신 사용하도록 코드를 쉽게 변경할 수 있습니다. await를 사용하면 데이터베이스 쿼리가 진행 중인 동안 현재 스레드가 다른 작업 사이트를 서비스할 수 있습니다. 데이터베이스 조회가 완료되면 ThreadPool 스레드가 실행을 다시 시작합니다. 이렇게 하면 각 요청 중에 코드에서 스레드가 차단되지 않습니다.
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
엔드포인트에 부하를 보내기 위해 api/diagscenario/taskasyncwait Bombadier를 실행하면 ThreadPool 스레드 수가 훨씬 낮게 유지되고 비동기/대기 방법을 사용할 때 평균 대기 시간이 500ms 가까이 유지됨을 보여 줍니다.
>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
.NET