Mert Özen Her Satırda Daha İleri
Geliştirici Araçları Debugging ve Problem Çözme — #09

Crash ve Memory Dump Analizi: dotnet-dump, SOS Komutları ve Container'da Dump Almak

Mert Özen 8 Kas 2026 13 dk 7 görüntülenme
Crash ve Memory Dump Analizi: dotnet-dump, SOS Komutları ve Container'da Dump Almak

Crash dump'ı DOTNET_DbgEnableMiniDump ile otomatik alıyor, dotnet-dump analyze içinde pe -lines, clrthreads, dumpheap -stat, gcroot, syncblk ve dumpasync ile çöken thread'i, büyüyen static cache'i ve lock deadlock'unu buluyoruz. Container'da PID 1 ve --init farkı, SDK'sız image'a aracı koymak, Heap/Full dump'ın OOM riski ve dump'lardaki kişisel veri.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (container'da runtime 10.0.14) ve dotnet-dump 10.0.750501 ile derlenip denenmiştir; dump'lar Docker Desktop üzerindeki Linux arm64 container'larda alınmıştır.

Önce Tahmin Et

Bookstore API'si (Application Programming Interface) bir container'da çalışıyor. Bir istek, arka planda açılan bir thread'de NullReferenceException fırlatıyor ve kimse yakalamıyor. Container'a yalnızca üç ortam değişkeni verdin: DOTNET_DbgEnableMiniDump=1, bir dump yolu ve DOTNET_EnableCrashReport=1. İki soru: process çöktükten sonra container'ın durumu ne olur, "Exited" mı yoksa hâlâ "Up" mı? Ve 180 MB'lık dump dosyasını macOS'taki dotnet-dump analyze ile açmaya çalışırsan ne olur? İkisinin de cevabı aşağıda, gerçek çıktılarla. İlki beni de şaşırttı.

Dump Nedir, Log'un Yetmediği Yer Neresi?

Log, uygulamanın senin önceden "bunu yaz" dediğin anlarını kaydeder. Dump ise process'in belleğinin belirli bir andaki fotoğrafıdır: bütün thread'ler, stack'leri, heap'teki her object, kim kimi tutuyor. 08'deki log çalışması "ne zaman ve hangi request'te" sorusunu cevaplıyordu; dump "o anda içeride ne vardı" sorusunu cevaplar. Process çöktüyse ve son log satırı hiçbir şey söylemiyorsa, bellek sürekli büyüyorsa ya da uygulama bir yerde takılıp kaldıysa elindeki en zengin kanıt dump'tır.

.NET'te dump almak için iki yol var. Birincisi crash anında runtime'ın kendi kendine dump yazması; ikincisi çalışan bir process'ten dotnet-dump collect ile istediğin anda dump almak. Analiz için ikisinde de aynı araç kullanılır: dotnet-dump analyze, içinde SOS (Son of Strike, .NET runtime'ının debugger extension'ı) komutlarını çalıştıran etkileşimli bir kabuk. Bu yazıdaki her komutu Bookstore'un küçük bir kopyası üzerinde, Linux container'larda çalıştırdım.

Crash Anında Otomatik Dump

Önce bilerek çöken bir kod. Kargo etiketi basan bir class, adresi olmayan siparişte null ile karşılaşıyor ve iş ayrı bir thread'de yapıldığı için exception doğrudan process'i düşürüyor:

namespace Bookstore.Api.Shipping;

public sealed class LabelPrinter
{
    private readonly Dictionary<int, string> _addresses = new() { [1] = "Kadikoy, Istanbul" };

    public void Print(int orderId)
    {
        // adresi olmayan siparişte GetValueOrDefault null döner
        string address = _addresses.GetValueOrDefault(orderId)!;
        Console.WriteLine($"Printing label for order {orderId}: {address.ToUpperInvariant()}");
    }
}

Endpoint bu method'u new Thread(() => printer.Print(id)) ile çalıştırıyor. Crash dump'ı açan ortam değişkenleri Microsoft Learn'deki "Collect dumps on crash" sayfasında listelenmiş; ben şöyle başlattım:

docker run -d --name debugwork-app-09 -p 5409:8080 -v "$PWD/dumps:/dumps" \
  -e DOTNET_DbgEnableMiniDump=1 \
  -e DOTNET_DbgMiniDumpType=2 \
  -e 'DOTNET_DbgMiniDumpName=/dumps/crash-%e-%p-%t.dmp' \
  -e DOTNET_EnableCrashReport=1 \
  debugwork-app-09

curl -s -i -X POST http://localhost:5409/orders/42/label

Container log'unun son kısmı:

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Bookstore.Api.Shipping.LabelPrinter.Print(Int32 orderId) in /src/Shipping/LabelPrinter.cs:line 10
   at Program.<>c__DisplayClass0_1.<<Main>$>b__6() in /src/Program.cs:line 30
   at System.Threading.Thread.StartHelper.Callback(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
[createdump] Gathering state for process 1 dotnet
[createdump] Crashing thread 0021 signal 6 (0006)
[createdump] Writing crash report to file /dumps/crash-dotnet-1-1794128114.dmp.crashreport.json
[createdump] Crash report successfully written
[createdump] Writing minidump with heap to file /dumps/crash-dotnet-1-1794128114.dmp
[createdump] Written 179118080 bytes (43730 pages) to core file
[createdump] Target process is alive
[createdump] Dump successfully written in 1164ms

Değişkenlerin anlamı, dokümandaki tabloya göre:

DeğişkenAnlamıVarsayılan
DOTNET_DbgEnableMiniDump1 ise crash'te dump üretilir0
DOTNET_DbgMiniDumpType1 Mini, 2 Heap, 3 Triage, 4 Full2 (Heap)
DOTNET_DbgMiniDumpNamedump dosyasının yolu; .NET 5'ten beri şablon alır/tmp/coredump.<pid>
DOTNET_EnableCrashReportdump'ın yanına JSON (JavaScript Object Notation) crash raporu yazar; Windows'ta yoktanımsız
DOTNET_CreateDumpDiagnosticsdump sürecinin kendi tanılama log'u0

Şablonlar: %p PID (Process ID), %e çalıştırılabilir dosyanın adı, %h host adı, %t Unix epoch'tan beri saniye, %% tek bir yüzde işareti. Çıktıdaki dosya adına bak: %e "Bookstore.Api" değil "dotnet" oldu, çünkü process'i dotnet Bookstore.Api.dll ile başlatan host çalıştırılabilir dosyası dotnet. Aynı volume'a birden fazla servis dump yazıyorsa %e onları ayırmaz; %h (container'da host adı container ID'sidir) ya da dosya yoluna servis adını sabit yazmak daha işe yarar.

Crash raporu, dump'ı açmadan ilk bakış için iyi:

jq -c '.payload.threads[] | select(.crashed == "true") | {managed_exception_type, managed_exception_hresult, native_thread_id}' \
  crash-dotnet-1-1794128114.dmp.crashreport.json
{"managed_exception_type":"System.NullReferenceException","managed_exception_hresult":"0x80004003","native_thread_id":"0x21"}

Analiz: pe -lines ve clrthreads

Şimdi baştaki ikinci soru. Dump'ı Mac'te açmayı denedim:

$ dotnet tool run dotnet-dump analyze p09/dumps3/after.dmp
Loading core dump: p09/dumps3/after.dmp ...
Analyzing Windows or Linux dumps not supported when running on MacOS

Linux dump'ı Linux'ta analiz edilir. Bu yüzden analizi, uygulamanın image'ının içinde (aracı oraya da koyduğum için) çalıştırdım. clrthreads exception'ı taşıyan thread'i gösteriyor; en sağdaki sütun belirleyici:

docker run --rm -v "$PWD/dumps:/dumps" --entrypoint dotnet-dump debugwork-app-09 \
  analyze /dumps/crash-dotnet-1-1794128114.dmp -c "clrthreads" -c exit
 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1        1 0000AAAAFE06C590  2020020 Preemptive  0000FFBF7C8E15F0:0000FFBF7C8E2FF0 0000AAAAFE003490 -00001 Ukn
  15    2       15 0000AAAAFE06E520    21220 Preemptive  0000FFBF7C85E1A8:0000FFBF7C8600D8 0000AAAAFE003490 -00001 Ukn (Finalizer)
  27   13       21 0000FFBE801B9F60    21020 Cooperative 0000FFBF7C96A338:0000FFBF7C96B070 0000AAAAFE003490 -00001 Ukn System.NullReferenceException 0000ffbf7c95b390

13 thread'in yalnız üçünü bıraktım. OSID (işletim sisteminin thread ID'si) 0x21, crash raporundaki native_thread_id ile aynı. setthread 27 ile o thread'e geçip pe -lines (printexception) çalıştırınca exception kaynak satırıyla geliyor (runtime frame'lerinin bir kısmını kestim):

> setthread 27
> pe -lines
Exception object: 0000ffbf7c95b390
Exception type:   System.NullReferenceException
Message:          Object reference not set to an instance of an object.
InnerException:   <none>
StackTrace (generated):
    SP               IP               Function
    0000FFBE7EA65470 0000FFFF7E041F7C Bookstore.Api.dll!Bookstore.Api.Shipping.LabelPrinter.Print(Int32)+0xec [/src/Shipping/LabelPrinter.cs @ 10]
    0000FFBE7EA654D0 0000FFFF7E041780 Bookstore.Api.dll!Program+<>c__DisplayClass0_1.<<Main>$>b__6()+0x30 [/src/Program.cs @ 30]
    0000FFBE7EA654F0 0000FFFF7D0FE2E4 System.Private.CoreLib.dll!System.Threading.Thread+StartHelper.Callback(System.Object)+0x94

StackTraceString: <none>
HResult: 80004003

Satır numaraları, image'ta .pdb dosyası yayınlandığı için geldi; dotnet publish Release'te de portable PDB (Program Database) üretir. PDB'leri image'tan silen bir pipeline'ın varsa dump'ta method adı kalır ama satır gider.

Container'da Dump: PID 1, --init ve Araç Kurulumu

Baştaki ilk sorunun cevabı: dump yazıldıktan yaklaşık 50 saniye sonra bile container "Up 58 seconds" görünüyordu ve docker top process'i yüzde 85 CPU (Central Processing Unit) ile çalışır hâlde gösteriyordu. Aynı image'ı --init ile başlattığımda tablo değişti:

$ docker ps -a --filter name=debugwork-app-09 --format '{{.Status}}'
Exited (134) 7 seconds ago

134 = 128 + 6, yani SIGABRT (abort sinyali) ile ölüm. Fark, process'in container'da PID 1 olması. Linux, pid_namespaces man sayfasına göre bir PID namespace'inin init process'ine handler'ı kurulmamış sinyalleri teslim etmez; dotnet PID 1 iken abort beklenen şekilde sonuçlanmadı. --init (Docker'ın küçük init process'i) PID 1 olunca dotnet sıradan bir child process oluyor ve crash'te gerçekten ölüyor. Orchestrator'ın container'ı yeniden başlatabilmesi buna bağlı; Kubernetes'te aynı etkiyi image'a tini gibi bir init koyarak ya da pod'da process namespace paylaşımıyla alırsın. Bunu kendi ortamında denemeden "crash olunca pod restart olur" varsayma.

İkinci konu: production image'ında SDK (Software Development Kit) yok, dolayısıyla dotnet tool install de yok. Multi-stage build ile aracı SDK stage'inde ayrı bir klasöre kurup runtime image'ına kopyalamak yeterli:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY src/ .
RUN dotnet publish -c Release -o /app
# aracı global değil, ayrı bir klasöre kuruyoruz
RUN dotnet tool install dotnet-dump --version 10.0.750501 --tool-path /tools

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
COPY --from=build /tools /tools
ENV PATH="$PATH:/tools"
ENTRYPOINT ["dotnet", "Bookstore.Api.dll"]

Doküman bir alternatif daha veriyor: Linux x64/arm64 için tek dosyalık indirilebilir dotnet-dump. Aracı uygulamayla aynı container'da docker exec ile çalıştırdığımda Docker Desktop'ın varsayılan ayarlarıyla ek yetki gerekmedi. Ayrı bir sidecar container'dan bağlanıyorsan durum farklı: dokümana göre ptrace yetkisi (genelde --cap-add=SYS_PTRACE), ortak process namespace ve aynı /tmp gerekir. Yanlış PID'e bağlanınca araç zaten bunu söylüyor:

Unable to connect to Process 7. Please verify that /tmp/ is writable by the current user. If the target process is in a different container, ensure this process runs with 'pid: host' and has access to /proc/{pid}/root/ (requires CAP_SYS_PTRACE or privileged mode).

Burada asıl sebep basitti: --init ile dotnet'in PID'i 6 olmuştu, 7 değil. Önce dotnet-dump ps, sonra collect.

Büyüyen Bellek: İki Dump ve gcroot

İkinci vaka, "pod her gün biraz daha fazla bellek kullanıyor". Kod masum görünüyor: son görüntülenen müşterileri hatırlayan küçük bir static class.

namespace Bookstore.Api.Customers;

public static class RecentCustomers
{
    // hiçbir zaman temizlenmiyor: her request listeye bir Customer ekliyor
    private static readonly List<Customer> Items = [];
    private static readonly Lock Sync = new();

    public static void Remember(Customer customer)
    {
        lock (Sync)
        {
            Items.Add(customer);
        }
    }
}

Container'ı --cpus 1 --memory 512m ile başlattım, 2.000 istek attım, Heap dump aldım; sonra 18.000 istek daha atıp ikinci dump'ı aldım:

docker exec debugwork-app-09 dotnet-dump ps
docker exec debugwork-app-09 dotnet-dump collect -p 7 --type Heap -o /dumps/before.dmp
# 18.000 istek daha
docker exec debugwork-app-09 dotnet-dump collect -p 7 --type Heap -o /dumps/after.dmp

docker stats iki anda 32,32 MiB ve 76,72 MiB gösteriyordu; dump dosyaları 155 MB ve 214 MB. Dump boyutu working set'e eşit değildir, dosya boyutundan sızıntı büyüklüğü çıkarma. İki dump'ta dumpheap -stat'ın en alt satırları:

=== before
ffff78811140     4    28,768 Bookstore.Api.Customers.Customer[]
ffff78583f58 2,000    96,000 Bookstore.Api.Customers.Customer
ffff74dbdd58 8,292   527,344 System.String
ffff75935470 2,422 4,189,526 System.Byte[]
Total 60,918 objects, 8,544,368 bytes
=== after
ffff78811140      3    393,288 Bookstore.Api.Customers.Customer[]
ffff78583f58 20,000    960,000 Bookstore.Api.Customers.Customer
ffff74dbdd58 44,431  2,699,622 System.String
ffff75935470 20,445 41,519,257 System.Byte[]
Total 135,935 objects, 49,465,857 bytes

Okuma sırası: önce toplam (8,5 MB'tan 49,5 MB'a), sonra en çok büyüyen satır. Byte[] en büyük kalem ama tek başına bir şey söylemez; her yerde byte[] var. Asıl ipucu Customer sayısının istek sayısıyla birebir aynı olması: 2.000 ve 20.000. Her Customer'ın 2 KB'lık ProfilePhoto array'i Byte[] satırını da açıklıyor. Sonraki adım, tek bir Customer'ı canlı tutan zinciri bulmak. MT (MethodTable) ile adresleri listele, herhangi birini gcroot'a ver:

> dumpheap -mt ffff78583f58
         Address               MT           Size
    fffee60a2ef8     ffff78583f58             48
    fffee60abd48     ffff78583f58             48
    fffee60ac1c8     ffff78583f58             48
> gcroot fffeea0d8c20
Caching GC roots, this may take a while.
Subsequent runs of this command will be faster.

HandleTable:
    0000ffffb3de1308 (strong handle)
          -> fffee4003038     System.Object[]
          -> fffee60a3780     System.Collections.Generic.List<Bookstore.Api.Customers.Customer>
          -> fffee6c20080     Bookstore.Api.Customers.Customer[]
          -> fffeea0d8c20     Bookstore.Api.Customers.Customer

Found 1 unique roots.

09-heap-to-gcroot-tr.png

Zincir yukarıdan aşağı okunur. GC (garbage collector) root'u bir strong handle; onun tuttuğu 2.040 element'li Object[], runtime'ın static field değerlerini tuttuğu yer (içinde HTTP (Hypertext Transfer Protocol) header adları gibi başka static'ler de var). gcroot field adını yazmaz ama tip zinciri yetiyor: process'te tek bir List<Customer> var ve kodda static bir List<Customer> tek yerde. Düzeltme ya listeyi sınırlamak (son 100 müşteri) ya da gerçekten gerekiyorsa süresi ve boyutu olan bir cache kullanmak. Static bir collection'a "şimdilik" eklenen her şey, process yaşadıkça yaşar.

Deadlock: syncblk ve clrstack

Üçüncü vaka, "stok transferi endpoint'i 202 dönüyor ama transfer hiç bitmiyor". StockLedger'da iki method iki lock'u ters sırada alıyor; teori C# Asenkron serisinin "C# Deadlock, SynchronizationContext ve ConfigureAwait" yazısında, 06'da da debugger tarafı var. Deadlock anında aldığım Heap dump'ta görünüşü şöyle:

> syncblk
Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
    6 0000AAAAE9B9BA50            3         1 0000AAAAE9A51D70 5d  15   0000fffeeb3a1338 System.Object
    7 0000AAAAE9B9BAA8            3         1 0000FFFEC41AB1A0 5e  16   0000fffeeb3a1350 System.Object

MonitorHeld 3: sahibi 1, bekleyen her thread 2 ekler; yani her lock'un bir sahibi ve bir bekleyeni var. Sahipler 15 ve 16 numaralı thread'ler. İkisinin stack'i (en alttaki runtime frame'lerini kestim):

> setthread 15
> clrstack
OS Thread Id: 0x5d (15)
0000FFFEC2B6E3A0 0000FFFF73EE9A58 System.Threading.Monitor.Enter_Slowpath(System.Object)
0000FFFEC2B6E470 0000FFFF78A06D1C System.Threading.Monitor.Enter(System.Object, Boolean ByRef)
0000FFFEC2B6E490 0000FFFF78CCE470 Bookstore.Api.Inventory.StockLedger.MoveFromWarehouseToStore(Int32) [/src/Inventory/StockLedger.cs @ 15]
0000FFFEC2B6E4D0 0000FFFF78CCE3C0 Program+<>c__DisplayClass0_0.<<Main>$>b__4() [/src/Program.cs @ 21]
> setthread 16
> clrstack
OS Thread Id: 0x5e (16)
0000FFFEC235E3A0 0000FFFF73EE9A58 System.Threading.Monitor.Enter_Slowpath(System.Object)
0000FFFEC235E470 0000FFFF78A06D1C System.Threading.Monitor.Enter(System.Object, Boolean ByRef)
0000FFFEC235E490 0000FFFF78CCE628 Bookstore.Api.Inventory.StockLedger.MoveFromStoreToWarehouse(Int32)

İki thread de bir lock'u tutup Monitor.Enter'da öbürünü bekliyor: klasik lock sırası deadlock'u. Çok thread'li bir process'te bunu görmenin hızlı yolu parallelstacks; Visual Studio'nun Parallel Stacks penceresinin metin hâli. Çıktının sonu:

      ~~~~ 5d
         1 System.Threading.Monitor.Enter_Slowpath(Object)
         1 System.Threading.Monitor.Enter(Object, Boolean ByRef)
         1 Bookstore.Api.Inventory.StockLedger.MoveFromWarehouseToStore(Int32)
         1 Program+<>c__DisplayClass0_0.<<Main>$>b__4()
      ~~~~ 5e
         1 System.Threading.Monitor.Enter_Slowpath(Object)
         1 System.Threading.Monitor.Enter(Object, Boolean ByRef)
         1 Bookstore.Api.Inventory.StockLedger.MoveFromStoreToWarehouse(Int32)
         1 Program+<>c__DisplayClass0_0.<<Main>$>b__5()
    2 System.Threading.Thread+StartHelper.Callback(Object)
    2 System.Threading.ExecutionContext.RunInternal(ExecutionContext, ContextCallback, Object)
==> 11 threads with 3 roots

Aynı dump'ta dumpasync, beş dakikalık bir kargo durumu çağrısında bekleyen async akışları zincir hâlinde gösterdi; thread'e bağlı olmadıkları için bunları clrstack göremez (zincirin Kestrel tarafını kestim):

STACK 2
0000fffeeb3a9658 0000ffff77c24f60 ( ) System.Threading.Tasks.Task+DelayPromiseWithCancellation
  0000fffeeb3a97d8 0000ffff78d228a8 (0) Bookstore.Api.Shipping.ShippingClient+<GetStatusAsync>d__0
    0000fffeeb3a9840 0000ffff78d23550 (0) Program+<>c+<<<Main>$>b__0_3>d
      0000fffeeb3a98a8 0000ffff78d24518 (0) Microsoft.AspNetCore.Http.RequestDelegateFactory+<ExecuteTaskResult>d__144<Microsoft.AspNetCore.Http.IResult>

threadpool ise havuzun durumunu verir: bu dump'ta "Workers Total: 2, Workers Running: 0", yani deadlock thread pool'da değil, elle açılmış iki thread'de. Thread pool'un tükenmesi ayrı bir hikâye; 10'da ona bakıyoruz.

Dump'ı Visual Studio 2026'da Açmak

Bu makinede Windows olmadığı için bu kısmı çalıştırmadım; Microsoft Learn'deki "Use dump files in the debugger" sayfasından aktarıyorum. Visual Studio'da File > Open > File ile .dmp dosyasını açınca Minidump File Summary penceresi gelir. Oradan Debug with Managed Only ile dump'ı bir breakpoint'te durmuş gibi gezersin: Call Stack, Locals, Parallel Stacks; ilerletemezsin. Run Diagnostic Analysis ise dump üzerinde hazır analyzer'ları çalıştırır. Doküman, Visual Studio'nun Linux'tan gelen managed dump'ları açabildiğini açıkça yazıyor; sembol ve binary'lerin dump'taki sürümle birebir eşleşmesi gerekiyor. Pratikte ben önce dotnet-dump ile hipotezi daraltıp, kaynak kodla yan yana gezmek gerektiğinde IDE'ye (Integrated Development Environment) geçiyorum.

Üretimde Dikkat

Dump'lar sır taşır. Sızıntı dump'ında rastgele bir Customer'ı açtım:

> dumpobj 0000fffeea0d8bd8
String:      customer14842@example.com

Heap dump, bellekteki her şeyi içerir: müşteri e-postaları, connection string'ler, token'lar, request body'leri. Yani PII (Personally Identifiable Information, kişisel veri) taşıyan bir dosyayı paylaşıyorsun. Kurallarım şunlar:

  • Mümkünse Triage tipini seç: doküman onu "Mini gibi, ama yollar ve parolalar gibi kişisel bilgiler çıkarılmış" diye tanımlıyor. Bedeli şu: heap yok, sızıntı analizi yapılamaz.
  • Heap ya da Full dump'ı yalnızca erişimi kısıtlı bir yerde tut, kimin indirdiğini kayıt altına al, işi bitince sil; ticket'a ek olarak yükleme.
  • Crash dump volume'u dolarsa bir sonraki crash'in kanıtı yazılamaz; saklama süresi ve kota belirle.

Bellek limiti de ayrı bir risk. Doküman açıkça uyarıyor: Full ya da Heap dump almak, process'in sanal belleğinin büyük kısmını sayfalara geri çağırabilir ve memory limit'li bir container'da bu, container'ın OOM (Out of Memory) ile öldürülmesine yol açabilir. Limitin yüzde 90'ında gezen bir pod'dan dump alırken dump'ın kendisi kesintiye sebep olabilir. Bunu ölçüp limiti önceden yükselt ya da trafiği o pod'dan çek.

Bir de tip seçimi: crash ortam değişkenlerinde varsayılan Heap (2), ama dotnet-dump collect için varsayılan Full. Komutu ezbere --type vermeden çalıştırırsan beklediğinden büyük bir dosya ve daha uzun bir donma alırsın.

Sık Düşülen Tuzaklar

Dump'ı yanlış işletim sisteminde açmaya çalışmak. Linux dump'ı Linux'ta analiz edilir; macOS'taki araç bunu açıkça reddediyor. Analizi uygulamanın image'ından türetilmiş bir container'da yapmak en az sürprizli yol.

gcroot "0 root" dediğinde object'i çöp sanmak. Server GC ile çalışan ilk denememde (eeversion: "Server mode with 1 gc heaps", DATAS (Dynamic Adaptation To Application Sizes) açık) gcroot, listede açıkça duran bir Customer için "Found 0 unique roots" verdi. Aynı uygulamayı Workstation GC ile çalıştırınca (--cpus 1 ile runtime kendisi seçti) zincir yukarıdaki gibi çıktı. Bunu dotnet-dump 10.0.750501 ve runtime 10.0.14 ile, arm64'te gördüm; başka bir ortamda tekrar etmeyebilir. Ders şu: araç "kök yok" diyor ama sayılar istek sayısıyla birebir büyüyorsa, araca değil sayılara inan ve başka bir yoldan (Workstation GC ile repro, ya da dumpobj ile elle zincir) doğrula.

Heap dump'ta her komutun çalışacağını varsaymak. Heap tipi, dokümana göre map edilmiş image'lar hariç belleği içerir. Benim denememde Heap dump'ta dumpobj field listesini basmadan çöktü (çıkış kodu 139), aynı andaki Full dump'ta sorunsuz çalıştı. dumpheap, gcroot, syncblk, clrstack Heap dump'ta yeterliydi.

Tek dump ile sızıntı teşhisi koymak. Tek dump'ta 20.000 Customer görmek, bir cache'in doğal hâli de olabilir. Büyümeyi kanıtlayan iki dump arasındaki farktır; arada yük ve zaman olmalı.

%e ile dosya adlarının ayrışacağını sanmak ve PID 1 sorununu bilmemek. İkisini de yukarıda gördük: dump adı "dotnet" olur, --init olmadan çöken process'in container'ı ayakta kalabilir.

Küçük Bir Deneme

★☆☆ DOTNET_DbgMiniDumpType=1 (Mini) ile aynı crash'i tekrarla. Dosya boyutunu Heap dump'la karşılaştır, pe -lines hâlâ çalışıyor mu, dumpheap -stat ne diyor, bak.

★★☆ RecentCustomers'ı son 100 kaydı tutacak şekilde düzelt. 2.000 ve 20.000 istek sonrası iki dump al ve dumpheap -stat -type Bookstore.Api çıktısında Customer sayısının sabit kaldığını göster.

★★★ Sızıntılı uygulamayı --memory 128m ile başlat, belleği limite yaklaştır ve dotnet-dump collect --type Full çalıştır. Container OOM ile öldürüldü mü (docker inspect içindeki OOMKilled)? Aynı denemeyi --type Heap ile yap ve farkı not et.

Dump bir anın fotoğrafıydı. Ama "uygulama cevap vermiyor" şikâyetinde bazen tek fotoğraf yetmez; thread'lerin o anda nerede beklediğini hızlıca görmek ve birkaç saniyelik bir kayıt almak gerekir. Sonraki yazıda dotnet-stack ve dotnet-trace ile hang ve yavaşlık teşhisine, özellikle de thread pool starvation'a geçiyoruz.