C# ile Memory Dump Alma: Thread Pool Tıkanmasını Yakala
Üretim ortamında uygulama yanıt vermemeye başladığında elde genelde bir tek log kayıtları kalır, o kayıtlar da çoğu zaman “o an bellekte ne vardı” sorusuna cevap vermez. Aaron Powell’ın.NET Blog’daki yazısı, uygulamanın kendi kendine memory dump üretmesini sağlayan pratik bir yaklaşımı anlatıyor: thread pool’u düzenli aralıklarla yoklayan bir izleyici thread kurmak, eşiği aşan bir gecikme görüldüğünde işlem belleğini diske yazmak. Yaklaşımın Windows ve Linux tarafındaki uygulanışı, örnek kodlar ve dikkat edilmesi gereken noktalar aşağıda.
Memory dump neden işe yarar?
Masaüstü uygulamasının donup ekranın grileşmesi ya da tarayıcıda tıkladığınız bağlantının sonsuza kadar dönüp durması tanıdık sahnelerdir. Geliştirici uygulamaya loglama ve telemetri eklemiş olsa bile yürütme yollarını takip etmekle uygulamanın o andaki durumunu anlamak ayrı şeylerdir.
Memory dump işte bu boşluğu doldurur, uygulamanın anlık durumunu yakalar. Tam (full) ya da kısmi dump tercihine göre, çöp toplayıcının temizlemesini bekleyen nesneler dahil bellekteki nesnelerin bir görünümünü verir. Kapsam dışına çıkmış ama henüz temizlenmemiş durum bile uygulamanın genel davranışı hakkında ipucu sağlayabilir.
Yazıdaki örnek senaryo, yanıt vermemeye başlayan bir uygulama. Bu tür sorunların yaygın sebeplerinden biri de asenkron kodun ve task’ların nasıl kullanıldığı.
Thread pool’u izleyen bir watcher
Yöntem gayet düz. Belirli aralıklarla thread pool’a kendi Task‘ımızı ekleyip tamamlanma süresini ölçüyoruz. Süre izin verilen eşiği aşıyorsa thread pool büyük olasılıkla doymuştur, dump almaya değer bir an da yakalanmış demektir.
internal class ThreadPoolWatcher(string name = "ThreadPool Watcher", int interval = 3_000)
{
private static readonly object DumpLock = new();
private static int dumpCount;
private readonly Thread thread = new(() => Watcher(interval))
{
Name = name,
IsBackground = true
};
private static void Watcher(int interval)
{
while (true)
{
Thread.Sleep(interval);
Stopwatch stopwatch = Stopwatch.StartNew();
Task task = Task.Run(stopwatch.Stop);
if (!task.Wait(interval))
{
Console.WriteLine($"Task did not complete within {interval} ms");
}
if (stopwatch.ElapsedMilliseconds <= interval) continue;
lock (DumpLock)
{
if (dumpCount++ > 0)
{
Console.WriteLine("Dump already created for this run; skipping additional dumps.");
continue;
}
}
// Took over the interval to complete
Console.WriteLine($"Task took too long: {stopwatch.ElapsedMilliseconds} ms");
string path = Path.Combine(AppContext.BaseDirectory, $"fulldump-{Environment.ProcessId}-{DateTime.Now:yyyyMMdd-HHmmss}.dmp");
if (OperatingSystem.IsWindows())
{
WindowsDumper.WriteCurrentProcess(path);
}
else if (OperatingSystem.IsLinux())
{
LinuxDumper.WriteCurrentProcess(path);
}
}
}
internal void Join() => thread.Join();
internal void Start() => thread.Start();
}
Kodun mantığı şöyle. Hata ayıklama sırasında tanınabilsin diye ada sahip yeni bir Thread oluşturuluyor, bu thread çalıştığında da sürekli olarak Watcher metodunu çağırıyor. Watcher, her kontrol arasında belirtilen süre kadar beklemek için Thread.Sleep kullanıyor.
Thread uyandığında thread pool’a yeni bir task ekliyor ve tamamlanma süresini ölçüyor. Süre eşiği aşarsa bu, thread pool’un doymuş olabileceğine işarettir, daha derin inceleme için dump almak da anlamlı hale gelir; aşmazsa thread tekrar uykuya döner. Task zamanlamasından yararlanan bu basit yöntem, thread pool davranışını gerçek zamanlı gözlemlemenin pratik bir yolu.
Üretimde tekrarlayan dump’lara karşı önlem
Yazıda özellikle vurgulanan bir uyarı var. Üretim kullanımında tekrar tekrar dump üretilmesine karşı koruma koymak gerekiyor; full dump dosyaları büyük olabilir, kimlik bilgileri, token’lar, bağlantı dizeleri gibi hassas verileri içerebilir. Her geciken yoklamada dump almak yerine dosyaları erişimi kısıtlı bir dizinde saklamak, bir bekleme süresi (cooldown) eklemek ya da üretilecek dosya sayısını sınırlamak daha güvenli. Yukarıdaki örnekte DumpLock ve dumpCount alanları, tek çalıştırmada yalnızca bir dump alınmasını sağlıyor.
Windows tarafı: MiniDumpWriteDump
Windows’ta mevcut işlemin dump’ını almak için yerel bir kütüphaneye, dbghelp.dll‘e P/Invoke yapmak ve dump üretimini Windows’a bırakmak gerekiyor.
[SupportedOSPlatform("windows")]
internal static class WindowsDumper
{
[Flags]
private enum DumpType : uint
{
Normal = 0x00000000,
WithDataSegs = 0x00000001,
WithFullMemory = 0x00000002,
WithHandleData = 0x00000004,
WithUnloadedModules = 0x00000020,
WithFullMemoryInfo = 0x00000800,
WithThreadInfo = 0x00001000,
WithTokenInformation = 0x00040000,
}
[DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool MiniDumpWriteDump(
IntPtr hProcess,
uint processId,
SafeHandle hFile,
DumpType dumpType,
IntPtr exceptionParam,
IntPtr userStreamParam,
IntPtr callbackParam);
/// <summary>
/// Writes a full memory dump of the current process.
/// </summary>
public static void WriteCurrentProcess(string path)
{
Write(Process.GetCurrentProcess(), path);
}
/// <summary>
/// Writes a full memory dump of <paramref name="process"/> to <paramref name="path"/>.
/// </summary>
public static void Write(Process process, string path)
{
ArgumentNullException.ThrowIfNull(process);
ArgumentException.ThrowIfNullOrEmpty(path);
string? directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
using FileStream stream = new(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
// Full memory dump: entire address space (including the heap), handles, modules and thread state.
bool success = MiniDumpWriteDump(
process.Handle,
(uint)process.Id,
stream.SafeFileHandle,
DumpType.WithFullMemory |
DumpType.WithFullMemoryInfo |
DumpType.WithDataSegs |
DumpType.WithHandleData |
DumpType.WithUnloadedModules |
DumpType.WithThreadInfo |
DumpType.WithTokenInformation,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero);
if (!success)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"MiniDumpWriteDump failed for process {process.Id}.");
}
}
}
Sınıf yalnızca Windows’ta çalıştığı için SupportedOSPlatform("windows") özniteliğiyle işaretlenmiş. Sonra üretilebilecek dump türlerini tanımlayan bir enum geliyor: tam bellek dump’ı, handle verisi içeren dump, thread bilgisi içeren dump. dbghelp.dll içindeki MiniDumpWriteDump fonksiyonu import ediliyor, sınıf da hem mevcut işlemin hem de belirtilen herhangi bir işlemin dump’ını yazmak için pratik metotlar sunuyor.
Örnekte üretilen dump’a her şey dahil ediliyor, dolayısıyla dosya da hayli büyük oluyor. Yazıdaki örnek çalıştırmada yaklaşık 125 MB boyutunda bir dump oluşmuş. Boyut, işlemin bellek kullanımına ve seçilen dump içeriğine göre değişir.
Linux tarafı: createdump ve ptrace izinleri
Linux’ta eşdeğer bir dump almak biraz daha zahmetli olabiliyor; kullanılan dağıtıma, container içinde çalışıp çalışmadığına ve işlemin sahip olduğu izinlere bağlı. Aşağıdaki örnek,.NET runtime ile birlikte gelen createdump aracını kullanıyor.
[SupportedOSPlatform("linux")]
internal static class LinuxDumper
{
// Yama LSM (see /proc/sys/kernel/yama/ptrace_scope). With the default scope of 1
// ("restricted ptrace"), a process may only be ptraced by its own descendants unless
// it explicitly designates another process (or PR_SET_PTRACER_ANY) as an allowed
// tracer via prctl(PR_SET_PTRACER,...). "Yama" spelled out in ASCII.
private const int PR_SET_PTRACER = 0x59616d61;
private static readonly IntPtr PR_SET_PTRACER_ANY = new(-1);
[DllImport("libc", SetLastError = true)]
private static extern int prctl(int option, IntPtr arg2, IntPtr arg3, IntPtr arg4, IntPtr arg5);
public static void WriteCurrentProcess(string path)
{
AllowAnyProcessToPtraceSelf();
Write(Process.GetCurrentProcess(), path);
}
/// <summary>
/// Best-effort: on distros using the Yama LSM (e.g. Ubuntu/Debian) with the default
/// ptrace_scope of 1 ("restricted ptrace"), a process may only be ptraced by its own
/// descendants - not the parent that spawned it. createdump attaches to us as our
/// child, so we explicitly allow any process to ptrace us. This is a no-op (and
/// harmless) on distros where Yama isn't enabled (e.g. many Fedora/RHEL setups), and
/// is swallowed entirely if "libc" or prctl can't be resolved at all, which can happen
/// on musl-based distros like Alpine that don't ship an unversioned libc.so.
/// </summary>
private static void AllowAnyProcessToPtraceSelf()
{
try
{
_ = prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException)
{
// libc/prctl isn't resolvable this way on this platform (e.g. musl/Alpine) -
// fall through and let createdump itself report any real permission failure.
}
}
public static void Write(Process process, string path)
{
ArgumentNullException.ThrowIfNull(process);
ArgumentException.ThrowIfNullOrEmpty(path);
string? directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
string createDumpPath = FindCreateDump();
using Process createDump = new()
{
StartInfo = new ProcessStartInfo
{
FileName = createDumpPath,
// --full: entire address space (analogous to MiniDumpWithFullMemory).
// -f: explicit output path (createdump would otherwise pick its own name/location).
ArgumentList =
{
"--full",
"-f", path,
process.Id.ToString(),
},
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
},
};
createDump.Start();
string stdout = createDump.StandardOutput.ReadToEnd();
string stderr = createDump.StandardError.ReadToEnd();
createDump.WaitForExit();
if (createDump.ExitCode != 0)
{
string hint = process.Id != Environment.ProcessId
? " Dumping another process typically requires running as root, the " +
"CAP_SYS_PTRACE capability, or /proc/sys/kernel/yama/ptrace_scope set to 0."
: " If this is a container, ensure ptrace isn't blocked by seccomp " +
"(add --cap-add=SYS_PTRACE) or by an SELinux/AppArmor policy.";
throw new InvalidOperationException(
$"createdump failed for process {process.Id} with exit code {createDump.ExitCode}.{hint}{Environment.NewLine}{stdout}{stderr}");
}
}
private static string FindCreateDump()
{
string runtimeDirectory = RuntimeEnvironment.GetRuntimeDirectory();
string candidate = Path.Combine(runtimeDirectory, "createdump");
if (!File.Exists(candidate))
{
throw new FileNotFoundException(
$"Could not find the 'createdump' utility next to the runtime directory '{runtimeDirectory}'.",
candidate);
}
return candidate;
}
}
Bu sınıf iki ek iş yapıyor. Biri, Yama’nın kısıtlı ptrace politikası altında çocuk süreç olarak başlatılan createdump‘ın kendi ebeveynine bağlanabilmesi için libc içindeki prctl çağrısını kullanmak. Diğeri, createdump aracını runtime dizininin yanında bulup tam bellek dump’ını üretmek. Yazıdaki örnekte bu yaklaşım yaklaşık 800 MB’lık bir dump üretmiş; boyut yine işlemin bellek kullanımına ve dump yapılandırmasına bağlı.
Sorunu yapay olarak üretmek
Dump alma altyapısı hazır olduğuna göre sıra, analiz edebileceğimiz bir sorunu bilerek tetiklemekte.
internal static class ApplicationRunner
{
public static void DoLotsOfWork() => Parallel.For(0, 1000, DoSomeWork);
private static void DoSomeWork(int i)
{
Console.WriteLine("Running task {0}", i);
Thread.Sleep(10_000);
}
}
Kod, her biri uzun süren çok sayıda paralel görevi simüle ediyor. Thread pool üzerinde çalışabilecek görev sayısına sınır konmadığı için havuz doyma noktasına gelebiliyor; sonuç, performans sorunları ya da uygulamanın donmuş gibi görünmesi.
Uygulamayı çalıştırmak için ThreadPoolWatcher örneğini oluşturup başlatmak ve iş yükünü çalıştırmak yeterli, adanmış izleyici thread bir sonraki yoklamayı beklemeye devam eder.
var tpw = new ThreadPoolWatcher();
tpw.Start();
ApplicationRunner.DoLotsOfWork();
// The application keeps running until the process exits or the watcher is stopped.
Bir süre sonra uygulama yanıt vermemeye başlar, dump dosyası da üretilir.
Dump dosyasını analiz etmek
Üretilen .dmp dosyaları, yönetilen hata ayıklama (managed debugging) ile Visual Studio’da açılabiliyor. Çağrı yığınlarında gezinebilir, erişilebilir değişkenleri inceleyebilir, dump alındığı andaki uygulama durumunu görebilirsiniz. Aaron Powell, memory dump analizi ve Visual Studio’daki parallel stacks görünümü için Visual Studio blogundaki tamamlayıcı yazıya yönlendiriyor.
Değerlendirme
Uygulamanın, performans sorunuyla ya da yanıt vermeyen bir thread pool’la karşılaştığında kendi memory dump’ını üretmesini sağlamak sanıldığından kolay; yazının özü bu. Böylece loglara ve senaryoyu yeniden üretme çabasına bağlı kalmadan, sorunun yaşandığı andaki uygulama durumunu doğrudan analiz edebilirsiniz. Visual Studio’nun dump analiz araçlarıyla birleşince karmaşık sorunların teşhisi belirgin biçimde kolaylaşıyor.
Dump üretimini geliştirme ve izleme pratiklerine dahil etmek, performans darboğazlarına ve donmalara proaktif yaklaşma imkanı verir. Göz ardı edilmemesi gereken tek şey, dosya boyutu ve içerilen hassas veriler konusundaki uyarı; tam dump’lar büyük olur ve kimlik bilgisi türünden veriler barındırabilir.
Kaynaklar ve İleri Okuma
- Creating a memory dump in C# –.NET Blog (Aaron Powell)
- Today I will debug a production crash – Visual Studio Blog
- .NET Blog
- Visual Studio Parallel Stacks ile Memory Dump Analizi







0 comments