Berburu dan Menganalisis Penggunaan CPU yang Tinggi di Aplikasi .NET
Diterbitkan: 2022-03-11Pengembangan perangkat lunak bisa menjadi proses yang sangat rumit. Kami sebagai pengembang perlu mempertimbangkan banyak variabel yang berbeda. Beberapa tidak berada di bawah kendali kami, beberapa tidak kami ketahui pada saat eksekusi kode yang sebenarnya, dan beberapa secara langsung dikendalikan oleh kami. Dan pengembang .NET tidak terkecuali dalam hal ini.
Mengingat kenyataan ini, segala sesuatunya biasanya berjalan sesuai rencana ketika kita bekerja di lingkungan yang terkendali. Contohnya adalah mesin pengembangan kami, atau lingkungan integrasi yang dapat kami akses penuh. Dalam situasi ini, kami memiliki alat yang kami miliki untuk menganalisis berbagai variabel yang memengaruhi kode dan perangkat lunak kami. Dalam kasus ini, kami juga tidak harus berurusan dengan beban server yang berat, atau pengguna bersamaan yang mencoba melakukan hal yang sama pada saat yang bersamaan.
Dalam situasi yang dijelaskan dan aman, kode kami akan berfungsi dengan baik, tetapi dalam produksi di bawah beban berat atau beberapa faktor eksternal lainnya, masalah tak terduga dapat terjadi. Kinerja perangkat lunak dalam produksi sulit untuk dianalisis. Sebagian besar waktu kita harus berurusan dengan masalah potensial dalam skenario teoretis: kita tahu bahwa masalah bisa terjadi, tetapi kita tidak bisa mengujinya. Itu sebabnya kami perlu mendasarkan pengembangan kami pada praktik dan dokumentasi terbaik untuk bahasa yang kami gunakan, dan menghindari kesalahan umum.
Seperti yang disebutkan, ketika perangkat lunak ditayangkan, ada yang salah, dan kode dapat mulai dieksekusi dengan cara yang tidak kami rencanakan. Kita bisa berakhir dalam situasi ketika kita harus menghadapi masalah tanpa kemampuan untuk men-debug atau mengetahui dengan pasti apa yang sedang terjadi. Apa yang bisa kita lakukan dalam kasus ini?
Pada artikel ini kita akan menganalisis skenario kasus nyata penggunaan CPU yang tinggi dari aplikasi web .NET di server berbasis Windows, proses yang terlibat untuk mengidentifikasi masalah, dan yang lebih penting, mengapa masalah ini terjadi dan bagaimana kita menyelesaikannya.
Penggunaan CPU dan konsumsi memori adalah topik yang banyak dibahas. Biasanya sangat sulit untuk mengetahui dengan pasti berapa jumlah sumber daya yang tepat (CPU, RAM, I/O) yang harus digunakan oleh proses tertentu, dan untuk jangka waktu berapa. Meskipun satu hal yang pasti - jika suatu proses menggunakan lebih dari 90% CPU untuk jangka waktu yang lama, kami berada dalam masalah hanya karena fakta bahwa server tidak akan dapat memproses permintaan lain dalam keadaan ini.
Apakah ini berarti ada masalah dengan proses itu sendiri? Belum tentu. Bisa jadi proses tersebut membutuhkan lebih banyak kekuatan pemrosesan, atau sedang menangani banyak data. Sebagai permulaan, satu-satunya hal yang dapat kita lakukan adalah mencoba mengidentifikasi mengapa ini terjadi.
Semua sistem operasi memiliki beberapa alat yang berbeda untuk memantau apa yang terjadi di server. Server Windows secara khusus memiliki pengelola tugas, Monitor Kinerja, atau dalam kasus kami, kami menggunakan Server Relik Baru yang merupakan alat hebat untuk memantau server.
Gejala Pertama dan Analisis Masalah
Setelah kami menerapkan aplikasi kami, selama selang waktu dua minggu pertama kami mulai melihat bahwa server memiliki puncak penggunaan CPU, yang membuat server tidak responsif. Kami harus memulai ulang untuk membuatnya tersedia lagi, dan peristiwa ini terjadi tiga kali selama jangka waktu tersebut. Seperti yang saya sebutkan sebelumnya, kami menggunakan Server Relik Baru sebagai monitor server, dan itu menunjukkan bahwa proses w3wp.exe menggunakan 94% CPU pada saat server mogok.
Proses pekerja Layanan Informasi Internet (IIS) adalah proses windows ( w3wp.exe ) yang menjalankan aplikasi Web, dan bertanggung jawab untuk menangani permintaan yang dikirim ke Server Web untuk kumpulan aplikasi tertentu. Server IIS dapat memiliki beberapa kumpulan aplikasi (dan beberapa proses w3wp.exe yang berbeda) yang dapat menimbulkan masalah. Berdasarkan pengguna yang memiliki proses (ini ditunjukkan dalam laporan New Relic), kami mengidentifikasi bahwa masalahnya adalah aplikasi warisan formulir web .NET C# kami.
.NET Framework terintegrasi erat dengan alat debugging windows, jadi hal pertama yang kami coba lakukan adalah melihat penampil acara dan file log aplikasi untuk menemukan beberapa informasi berguna tentang apa yang sedang terjadi. Apakah kami memiliki beberapa pengecualian yang masuk ke penampil acara, mereka tidak menyediakan data yang cukup untuk dianalisis. Karena itu kami memutuskan untuk melangkah lebih jauh dan mengumpulkan lebih banyak data, sehingga ketika peristiwa itu muncul lagi kami akan siap.
Pengumpulan data
Cara termudah untuk mengumpulkan dump proses mode pengguna adalah dengan Debug Diagnostic Tools v2.0 atau cukup DebugDiag. DebugDiag memiliki seperangkat alat untuk mengumpulkan data (Koleksi DebugDiag) dan menganalisis data (Analisis DebugDiag).
Jadi, mari kita mulai mendefinisikan aturan untuk mengumpulkan data dengan Alat Diagnostik Debug:
Buka Koleksi DebugDiag dan pilih
Performance.- Pilih
Performance Countersdan klikNext. - Klik
Add Perf Triggers. - Perluas objek
Processor(bukanProcess) dan pilih% Processor Time. Perhatikan bahwa jika Anda menggunakan Windows Server 2008 R2 dan Anda memiliki lebih dari 64 prosesor, pilih objekProcessor Informationalih-alih objekProcessor. - Dalam daftar instance, pilih
_Total. - Klik
Adddan kemudian klikOK. Pilih pemicu yang baru ditambahkan dan klik
Edit Thresholds.- Pilih
Abovedi dropdown. - Ubah ambang batas menjadi
80. Masukkan
20untuk jumlah detik. Anda dapat menyesuaikan nilai ini jika diperlukan, tetapi berhati-hatilah untuk tidak menentukan jumlah detik yang sedikit untuk mencegah pemicu palsu.- Klik
OK. - Klik
Next. - Klik
Add Dump Target. - Pilih
Web Application Pooldari tarik-turun. - Pilih kumpulan aplikasi Anda dari daftar kumpulan aplikasi.
- Klik
OK. - Klik
Next. - Klik
Nextlagi. - Masukkan nama untuk aturan Anda jika Anda mau dan catat lokasi di mana dump akan disimpan. Anda dapat mengubah lokasi ini jika diinginkan.
- Klik
Next. - Pilih
Activate the Rule Nowdan klikFinish.
Aturan yang dijelaskan akan membuat satu set file minidump yang ukurannya cukup kecil. Dump terakhir akan menjadi dump dengan memori penuh, dan dump itu akan jauh lebih besar. Sekarang, kita hanya perlu menunggu event high CPU terjadi lagi.

Setelah kami memiliki file dump di folder yang dipilih, kami akan menggunakan alat Analisis DebugDiag untuk menganalisis data yang dikumpulkan:
Pilih Penganalisis Kinerja.
Tambahkan file dump.
Mulai Analisis.
DebugDiag akan memakan waktu beberapa (atau beberapa) menit untuk menguraikan dump dan memberikan analisis. Setelah analisis selesai, Anda akan melihat halaman web dengan ringkasan dan banyak informasi tentang utas, mirip dengan yang berikut:
Seperti yang Anda lihat di ringkasan, ada peringatan yang mengatakan "Penggunaan CPU yang tinggi antara file dump terdeteksi pada satu atau lebih utas." Jika kami mengklik rekomendasi, kami akan mulai memahami di mana masalahnya dengan aplikasi kami. Contoh laporan kami terlihat seperti ini:
Seperti yang bisa kita lihat di laporan, ada pola terkait penggunaan CPU. Semua utas yang memiliki penggunaan CPU tinggi terkait dengan kelas yang sama. Sebelum melompat ke kode, mari kita lihat yang pertama.
Ini adalah detail untuk utas pertama dengan masalah kami. Bagian yang menarik bagi kami adalah sebagai berikut:
Di sini kami memiliki panggilan ke kode kami GameHub.OnDisconnected() yang memicu operasi bermasalah, tetapi sebelum panggilan itu kami memiliki dua panggilan Kamus, yang mungkin memberikan gambaran tentang apa yang sedang terjadi. Mari kita lihat kode .NET untuk melihat apa yang dilakukan metode itu:
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }Kami jelas punya masalah di sini. Tumpukan panggilan laporan mengatakan bahwa masalahnya ada pada Kamus, dan dalam kode ini kami mengakses kamus, dan khususnya baris yang menyebabkan masalah adalah yang ini:
if (onlineSessions.TryGetValue(userId, out connId))Ini adalah deklarasi kamus:
static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();Apa masalah dengan kode .NET ini?
Setiap orang yang memiliki pengalaman pemrograman berorientasi objek tahu bahwa variabel statis akan digunakan bersama oleh semua instance kelas ini. Mari kita lihat lebih dalam apa arti statis di dunia .NET.
Menurut spesifikasi .NET C#:
Gunakan pengubah statis untuk mendeklarasikan anggota statis, yang termasuk ke dalam tipe itu sendiri daripada objek tertentu.
Inilah yang dikatakan spesifikasi bahasa .NET C# tentang kelas dan anggota statis:
Seperti halnya dengan semua jenis kelas, informasi jenis untuk kelas statis dimuat oleh runtime bahasa umum (CLR) .NET Framework ketika program yang mereferensikan kelas dimuat. Program tidak dapat menentukan dengan tepat kapan kelas dimuat. Namun, itu dijamin untuk dimuat dan bidangnya diinisialisasi dan konstruktor statisnya dipanggil sebelum kelas direferensikan untuk pertama kalinya dalam program Anda. Konstruktor statis hanya dipanggil satu kali, dan kelas statis tetap berada di memori selama masa pakai domain aplikasi tempat program Anda berada.
Kelas non-statis dapat berisi metode statis, bidang, properti, atau peristiwa. Anggota statis dapat dipanggil di kelas bahkan ketika tidak ada instance kelas yang dibuat. Anggota statis selalu diakses dengan nama kelas, bukan nama instance. Hanya ada satu salinan anggota statis, terlepas dari berapa banyak instance kelas yang dibuat. Metode dan properti statis tidak dapat mengakses bidang dan peristiwa non-statis dalam tipe yang memuatnya, dan mereka tidak dapat mengakses variabel instan dari objek apa pun kecuali secara eksplisit diteruskan dalam parameter metode.
Ini berarti bahwa anggota statis termasuk dalam tipe itu sendiri, bukan objeknya. Mereka juga dimuat ke domain aplikasi oleh CLR, oleh karena itu anggota statis termasuk dalam proses yang menghosting aplikasi dan bukan utas tertentu.
Mengingat fakta bahwa lingkungan web adalah lingkungan multithreaded, karena setiap permintaan adalah utas baru yang dimunculkan oleh proses w3wp.exe ; dan mengingat bahwa anggota statis adalah bagian dari proses, kami mungkin memiliki skenario di mana beberapa utas berbeda mencoba mengakses data variabel statis (dibagi oleh beberapa utas), yang pada akhirnya dapat menyebabkan masalah multithreading.
Dokumentasi Kamus di bawah keamanan utas menyatakan sebagai berikut:
Dictionary<TKey, TValue>dapat mendukung banyak pembaca secara bersamaan, selama koleksi tidak dimodifikasi. Meski begitu, enumerasi melalui koleksi secara intrinsik bukan prosedur yang aman untuk thread. Dalam kasus yang jarang terjadi di mana enumerasi bersaing dengan akses tulis, koleksi harus dikunci selama seluruh enumerasi. Agar koleksi dapat diakses oleh banyak utas untuk membaca dan menulis, Anda harus menerapkan sinkronisasi Anda sendiri.
Pernyataan ini menjelaskan mengapa kita mungkin memiliki masalah ini. Berdasarkan informasi dumps, masalahnya adalah dengan metode FindEntry kamus:
Jika kita melihat kamus implementasi FindEntry, kita dapat melihat bahwa metode ini berulang melalui struktur internal (bucket) untuk menemukan nilainya.
Jadi kode .NET berikut menghitung koleksi, yang bukan merupakan operasi aman utas.
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }Kesimpulan
Seperti yang kita lihat di dump, ada beberapa utas yang mencoba mengulangi dan memodifikasi sumber daya bersama (kamus statis) pada saat yang sama, yang akhirnya menyebabkan iterasi memasuki loop tak terbatas, menyebabkan utas menghabiskan lebih dari 90% CPU .
Ada beberapa kemungkinan solusi untuk masalah ini. Yang pertama kami terapkan adalah mengunci dan menyinkronkan akses ke kamus dengan mengorbankan kinerja. Server mogok setiap hari pada waktu itu, jadi kami harus memperbaikinya sesegera mungkin. Bahkan jika ini bukan solusi optimal, itu memecahkan masalah.
Langkah selanjutnya dalam memecahkan masalah ini adalah menganalisis kode dan menemukan solusi optimal untuk ini. Untuk memfaktorkan ulang kode adalah opsi: kelas ConcurrentDictionary baru dapat mengatasi masalah ini karena hanya mengunci pada level bucket yang akan meningkatkan kinerja secara keseluruhan. Meskipun, ini adalah langkah besar, dan analisis lebih lanjut akan diperlukan.
