Bagikan melalui


Memperbaiki penyempitan ThreadPool

Artikel ini berlaku untuk: ✔️ .NET 9.0 dan versi yang lebih baru

Dalam tutorial ini, Anda akan mempelajari cara men-debug skenario kelaparan ThreadPool. Kehabisan sumber daya ThreadPool terjadi ketika pool tidak memiliki utas yang tersedia untuk memproses item kerja baru, dan hal ini sering menyebabkan aplikasi merespons dengan lambat. Dengan menggunakan contoh aplikasi web ASP.NET Core, Anda dapat menyebabkan kekurangan ThreadPool dengan sengaja dan mempelajari cara mendiagnosisnya.

Dalam tutorial ini, Anda akan:

  • Menyelidiki aplikasi yang merespons permintaan secara perlahan
  • Gunakan alat dotnet-counters untuk mengidentifikasi bahwa kelaparan ThreadPool kemungkinan besar terjadi.
  • Gunakan alat dotnet-stack dan dotnet-trace untuk menentukan pekerjaan apa yang membuat utas ThreadPool tetap bekerja.

Prasyarat

Tutorial ini menggunakan:

Menjalankan aplikasi sampel

Unduh kode untuk aplikasi sampel dan jalankan menggunakan .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

Jika Anda menggunakan browser web dan mengirim permintaan ke https://localhost:5001/api/diagscenario/taskwait, Anda akan melihat respons success:taskwait yang dikembalikan setelah sekitar 500 ms. Ini menunjukkan bahwa server web melayani lalu lintas seperti yang diharapkan.

Mengamati performa lambat

Server web demo memiliki beberapa titik akhir yang meniru melakukan permintaan database lalu mengembalikan respons kepada pengguna. Masing-masing titik akhir ini memiliki penundaan sekitar 500 ms saat melayani permintaan satu per satu tetapi performanya jauh lebih buruk ketika server web mengalami beberapa beban. Unduh alat pengujian beban Bombardier dan amati perbedaan latensi saat 125 permintaan bersamaan dikirim ke setiap titik akhir.

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

Titik akhir kedua ini menggunakan pola kode yang berkinerja lebih buruk:

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

Kedua pos akhir ini menunjukkan latensi yang jauh lebih tinggi dari rata-rata 500 ms ketika beban tinggi (3,48 detik dan 15,42 detik masing-masing untuk setiap pos akhir). Jika Anda menjalankan contoh ini pada versi .NET Core yang lebih lama, Anda mungkin melihat kedua contoh berkinerja sama buruknya. .NET 6 telah memperbarui heuristik ThreadPool yang mengurangi dampak performa pola pengodean buruk yang digunakan dalam contoh pertama.

Mendeteksi kekurangan sumber daya ThreadPool

Jika Anda mengamati perilaku di atas pada layanan dunia nyata, Anda akan tahu itu merespons dengan lambat di bawah beban tetapi Anda tidak akan tahu penyebabnya. dotnet-counters adalah alat yang dapat menampilkan penghitung kinerja secara real-time. Penghitung ini dapat memberikan petunjuk tentang masalah tertentu dan sering kali mudah didapatkan. Di lingkungan produksi, Anda mungkin memiliki penghitung serupa yang disediakan oleh alat pemantauan jarak jauh dan dasbor web. Instal penghitung dotnet dan mulai pantau layanan 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

Jika aplikasi Anda menjalankan versi .NET yang lebih lama dari .NET 9, antarmuka pengguna output penghitung dotnet akan terlihat sedikit berbeda; lihat penghitung dotnet untuk detailnya.

Penghitung sebelumnya adalah contoh saat server web tidak melayani permintaan apa pun. Jalankan Bombardier lagi selama 2 menit dengan titik akhir api/diagscenario/tasksleepwait dan beban berkelanjutan sehingga ada banyak waktu untuk mengamati apa yang terjadi pada penghitung performa.

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

Terjadi kelangkaan utas ThreadPool ketika tidak ada utas yang tersedia untuk menangani tugas yang ada dalam antrean dan runtime merespons dengan meningkatkan jumlah utas ThreadPool. Nilai dotnet.thread_pool.thread.count meningkat dengan cepat menjadi 2-3x jumlah inti prosesor pada komputer Anda, dan kemudian utas lebih lanjut ditambahkan 1-2 per detik sampai stabil di suatu tempat di atas 125. Sinyal utama kelaparan ThreadPool yang saat ini menjadi hambatan performa adalah peningkatan utas ThreadPool yang lambat dan stabil serta penggunaan CPU yang jauh di bawah 100%. Peningkatan jumlah utas akan berlanjut sampai kumpulan mencapai jumlah maksimum utas, utas yang cukup telah dibuat untuk memenuhi semua item kerja yang masuk, atau saat CPU telah mencapai titik jenuh. Sering, tetapi tidak selalu, kelaparan ThreadPool juga akan menunjukkan nilai besar untuk dotnet.thread_pool.queue.length dan nilai rendah untuk dotnet.thread_pool.work_item.count, yang berarti ada banyak pekerjaan yang tertunda dan hanya sedikit yang diselesaikan. Berikut adalah contoh dari penghitung angka saat jumlah thread masih meningkat:

[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

Setelah jumlah utas ThreadPool stabil, kumpulan tidak lagi kelaparan. Tetapi jika stabil pada nilai tinggi (lebih dari sekitar tiga kali jumlah inti prosesor), yang biasanya menunjukkan kode aplikasi memblokir beberapa utas ThreadPool dan ThreadPool mengimbangi dengan menjalankan lebih banyak utas. Menjaga stabilitas pada jumlah utas yang tinggi tidak akan selalu berdampak besar pada latensi permintaan, tetapi jika beban bervariasi secara dramatis dari waktu ke waktu atau aplikasi akan dihidupkan ulang secara berkala, maka setiap kali, ThreadPool cenderung memasuki periode kelaparan di mana ia perlahan meningkatkan jumlah utas dan menghasilkan latensi permintaan yang buruk. Setiap utas juga mengonsumsi memori, sehingga mengurangi jumlah total utas yang diperlukan memberikan manfaat lain.

Mulai dari .NET 6, heuristik ThreadPool dimodifikasi untuk meningkatkan jumlah thread ThreadPool dengan jauh lebih cepat sebagai respons terhadap API Task yang memblokir. Kekurangan sumber daya pada ThreadPool masih bisa terjadi dengan API ini, namun durasinya lebih singkat jika dibandingkan dengan versi .NET yang lebih lama karena runtime merespons lebih cepat. Jalankan Bombardier lagi pada alamat tujuan api/diagscenario/taskwait.

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

Pada .NET 6, Anda dapat mengamati bahwa kumpulan utas meningkatkan jumlahnya lebih cepat dari sebelumnya dan kemudian stabil pada jumlah utas yang tinggi. Starvasi ThreadPool terjadi ketika jumlah utas meningkat.

Mengatasi kelaparan ThreadPool

Untuk mengatasi ketidaktersediaan ThreadPool, utas ThreadPool harus tetap tidak diblokir agar dapat tersedia untuk menangani item kerja yang masuk. Ada beberapa cara untuk menentukan apa yang dilakukan setiap utas. Jika masalah hanya terjadi sesekali, maka mengumpulkan jejak dengan dotnet-trace adalah yang terbaik untuk merekam perilaku aplikasi selama jangka waktu tertentu. Jika masalah terjadi terus-menerus, maka Anda dapat menggunakan alat dotnet-stack atau mengambil cadangan dengan dotnet-dump yang dapat dilihat di Visual Studio. dotnet-stack bisa lebih cepat karena menampilkan tumpukan utas langsung di konsol. Tetapi analisis berkas dump di Visual Studio menawarkan visualisasi yang lebih baik dengan memetakan frame ke sumber, Just My Code dapat menyaring frame implementasi runtime, dan fitur Parallel Stacks dapat membantu mengelompokkan sejumlah besar utas dengan tumpukan yang serupa. Tutorial ini menunjukkan opsi dotnet-stack dan dotnet-trace. Untuk contoh meneliti tumpukan thread menggunakan Visual Studio, lihat video tutorial mendiagnosis kelaparan ThreadPool.

Mendiagnosis masalah berkelanjutan dengan dotnet-stack

Jalankan Bombardier lagi untuk menempatkan server web di bawah beban:

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

Kemudian jalankan dotnet-stack untuk melihat jejak tumpukan utas:

dotnet-stack report -n DiagnosticScenarios

Anda akan melihat output panjang yang berisi sejumlah besar tumpukan, banyak di antaranya terlihat seperti ini:

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()

Bingkai di bagian bawah tumpukan ini menunjukkan bahwa utas ini adalah utas ThreadPool:

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

Dan bingkai di dekat bagian atas mengungkapkan bahwa utas diblokir pada panggilan ke GetResultCore(bool) dari fungsi 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()

Mendiagnosis masalah yang terjadi secara intermittens menggunakan dotnet-trace

Pendekatan dotnet-stack hanya efektif untuk operasi pemblokiran yang jelas dan konsisten yang terjadi di setiap permintaan. Dalam beberapa skenario, pemblokiran terjadi secara sporatik hanya setiap beberapa menit, membuat dotnet-stack kurang berguna untuk mendiagnosis masalah. Dalam hal ini, Anda dapat menggunakan dotnet-trace untuk mengumpulkan peristiwa selama jangka waktu tertentu dan menyimpannya dalam file nettrace yang dapat dianalisis nanti.

Ada satu peristiwa tertentu yang membantu mendiagnosis kelaparan kumpulan utas: peristiwa WaitHandleWait, yang diperkenalkan di .NET 9. Ini dipancarkan ketika utas diblokir oleh operasi seperti panggilan sync-over-async (misalnya, Task.Result, Task.Wait, dan Task.GetAwaiter().GetResult()) atau oleh operasi penguncian lainnya seperti lock, Monitor.Enter, ManualResetEventSlim.Wait, dan SemaphoreSlim.Wait.

Jalankan Bombardier lagi untuk menempatkan server web di bawah beban:

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

Kemudian jalankan dotnet-trace untuk mengumpulkan peristiwa tunggu:

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

Itu harus menghasilkan file bernama DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace yang berisi peristiwa. Nettrace ini dapat dianalisis menggunakan dua alat yang berbeda:

  • PerfView: Alat analisis performa yang dikembangkan oleh Microsoft untuk Windows saja.
  • .NET Events Viewer: Alat analisis nettrace web Blazor yang dikembangkan oleh komunitas.

Bagian berikut menunjukkan cara menggunakan setiap alat untuk membaca file nettrace.

Menganalisis nettrace dengan Perfview

  1. Unduh PerfView dan jalankan.

  2. Buka file nettrace dengan mengklik dua kali.

    Cuplikan layar pembukaan file nettrace di PerfView

  3. Klik dua kali pada Grup Tingkat Lanjut>Tumpukan Mana Saja. Jendela baru terbuka.

    Cuplikan layar tampilan tumpukan apa pun di PerfView.

  4. Klik dua kali pada baris "Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".

    Sekarang Anda akan melihat jejak tumpukan tempat peristiwa WaitHandleWait dipancarkan. Mereka dibagi oleh "WaitSource". Saat ini ada dua sumber: MonitorWait untuk peristiwa yang dipancarkan melalui Monitor.Wait, dan Unknown untuk semua sumber lainnya.

    Cuplikan layar tampilan tumpukan apa pun untuk peristiwa tunggu di PerfView.

  5. Mulai dari MonitorWait karena itu mewakili 64,8% dari peristiwa%. Anda dapat memeriksa kotak centang untuk mengembangkan jejak tumpukan yang bertanggung jawab untuk menghasilkan peristiwa ini.

    Cuplikan layar tampilan tumpukan apa pun yang diperluas untuk peristiwa tunggu di PerfView.

    Jejak tumpukan ini dapat dibaca sebagai: Task<T>.Result menghasilkan peristiwa WaitHandleWait dengan WaitSource MonitorWait (Task<T>.Result menggunakan Monitor.Wait untuk melakukan penantian). Itu dipanggil oleh DiagScenarioController.TaskWait, yang dipanggil oleh lambda tertentu, yang dipanggil oleh kode ASP.NET tertentu.

Menganalisis nettrace dengan .NET Events Viewer

  1. Buka verdie-g.github.io/dotnet-events-viewer.

  2. Seret dan letakkan file nettrace.

    Cuplikan layar pembukaan nettrace di .NET Events Viewer.

  3. Buka halaman Pohon Peristiwa , pilih peristiwa "WaitHandleWaitStart", lalu pilih Jalankan kueri.

    Cuplikan layar kueri peristiwa di .NET Events Viewer.

  4. Anda akan melihat jejak tumpukan tempat peristiwa WaitHandleWait dipancarkan. Klik panah-panah untuk memperluas jejak tumpukan yang bertanggung jawab untuk menghasilkan kejadian ini.

    Cuplikan layar tampilan pohon di .NET Events Viewer.

    Jejak tumpukan ini dapat dibaca sebagai: ManualResetEventSlim.Wait menghasilkan peristiwa WaitHandleWait. Itu dipanggil oleh Task.SpinThenBlockWait, yang dipanggil oleh Task.InternalWaitCore, yang dipanggil oleh Task<T>.Result, yang dipanggil oleh DiagScenario.TaskWait, yang dipanggil oleh suatu lambda, yang dipanggil oleh suatu kode ASP.NET.

Dalam skenario dunia nyata, Anda mungkin menemukan banyak peristiwa tunggu yang dipancarkan dari utas di luar kumpulan utas. Di sini, Anda sedang menyelidiki kelaparan kumpulan utas, sehingga semua penantian pada utas khusus di luar kumpulan utas menjadi tidak relevan. Anda dapat mengetahui apakah jejak tumpukan berasal dari utas di kumpulan utas dengan melihat metode-metode pertama, yang harus memuat penyebutan kumpulan utas (misalnya, WorkerThread.WorkerThreadStart atau ThreadPoolWorkQueue).

Bagian atas pelacakan tumpukan utas kumpulan utas.

Perbaikan kode

Sekarang Anda dapat menavigasi ke kode untuk pengontrol ini dalam file Controllers/DiagnosticScenarios.cs aplikasi sampel untuk melihat bahwa ia memanggil API asinkron tanpa menggunakan await. Ini adalah pola kode sync-over-async, yang diketahui memblokir utas dan merupakan penyebab paling umum dari kelaparan ThreadPool.

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

Dalam kasus ini, kode dapat dengan mudah diubah untuk menggunakan async/await sebagai gantinya, seperti yang ditunjukkan di TaskAsyncWait() titik akhir. Menggunakan menunggu memungkinkan proses saat ini untuk melayani tugas lain sementara kueri database sedang berlangsung. Ketika pencarian database selesai, utas ThreadPool akan melanjutkan eksekusi. Dengan demikian, tidak ada utas yang terblokir dalam kode selama setiap permintaan.

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

Menjalankan Bombadier untuk mengirim beban ke api/diagscenario/taskasyncwait titik akhir menunjukkan bahwa jumlah utas ThreadPool tetap jauh lebih rendah dan latensi rata-rata tetap dekat 500ms saat menggunakan pendekatan asinkron/tunggu:

>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