.NET Uygulamalarında Yüksek CPU Kullanımını Arama ve Analiz Etme

Yayınlanan: 2022-03-11

Yazılım geliştirme çok karmaşık bir süreç olabilir. Geliştiriciler olarak birçok farklı değişkeni hesaba katmamız gerekiyor. Bazıları bizim kontrolümüz altında değil, bazıları gerçek kod yürütme anında bizim için bilinmiyor ve bazıları doğrudan bizim tarafımızdan kontrol ediliyor. Ve .NET geliştiricileri bunun bir istisnası değildir.

Bu gerçeklik göz önüne alındığında, kontrollü ortamlarda çalıştığımızda işler genellikle planladığımız gibi gider. Bir örnek, geliştirme makinemiz veya tam erişime sahip olduğumuz bir entegrasyon ortamıdır. Bu durumlarda, kodumuzu ve yazılımımızı etkileyen farklı değişkenleri analiz etmek için emrimizde olan araçlara sahibiz. Bu durumlarda, sunucunun ağır yükleriyle veya aynı şeyi aynı anda yapmaya çalışan eşzamanlı kullanıcılarla da uğraşmak zorunda değiliz.

Tanımlanmış ve güvenli durumlarda kodumuz iyi çalışacaktır, ancak ağır yük veya diğer bazı dış etkenler altında üretimde beklenmeyen sorunlar meydana gelebilir. Üretimdeki yazılım performansını analiz etmek zordur. Çoğu zaman potansiyel problemlerle teorik bir senaryoda uğraşmak zorunda kalırız: Bir problemin olabileceğini biliyoruz, ancak onu test edemeyiz. Bu nedenle, geliştirmemizi kullandığımız dil için en iyi uygulamalara ve belgelere dayandırmalı ve yaygın hatalardan kaçınmalıyız.

Belirtildiği gibi, yazılım yayına girdiğinde işler ters gidebilir ve kod, planlamadığımız bir şekilde yürütülmeye başlayabilir. Hata ayıklama yeteneği olmadan veya neler olup bittiğini kesin olarak bilmeden sorunlarla uğraşmak zorunda kaldığımız bir durumda kalabiliriz. Bu durumda ne yapabiliriz?

Yüksek CPU kullanımı, bir işlemin uzun bir süre boyunca CPU'nun %90'ından fazlasını kullanmasıdır - ve başımız belada

Bir işlem, uzun bir süre boyunca CPU'nun %90'ından fazlasını kullanıyorsa, başımız belada demektir.
Cıvıldamak

Bu makalede, Windows tabanlı bir sunucuda bir .NET web uygulamasının yüksek CPU kullanımına ilişkin gerçek bir vaka senaryosunu, sorunu belirlemek için gerekli süreçleri ve daha da önemlisi, bu sorunun neden ilk etapta olduğunu ve nasıl yaptığımızı analiz edeceğiz. çöz onu.

CPU kullanımı ve bellek tüketimi yaygın olarak tartışılan konulardır. Genellikle, belirli bir işlemin kullanması gereken doğru miktarda kaynağın (CPU, RAM, G/Ç) ne olduğunu ve hangi süre boyunca kullanacağını kesin olarak bilmek çok zordur. Kesin olan bir şey olsa da - bir işlem uzun bir süre boyunca CPU'nun %90'ından fazlasını kullanıyorsa, sunucunun bu durumda başka hiçbir isteği işleyemeyeceği gerçeğinden dolayı başımız belaya girer.

Bu, sürecin kendisinde bir sorun olduğu anlamına mı geliyor? Şart değil. İşlemin daha fazla işlem gücüne ihtiyacı olabilir veya çok fazla veri işliyor olabilir. Başlangıç ​​olarak, yapabileceğimiz tek şey bunun neden olduğunu belirlemeye çalışmaktır.

Tüm işletim sistemleri, bir sunucuda neler olup bittiğini izlemek için birkaç farklı araca sahiptir. Windows sunucularında özellikle görev yöneticisi, Performans İzleyicisi bulunur veya bizim durumumuzda sunucuları izlemek için harika bir araç olan New Relic Sunucularını kullandık.

İlk Belirtiler ve Problem Analizi

Uygulamamızı dağıttıktan sonra, ilk iki hafta gibi bir zaman aralığında sunucunun CPU kullanımının zirve yaptığını ve bu da sunucunun yanıt vermemesine neden olduğunu görmeye başladık. Tekrar kullanılabilir hale getirmek için yeniden başlatmamız gerekti ve bu olay o zaman aralığında üç kez gerçekleşti. Daha önce de bahsettiğim gibi, sunucu monitörü olarak New Relic Sunucuları kullandık ve bu, sunucu çöktüğünde w3wp.exe işleminin CPU'nun %94'ünü kullandığını gösterdi.

Internet Information Services (IIS) çalışan işlemi, Web uygulamalarını çalıştıran ve belirli bir uygulama havuzu için bir Web Sunucusuna gönderilen isteklerin işlenmesinden sorumlu olan bir windows işlemidir ( w3wp.exe ). IIS sunucusu, sorunu oluşturabilecek birkaç uygulama havuzuna (ve birkaç farklı w3wp.exe ) sahip olabilir. İşlemin sahip olduğu kullanıcıya dayanarak (bu, New Relic raporlarında gösterildi), sorunun .NET C# web formu eski uygulamamız olduğunu belirledik.

.NET Framework, Windows hata ayıklama araçlarıyla sıkı bir şekilde entegre edilmiştir, bu nedenle yapmaya çalıştığımız ilk şey, neler olduğu hakkında bazı yararlı bilgiler bulmak için olay görüntüleyiciye ve uygulama günlük dosyalarına bakmaktı. Olay görüntüleyicide oturum açmış bazı istisnalar olsa da, analiz etmek için yeterli veri sağlamadılar. Bu yüzden bir adım daha atmaya ve daha fazla veri toplamaya karar verdik, böylece olay tekrar ortaya çıktığında hazırlıklı olacaktık.

Veri toplama

Kullanıcı modu işlem dökümlerini toplamanın en kolay yolu Debug Diagnostic Tools v2.0 veya sadece DebugDiag'dır. DebugDiag, veri toplamak (DebugDiag Collection) ve verileri analiz etmek (DebugDiag Analysis) için bir dizi araca sahiptir.

Öyleyse, Hata Ayıklama Teşhis Araçları ile veri toplama kurallarını tanımlamaya başlayalım:

  1. DebugDiag Collection'ı açın ve Performance öğesini seçin.

    Hata Ayıklama Teşhis Aracı

  2. Performance Counters seçin ve Next tıklayın.
  3. Add Perf Triggers tıklayın.
  4. Processor ( Process değil) nesnesini genişletin ve % Processor Time seçin. Windows Server 2008 R2 kullanıyorsanız ve 64'ten fazla işlemciniz varsa, lütfen İşlemci nesnesi yerine Processor Processor Information nesnesini seçin.
  5. Örnekler listesinde _Total öğesini seçin.
  6. Add ve ardından OK tıklayın.
  7. Yeni eklenen tetikleyiciyi seçin ve Edit Thresholds tıklayın.

    Performans Sayaçları

  8. Açılır menüden Above ​​seçin.
  9. Eşiği 80 olarak değiştirin.
  10. Saniye sayısı için 20 girin. Gerekirse bu değeri ayarlayabilirsiniz, ancak yanlış tetiklemeleri önlemek için az sayıda saniye belirtmemeye dikkat edin.

    Performans İzleyici Tetikleyicisinin Özellikleri

  11. OK tıklayın.
  12. Next tıklayın.
  13. Add Dump Target tıklayın.
  14. Açılır menüden Web Application Pool seçin.
  15. Uygulama havuzları listesinden uygulama havuzunuzu seçin.
  16. OK tıklayın.
  17. Next tıklayın.
  18. Tekrar Next tıklayın.
  19. Dilerseniz kuralınız için bir ad girin ve dökümlerin kaydedileceği yeri not edin. İsterseniz bu konumu değiştirebilirsiniz.
  20. Next tıklayın.
  21. Activate the Rule Now seçin ve Finish tıklayın.

Tanımlanan kural, boyut olarak oldukça küçük olacak bir dizi mini döküm dosyası oluşturacaktır. Son döküm, tam belleğe sahip bir döküm olacak ve bu dökümler çok daha büyük olacak. Şimdi sadece yüksek CPU olayının tekrar olmasını beklememiz gerekiyor.

Döküm dosyalarını seçilen klasöre yerleştirdikten sonra, toplanan verileri analiz etmek için DebugDiag Analiz aracını kullanacağız:

  1. Performans Çözümleyicileri'ni seçin.

    DebugDiag Analiz Aracı

  2. Döküm dosyalarını ekleyin.

    DebugDiag Analizi Ücretli Döküm Dosyaları

  3. Analizi Başlatın.

DebugDiag, dökümleri ayrıştırmak ve bir analiz sağlamak için birkaç (veya birkaç) dakika sürecektir. Analizi tamamladığında, aşağıdakine benzer bir özet ve başlıklarla ilgili birçok bilgi içeren bir web sayfası göreceksiniz:

Analiz Özeti

Özette görebileceğiniz gibi, “Bir veya daha fazla iş parçacığında döküm dosyaları arasında yüksek CPU kullanımı algılandı” diyen bir uyarı var. Tavsiyeye tıklarsak, uygulamamızdaki sorunun nerede olduğunu anlamaya başlayacağız. Örnek raporumuz şuna benzer:

Ortalama CPU Tarafından En İyi 10 Konu

Raporda gördüğümüz gibi, CPU kullanımı ile ilgili bir kalıp var. CPU kullanımı yüksek olan tüm threadler aynı sınıfla ilgilidir. Koda geçmeden önce ilkine bir göz atalım.

.NET Çağrı Yığını

Bu, sorunumuzla ilgili ilk iş parçacığının ayrıntısıdır. Bizim için ilginç olan kısım şudur:

.NET Çağrı Yığını Ayrıntısı

Burada, sorunlu işlemi tetikleyen GameHub.OnDisconnected() bir çağrımız var, ancak bu çağrıdan önce, neler olduğu hakkında bir fikir verebilecek iki Sözlük çağrımız var. Bu yöntemin ne yaptığını görmek için .NET koduna bir göz atalım:

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

Belli ki burada bir sorunumuz var. Raporların çağrı yığını, sorunun bir Sözlükte olduğunu söyledi ve bu kodda bir sözlüğe erişiyoruz ve özellikle soruna neden olan satır şu:

 if (onlineSessions.TryGetValue(userId, out connId))

Bu sözlük bildirimidir:

 static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();

Bu .NET koduyla ilgili sorun nedir?

Nesne yönelimli programlama deneyimi olan herkes, statik değişkenlerin bu sınıfın tüm örnekleri tarafından paylaşılacağını bilir. .NET dünyasında statikin ne anlama geldiğine daha yakından bakalım.

.NET C# belirtimine göre:

Belirli bir nesneden ziyade türün kendisine ait olan bir statik üye bildirmek için statik değiştiriciyi kullanın.

.NET C# langunge belirtimleri statik sınıflar ve üyelerle ilgili şunları söylüyor:

Tüm sınıf türlerinde olduğu gibi, statik bir sınıf için tür bilgisi, sınıfa başvuran program yüklendiğinde .NET Framework ortak dil çalışma zamanı (CLR) tarafından yüklenir. Program, sınıfın tam olarak ne zaman yüklendiğini belirleyemez. Ancak, programınızda sınıfa ilk kez başvurulmadan önce yüklenmesi ve alanlarının başlatılması ve statik oluşturucusunun çağrılması garanti edilir. Statik bir kurucu yalnızca bir kez çağrılır ve statik bir sınıf, programınızın bulunduğu uygulama etki alanının ömrü boyunca bellekte kalır.

Statik olmayan bir sınıf, statik yöntemler, alanlar, özellikler veya olaylar içerebilir. Statik üye, sınıfın hiçbir örneği oluşturulmamış olsa bile bir sınıfta çağrılabilir. Statik üyeye, örnek adıyla değil, her zaman sınıf adıyla erişilir. Sınıfın kaç örneğinin oluşturulduğuna bakılmaksızın, statik bir üyenin yalnızca bir kopyası vardır. Statik yöntemler ve özellikler, içerme türlerindeki statik olmayan alanlara ve olaylara erişemez ve bir yöntem parametresinde açıkça iletilmediği sürece herhangi bir nesnenin örnek değişkenine erişemezler.

Bu, statik üyelerin nesneye değil, türün kendisine ait olduğu anlamına gelir. Ayrıca CLR tarafından uygulama etki alanına yüklenirler, bu nedenle statik üyeler belirli iş parçacıklarına değil, uygulamayı barındıran işleme aittir.

Bir web ortamının çok iş parçacıklı bir ortam olduğu gerçeği göz önüne alındığında, her istek w3wp.exe işlemi tarafından oluşturulan yeni bir iş parçacığıdır; ve statik üyelerin sürecin bir parçası olduğu göz önüne alındığında, birkaç farklı iş parçacığının statik (birkaç iş parçacığı tarafından paylaşılan) değişkenlerin verilerine erişmeye çalıştığı ve sonunda çoklu iş parçacığı sorunlarına yol açabilecek bir senaryomuz olabilir.

İş parçacığı güvenliği altındaki Sözlük belgeleri şunları belirtir:

Bir Dictionary<TKey, TValue> , koleksiyon değiştirilmediği sürece aynı anda birden çok okuyucuyu destekleyebilir. Öyle olsa bile, bir koleksiyon aracılığıyla numaralandırma, özünde iş parçacığı için güvenli bir prosedür değildir. Bir numaralandırmanın yazma erişimleriyle çakıştığı nadir durumlarda, koleksiyonun tüm numaralandırma sırasında kilitlenmesi gerekir. Koleksiyona okuma ve yazma için birden fazla iş parçacığı tarafından erişilmesine izin vermek için kendi senkronizasyonunuzu uygulamanız gerekir.

Bu ifade neden bu sorunu yaşayabileceğimizi açıklıyor. Döküm bilgilerine dayanarak, sorun sözlük FindEntry yöntemiyle ilgiliydi:

.NET Çağrı Yığını Ayrıntısı

Sözlük FindEntry uygulamasına bakarsak, yöntemin değeri bulmak için iç yapıyı (kovalar) yinelediğini görebiliriz.

Bu nedenle, aşağıdaki .NET kodu, iş parçacığı için güvenli bir işlem olmayan koleksiyonu numaralandırıyor.

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

Çözüm

Dökümlerde gördüğümüz gibi, paylaşılan bir kaynağı (statik sözlük) aynı anda yinelemeye ve değiştirmeye çalışan birkaç iş parçacığı var, bu da sonunda yinelemenin sonsuz bir döngüye girmesine ve iş parçacığının CPU'nun %90'ından fazlasını tüketmesine neden oldu. .

Bu sorun için birkaç olası çözüm var. İlk uyguladığımız, performans kaybı pahasına sözlüğe erişimi kilitlemek ve senkronize etmekti. O sırada sunucu her gün çöküyordu, bu yüzden bunu mümkün olan en kısa sürede düzeltmemiz gerekiyordu. Bu en uygun çözüm olmasa bile sorunu çözdü.

Bu sorunu çözmenin bir sonraki adımı, kodu analiz etmek ve bunun için en uygun çözümü bulmak olacaktır. Kodu yeniden düzenlemek bir seçenektir: yeni ConcurrentDictionary sınıfı, yalnızca genel performansı artıracak bir kova düzeyinde kilitlendiğinden bu sorunu çözebilir. Bununla birlikte, bu büyük bir adımdır ve daha fazla analiz gerekli olacaktır.