Bildiğimiz gibi memory leak’lerden veya bilinmeyen sebeplerden dolayı crash veya hang olan uygulamalarımızı debug edebilmenin en iyi yolu, dump dosyalarını incelemekten geçmektedir. En azından benim tecrübelerim genelde bu yönde oldu.
Bizler ise developer’lar olarak, en azından uygulamalarımızın neden beklenmedik şekilde davrandıklarını anlayabilecek kadar dump analizlerini yapabiliyor olmamız gerekmektedir.
Daha önce benzer konular üzerinde farklı makaleler yazmış ve sunumlar gerçekleştirmiştim.
- dotnetKonf Etkinliği – Debugging and Profiling .NET Core Applications on Linux
- .NET Core Uygulamalarının Linux Üzerinde Debugging & Profiling İşlemlerine Genel Bakış – 1 (Perf, LTTNg)
- WinDBG ile Dump Analizi Yaparak Performans Sorunlarını Çözümleme
Bu makale kapsamında ise kubernetes ortamında çalışan hem windows-based hem de linux-based container’larımızdan nasıl dump alabileceğimizi ve en basit şekilde windows üzerinde nasıl analiz edebileceğimizi göstermeye çalışacağım.
Senaryo
Azure Kubernetes Service‘i üzerine deploy ettiğimiz bir uygulamanın, bir süre sonra memory leak sebebi ile crash olduğunu varsayalım. Bu memory leak’in nereden kaynaklandığını hızlı bir şekilde araştırabilmek için ise, ilgili uygulamanın memory dump’ını incelemeye karar verdiğimizi düşünelim.
Bu senaryonun dışında uygulamalarımız belirli bir zaman sonra hang oluyor veya bilinmedik problemlerden dolayı deadlock’lar, exception’lar üretip crash de oluyor olabilir. Bunlar gibi çeşitli problemleri adresleyebilmek için uygulamaların o anki memory dump’larını alarak, kolay bir şekilde bilgi sahibi olabiliriz
Ben hızlı bir örnek gerçekleştirebilmek adına aşağıdaki gibi memory üzerinde allocation gerçekleştirecek basit bir kod bloğu hazırladım.
using System; using System.Collections.Generic; using System.Threading; namespace MemoryLeakNETFramework { class Program { static void Main(string[] args) { Console.WriteLine("App started."); var productService = new ProductService(); productService.GetProducts(); while (true) { Thread.Sleep(TimeSpan.FromMinutes(1)); } } } class ProductDTO { public int Id { get; set; } public byte[] XParameter { get; set; } } static class ProductUtil { public static byte[] CalculateSomething() { byte[] buffer = new byte[1024]; return buffer; } } class ProductService { public List<ProductDTO> GetProducts() { List<ProductDTO> products = new List<ProductDTO>(); for (int i = 0; i < 500000; i++) { var product = new ProductDTO() { Id = i, XParameter = ProductUtil.CalculateSomething() }; products.Add(product); } return products; } } }
Gördüğümüz gibi ürün listesinin “ProductDTO” class’ına mapping işlemi sırasında bir allocation işlemi gerçekleştiriyorum.
Gerçek dünyada ise aşağıdaki gibi çeşitli sebeplerden dolayı memory leak problemleri ile karşılaşabilmemiz mümkündür.
- Dispose edilmeyen unmanaged resource’lar
- In-memory caching
- Yanlış thread kullanımları
- Closure allocation’ları
- Yanlış static durumları
Windows Container ile Dump Almaya Başlayalım
İlk olarak windows-based bir container içerisinde çalışan uygulamamızın, memory dump’ını nasıl alabileceğimiz konusuna değinelim.
x86 .NET Framework 4.8 uygulamasını containerize edebilmek için ise, aşağıdaki “Dockerfile” dosyasını kullandım.
FROM mcr.microsoft.com/dotnet/framework/runtime:4.8-windowsservercore-ltsc2019 WORKDIR /app COPY . . ENTRYPOINT ["C:\\app\\MemoryLeakNETFramework.exe"]
Şimdi ilk olarak aşağıdaki komutları çalıştıralım ve deploy etmiş olduğumuz uygulamaya bir bakalım.
kubectl get pod kubectl top pod
Gördüğümüz gibi ilgili uygulama şuan 579Mi civarında bir memory kullanımı gerçekleştirmekte. Bu kullanımın ise zamanla arttığını ve ilgili uygulamanın crash olmasına neden olduğunu varsayıyoruz. Şimdi ilgili uygulama crash olmadan önce pod’unun içerisine girelim ve memory dump alma işlemini gerçekleştirelim.
Öncelikle windows container’ın powershell session’ına girebilmek için aşağıdaki komutu çalıştıralım.
kubectl exec -it YOUR_POD_NAME -- powershell
Memory dump alma işlemini gerçekleştirebilmek için ise, ProcDump tool’unu kullanacağız. ProcDump command-line üzerinden kolayca dump’lar alabilmemize olanak sağlayan bir tool’dur.
Şimdi iligli pod’un powershell session’ına girdikten sonra, ProcDump tool’unu aşağıdaki komut yardımıyla ilgili pod’un içerisine download edelim ve ardından ilgili zip dosyası içerisinden çıkartalım.
PS C:\app> Invoke-WebRequest -UseBasicParsing -Uri https://download.sysinternals.com/files/Procdump.zip -OutFile C:\app\procdump.zip PS C:\app> Expand-Archive .\procdump.zip
Şimdi ise dump almak istediğimiz uygulamanın process ID bilgisine ihtiyacımız var. Bunun için “Get-Process” komutunu çalıştıralım ve ilgili process’in ID bilgisini kopyalayalım.
Ardından “procdump” klasörüne girelim ve aşağıdaki komutu çalıştırarak dump alma işlemini gerçekleştirelim.
.\procdump.exe -ma YOUR_PROCESS_ID -s 5 -n 1 -accepteula
Burada dikkat etmemiz gereken önemli nokta, uygulamanın “x86” veya “x64” olarak hangi hedef platform’da çalıştığını belirtmektir. Eğer uygulama “x64” olarak çalışıyorsa, dump alırken “-64” parametresinin eklenmesi yeterli olacaktır. Default “x86” olarak işlem gerçekleştirmektedir.
Yukarıdaki komut kısaca, “5” saniye gibi bir süre ile bir adet full process dump alma işlemi gerçekleştirdi.
Full process dump’ı aldığımız için dump dosyasının boyutu da bi hayli büyük. Fakat bir çok durum için full dump almak hayat kurtarıcı olabiliyor.
Şimdi aşağıdaki komut yardımı ile ilgili dump dosyasını sıkıştıralım. Böylece ilgili dump dosyasını daha hızlı bir şekilde pod içerisinden kendi lokal ortamımıza kopyalayabiliriz.
PS C:\app\procdump> Compress-Archive .\MemoryLeakNETFramework.exe_220610_144815.dmp .\mydump.zip
Sıkıştırma işlemi tamamlandığına göre, ilgili “mydump.zip” dosyasını kendi lokal ortamımıza kopyalayabiliriz. Bunun için kendi lokal ortamımız üzerinde “C:\” dizini altına gelelim. Arından aşağıdaki komut ile kopyalama işlemini gerçekleştirelim.
kubectl cp YOUR_POD_NAME:/app/procdump/mydump.zip ./procdump/mydump.zip
Gördüğümüz gibi ilgili dump dosyası, “C:\procdump” dizini altına kopyalanmış durumda.
Linux Container İçerisinden Dump Alalım
Linux container ile örnek gerçekleştirebilmek için ise aynı kod bloğunu .NET 6 ile containerize bir hale getirdim ve linux-based bir nodepool’a deployment işlemini gerçekleştirdim.
Containerize edebilmek için ise aşağıdaki “Dockerfile” ı kullandım.
FROM mcr.microsoft.com/dotnet/runtime:6.0-focal AS base WORKDIR /app FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build WORKDIR /src COPY ["MemoryLeakNET6.csproj", "./"] RUN dotnet restore "MemoryLeakNET6.csproj" COPY . . WORKDIR "/src/." RUN dotnet build "MemoryLeakNET6.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "MemoryLeakNET6.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "MemoryLeakNET6.dll"]
Şimdi ise dump alabilmek için container’ın shell session’ına girelim.
kubectl exec -it YOUR_POD_NAME -- /bin/sh
Dump alma işlemini linux container içerisinde gerçekleştirebilmek için bir kaç farklı seçeneğimiz bulunmakta. Ben bu sefer Microsoft‘un .NET Core 3.1 ve üzerine için sunmuş olduğu .NET diagnostics CLI tools‘unu kullanacağım. Bu tool’lar içerisinde ise “dotnet-dump” ve “dotnet-gcdump” olmak üzere iki farklı tool bulunmakta. Ben dump analiz işlemlerini gerçekleştirirken SOS komutlarından yararlanabilmek istediğim için, “dotnet-dump” tool’unu kullanacağım.
Öncelikle aşağıdaki komutları kullanarak, “dotnet-dump” tool’unu container içerisine indirelim. Farklı platform seçeneklerine ise, buradan erişebilirsiniz.
apt-get update apt-get install wget wget -O dotnet-dump https://aka.ms/dotnet-dump/linux-x64 chmod 777 ./dotnet-dump
Şimdi dump almak istediğimiz uygulamanın process ID bilgisine erişebilmek için ise, “./dotnet-dump ps” komutunu çalıştıralım.
Process ID bilgisini elde ettikten sonra, aşağıdaki komut ile uygulamanın dump’ını alalım.
./dotnet-dump collect -p YOUR_PROCESS_ID
Gördüğümüz gibi “630MB” lik bir full dump dosyası oluşturulmuş durumda.
Local ortamımıza kopyalamadan önce ilgili dump dosyasını aynı şekilde sıkıştırmamız gerekmektedir. Bu sefer ilgili container içerisinde bulunan “gzip” tool’undan yararlanabiliriz.
gzip DUMP_NAME
Ardından “.gz” uzantılı dump dosyasını, kendi local ortamımıza aşağıdaki gibi kopyalayabiliriz.
kubectl cp YOUR_POD_NAME:/app/DUMP_NAME.gz ./dotnetdump/mydump.gz
WinDbg ile Dump Analizine Başlayalım
Dump dosyalarını analiz edebilmek için WinDbg, Visual Studio, PerfViewvb. gibi farklı tool seçenekleri bulunmaktadır. Ben bu makale kapsamında ise windows container’dan almış olduğumuz dump için, WinDbg‘ı kullanacağım. Eğer daha görsel bir tool tercih etmek isterseniz ise diğer seçenekleri de değerlendirebilirsiniz.
WinDbg‘a sahip değilseniz buradaki link üzerinden “Debugging Tools for Windows” başlığını takip ederek elde edebilirsiniz. Ayrıca WinDbg Preview ismi ile yeni bir versiyonu da bulunmakta. Fakat ben eski versiyonu gibi stable bir şekilde çalıştıramadım. Bu yüzden preview olmayan versiyonu üzerinden ilerleyeceğim.
Dump alma işlemini x86 hedef platform’u ile gerçekleştirdiğimiz için, WinDbg (x86) versiyonunu çalıştıralım ve “File>Open Crash Dump” menüsünü takip ederek “C:\procdump” dizini altına kopyalamış olduğumuz dump dosyasını seçelim.
Şimdi ilk olarak debugging işlemlerinde symbol’leri kullanabilmek için (callstack, variables), aşağıdaki gibi symbol search path’ine “Microsoft Symbol” server ve uygulamanın pdb dosyalarının bulunduğu path’leri ekleyelim.
.sympath srv*https://msdl.microsoft.com/download/symbols .sympath+ C:\source\MemoryLeakNETFramework\bin\Debug
Ardından detaylı symbol log’larını görebilmek ve yeni symbol bilgilerinin yüklenebilmesi için, aşağıdaki iki komutu çalıştıralım.
!sym noisy .reload
Şimdi ise managed code debug işlemini gerçekleştirebilmemiz için SOS Debugging Extension‘ını, WinDbg içerisine yüklememiz gerekmektedir. Bu yükleme işlemi sırasında ise yükleyecek olduğumuz SOS extension’ının versiyonu ve bitness’ı, dump’ın alındığı host üzerindeki CLR versiyonu ve bitness’ı ile eşleşiyor olması gerekmektedir.
Neyseki “!analyze –v” komutu ile uygun olan SOS extension kolay bir şekilde yüklenebilmektedir. Normalde crash dump’ları için exception analizlerinde kullanabileceğimiz bu komut’u, gerekli olan tüm dll’lerin otomatik olarak symbol search path’leri üzerinden yüklenebilmesi için de kullanabiliriz.
NOT: Uygun SOS dll’inin bulunamaması durumunda ise işler biraz karmaşıklaşmaktadır. Eğer dump’ın alındığı hedef server’a hala erişim sağlanabilyorsa, oradan ilgili SOS ve mscordacwks dll’lerinin alınması veya ilgili Microsoft update patch’inin bulunup, ilgili patch içersinden ilgili dll’lerin alınması gerekmektedir. Ardından “.load C:\SOS\sos.XXX.dll” komutu ile WinDbg içerisine manuel bir şekilde dahil edilmelidir.
Şimdi ilgili SOS extension’ın yüklenebilmesi için aşağıdaki gibi “!analyze –v” komutunu çalıştıralım.
Gördüğümüz gibi SOS extension’ı, “x86_4.8.4515.00” versiyonu ile WinDbg içerisine load edilmiş durumda. Ayrıca load edilmiş olan diğer extension’ları da görebilmek için, “.chain” komutunu çalıştırmamız yeterli olacaktır.
Artık heap’i incelemeye başlayabiliriz. Bunun için ilk olarak aşağıdaki komutu çalıştıralım.
!dumpheap -stat
Bu komut bize kısaca managed heap içerisinde allocate edilmiş objeleri ve onların ne kadar memory kullandıklarının istatistiksel bir özetini göstermektedir. Bizimde örnek senaryo gereği amacımız memory leak’e sebebiyet veren noktayı belirleyebilmek olduğu için, heap üzerinde neler olup bittiğini incelemek bizi doğru bir noktaya götürecektir.
Yukarıdaki resme baktığımızda “MemoryLeakNETFramework.ProductDTO” ve “System.Byte[]” objelerinin “500000” kere kayıt edildiğini ve ortalama “8MB” ve “518MB” yer kaplıyor olduklarını görebiliriz. Bu bilgilerden yola çıkarak memory leak oluşumunun bu noktalardan kaynaklanıyor olabileceğini sanırım söyleyebiliriz. Kısacası memory leak kaynağının izini sürerken objelerin ya çok fazla kayıt ediliyor olmasına yada kapladıkları alanlarına göre inceliyor olmamız gerekmektedir.
Şimdi “MemoryLeakNETFramework.ProductDTO” objesinin biraz daha detaylarına bakalım. Bunun için öncelikle bu objenin method table’ına erişmemiz gerekmektedir. Yani “MemoryLeakNETFramework.ProductDTO” objesinin ilk sütun’unda bulunan MT (Method Table) adresini kopyalayalım ve aşağıdaki komutu çalıştıralım.
!dumpheap -mt 010d4e80
Ardından listelenen sonuçlar içerisinden herhangi bir instance’ın memory adresini alarak, aşağıdaki gibi detaylarına erişelim sağlayalım.
!dumpobj 386fde58
Gördüğümüz gibi memory’deki objeleri dump ederek, dump’ını aldığımız andaki değerlerine erişebilmekteyiz.
Dilersek “!objsize” komutu ile, istediğimiz bir objenin size’ını da ayrıca görebiliriz. Örneğin bu obje içerisindeki “System.Byte[]” field’ının size’ını görebilmek için, “Value” sütun’unda bulunan referans adresini kopyalayalım ve aşağıdaki komut’u çalıştıralım.
!objsize 386fde74
Gördüğümüz gibi “MemoryLeakNETFramework.ProductDTO” objesi içerisinde bulunan “System.Byte[]” tipindeki field, memory’de “1036” byte’lık yer kaplamaktadır. Heap içerisinde “500000” kere kayıt edildiğini de göz önüne alırsak, ortalama “518MB” lik bir allocation’ın bu field’lar sebebiyle gerçekleştiğini söyleyebiliriz.
Dotnet-Dump ile Core Dump’ı Analiz Edelim
Şimdi ise “dotnet-dump” tool’u ile linux container içerisinden almış olduğumuz core dump’ın analiz işlemine bir bakalım. “dotnet-dump” tool’u linux veya windows container’lar içerisinden dump alabilmemizi sağladığı gibi, ayrıca dump’ı analiz edebilmemizi de sağlamaktadır. Hatta ilgili dump dosyasını local ortamımıza taşımadan, ilgili container içerisinde de ad-hoc analizler gerçekleştirebilmemize olanak tanımaktadır.
Şimdi local ortamımızda analiz işlemini gerçekleştirmeden önce, “dotnet-dump” tool’unu aşağıdaki gibi local ortamımıza da kuralım.
dotnet tool install --global dotnet-dump
Ardından aşağıdaki komutu çalıştırarak, “dotnet-dump” ‘ın terminal üzerinden interaktif analiz session’ına erişelim.
dotnet-dump analyze .\YOUR_CORE_DUMP_PATH
Artık bu session üzerinden WinDbg ile analiz gerçekleştirdiğimiz gibi, yine aynı SOS komutlarından (bir çoğu) yararlanarak istediğimiz analizleri gerçekleştirebiliriz.
Örneğin yine heap’i incelemek için, aşağıdaki aynı komutu kullanalım.
dumpheap -stat
Gördüğümüz gibi yine SOS komutları ile aynı işlemleri uygulayarak, memory leak kaynağını aynı şekilde inceleyebiliriz.
Bunlar dışında yararlı bulduğum bazı komutlar ise:
- “threads” komutu ile tüm thread’leri listeleyip, istediğimiz thread’ler arası geçişler yapabiliriz. WinDbg için “~[threadid]s” komutu, “dotnet-dump” için ise “threads [threadid]” komutunu kullanabiliriz.
- “clrstack” komutu ile de ilgili thread’in stack’ini görüntüleyebilir ve memory’de neler olup bittiğini inceleyebilirz. Örneğin exception’ları görebilmek gibi.
- “gcroot” komutu ile ise ilgili objeyi referans olarak tutan diğer tüm objeleri listeleyebiliriz.
- “syncblk” komutu ile sync block table’a bakabiliriz. Özellikle deadlock’ları incelemek istiyorsak hangi thread’in lock’ı tuttuğunu sync block table üzerinden bulup, daha sonra ilgili thread’in clrstack’ini inceleyebiliriz.