Bellek Sızıntısı Tespiti: Windows Performans Monitörü Kullanımı

Bir gün patronunuz sizi aceleyle çağırıyor: “Sunucu yavaşlıyor, müşteriler şikayet ediyor, ne yapacaksın?” Siz de sunucuya bağlanıyorsunuz, Task Manager’ı açıyorsunuz ve görüyorsunuz ki RAM kullanımı %95’te. Ama hangi uygulama bu belleği yiyor? İşte bu noktada Windows Performance Monitor devreye giriyor ve size gerçek anlamda hayat kurtarıcı oluyor.

Bellek sızıntısı (memory leak), bir uygulamanın kullandığı belleği işi bittikten sonra sisteme iade etmemesi durumudur. Küçük sızıntılar başlangıçta fark edilmez, ama saatler veya günler içinde sunucunuzu felç edebilir. Windows Server ortamında bu sorunu tespit etmek için en güçlü yerleşik araç Performance Monitor’dür (PerfMon). Bu yazıda gerçek dünya senaryoları üzerinden PerfMon’u kullanarak bellek sızıntısını nasıl tespit edeceğinizi adım adım anlatacağım.

Performance Monitor Nedir ve Neden Kullanmalısınız?

PerfMon, Windows Server’ın içine gömülü, ücretsiz ve son derece güçlü bir izleme aracıdır. Üçüncü parti araçlara para harcamadan önce PerfMon ile neler yapabileceğinize bir bakın:

  • Gerçek zamanlı performans sayaçlarını izleyebilirsiniz
  • Veri toplama setleri (Data Collector Sets) oluşturarak saatler, günler boyunca veri kaydedebilirsiniz
  • Topladığınız verileri analiz edip raporlara dönüştürebilirsiniz
  • Belirli eşik değerleri aşıldığında uyarı tetikleyebilirsiniz
  • Birden fazla sunucuyu merkezi olarak izleyebilirsiniz

Bellek sızıntısı söz konusu olduğunda PerfMon’un değeri daha da artıyor, çünkü anlık snapshot yerine zaman içindeki değişimi görebiliyorsunuz. Bir uygulamanın belleği yavaş yavaş tükettiğini ancak trend analizi yaparak anlayabilirsiniz.

Temel Bellek Sayaçlarını Tanıyalım

PerfMon’da yüzlerce sayaç var ama bellek sızıntısı tespiti için odaklanmanız gereken spesifik sayaçlar var. Bunları iyi tanımadan doğru yorum yapmanız mümkün değil.

MemoryAvailable MBytes: Sistemde kullanılabilir fiziksel bellek miktarı. Bu değerin zamanla sürekli düşmesi ciddi bir uyarı işareti. Normal koşullarda dalgalanma olur ama tek yönlü düşüş tehlikeli.

MemoryCommitted Bytes: Sanal bellek için ayrılan toplam alan. Fiziksel RAM’in üzerine çıkabilir çünkü sayfalama dosyasını da kapsıyor. Bu değerin devamlı artması sızıntıya işaret eder.

MemoryPool Nonpaged Bytes: Kernel modu sürücülerinin kullandığı ve sayfalanamayan bellek havuzu. Sürücü kaynaklı sızıntılarda bu sayaç artıyor. Çok kritik bir sayaç.

MemoryPool Paged Bytes: Sayfalanabilir kernel belleği. Bu da artış trendi gösteriyorsa sorun var demektir.

ProcessPrivate Bytes: Her process için özel olarak ayrılan bellek. Belirli bir process izliyorsanız bu sayaç altın değerinde.

ProcessWorking Set: Process’in şu an fiziksel RAM’de tuttuğu bellek miktarı. Private Bytes ile birlikte değerlendirin.

ProcessVirtual Bytes: Process’in sanal adres alanı kullanımı. 32-bit uygulamalarda bu değer 2GB’ı geçemez, bu sınıra yaklaştığında uygulama çöküyor.

MemoryPages/sec: Bellek sayfalaması hızı. Yüksek değerler (sürekli 20-25 üzeri) thrashing yaşandığına işaret eder.

PerfMon ile İlk Kurulum: Veri Toplama Seti Oluşturma

Anlık izleme yapmak yerine veri toplama seti kurmanızı şiddetle tavsiye ederim. Sızıntılar çoğunlukla saatler içinde ortaya çıktığı için anlık bakış yeterli olmaz.

PerfMon’u açmak için:

# Run diyaloğundan veya PowerShell'den
perfmon.exe

# Ya da direkt veri toplama setleri konsolunu aç
perfmon /sys

Veri toplama seti oluşturmak için PowerShell ile de yapabilirsiniz, bu daha tekrar edilebilir ve otomatize edilebilir:

# Yeni bir Data Collector Set oluştur
$CollectorName = "MemoryLeakDetection"
$OutputPath = "C:PerfLogsMemoryLeak"

# Klasörü oluştur
New-Item -ItemType Directory -Path $OutputPath -Force

# logman ile veri toplama seti oluştur
logman create counter $CollectorName `
    -cf "C:PerfLogsmemory_counters.txt" `
    -f bincirc `
    -max 512 `
    -si 00:00:30 `
    -o "$OutputPathMemoryLeak.blg"

Sayaç dosyasını oluşturun:

# memory_counters.txt içeriği
$counters = @"
MemoryAvailable MBytes
MemoryCommitted Bytes
MemoryPool Nonpaged Bytes
MemoryPool Paged Bytes
MemoryPages/sec
MemoryPage Faults/sec
Process(*)Private Bytes
Process(*)Working Set
Process(*)Virtual Bytes
Process(*)Page Faults/sec
"@

$counters | Out-File -FilePath "C:PerfLogsmemory_counters.txt" -Encoding ASCII

Veri toplamayı başlatın ve durdurun:

# Başlat
logman start $CollectorName

# Durdur (yeterli veri toplandıktan sonra)
logman stop $CollectorName

# Durumu kontrol et
logman query $CollectorName

Gerçek Dünya Senaryosu 1: IIS Uygulama Havuzu Sızıntısı

Müşterilerimden birinin web sunucusunda her gece yeniden başlatma yapıldığını öğrendim. “Neden?” diye sorduğumda “sunucu gece yarısı yavaşlıyor” dediler. Klasik bellek sızıntısı belirtisi.

Bu tip senaryolarda IIS uygulama havuzları ana şüpheli. Şöyle bir PowerShell scripti ile IIS worker process’lerini izleyebilirsiniz:

# IIS Worker Process'lerinin bellek kullanımını izle
function Get-IISWorkerMemory {
    $w3wpProcesses = Get-Process -Name "w3wp" -ErrorAction SilentlyContinue
    
    if ($w3wpProcesses) {
        foreach ($proc in $w3wpProcesses) {
            $appPool = (Get-WmiObject -Class Win32_Process -Filter "ProcessId=$($proc.Id)").CommandLine
            
            [PSCustomObject]@{
                ProcessId       = $proc.Id
                WorkingSetMB    = [Math]::Round($proc.WorkingSet64 / 1MB, 2)
                PrivateBytesMB  = [Math]::Round($proc.PrivateMemorySize64 / 1MB, 2)
                VirtualMB       = [Math]::Round($proc.VirtualMemorySize64 / 1MB, 2)
                CommandLine     = $appPool
                StartTime       = $proc.StartTime
                RunningHours    = [Math]::Round((New-TimeSpan -Start $proc.StartTime -End (Get-Date)).TotalHours, 1)
            }
        }
    } else {
        Write-Host "Çalışan IIS Worker Process bulunamadı." -ForegroundColor Yellow
    }
}

# Her 5 dakikada bir çalıştır ve logla
$logFile = "C:PerfLogsIIS_Memory_$(Get-Date -Format 'yyyyMMdd').csv"

while ($true) {
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $data = Get-IISWorkerMemory
    
    if ($data) {
        $data | Add-Member -NotePropertyName "Timestamp" -NotePropertyValue $timestamp
        $data | Export-Csv -Path $logFile -Append -NoTypeInformation
        Write-Host "[$timestamp] Veri kaydedildi." -ForegroundColor Green
    }
    
    Start-Sleep -Seconds 300
}

Bu scripti çalıştırıp birkaç saat bekledikten sonra CSV dosyasını açtığımızda, belirli bir uygulama havuzunun PrivateBytesMB değerinin saatler içinde sürekli arttığını gördüm. 300MB’dan başlayan değer 6 saat içinde 1.8GB’a ulaşmıştı. Sorun belliydi: kod tarafında bir bellek sızıntısı vardı.

Gerçek Dünya Senaryosu 2: Pool Nonpaged Bytes Sorunu

Bir başka vakada sunucu yeniden başlatılmadan önce “out of memory” hatası veriyordu ama Task Manager’da hiçbir uygulama anormal görünmüyordu. Bu durumda kernel tarafında sorun var demektir.

PerfMon’da MemoryPool Nonpaged Bytes sayacına bakın. Bu değer sürekli artıyorsa, büyük ihtimalle bir sürücü (driver) bellek sızıntısı yaşıyorsunuzdur.

Hangi sürücünün sorun çıkardığını tespit etmek için PoolMon kullanabilirsiniz:

# PoolMon'u Windows SDK ile kurun, sonra çalıştırın
# Alternatif olarak WinDbg kullanabilirsiniz

# Kernel debug için kısıtlı ama temel bilgi toplamak için:
# Hangi tag'ların en fazla bellek kullandığını görmek

# Performans sayaçlarından pool bilgisi almak
$counters = @(
    "MemoryPool Nonpaged Bytes",
    "MemoryPool Paged Bytes",
    "MemoryPool Nonpaged Allocs",
    "MemoryPool Paged Allocs"
)

$result = Get-Counter -Counter $counters -SampleInterval 60 -MaxSamples 60

$result.CounterSamples | ForEach-Object {
    [PSCustomObject]@{
        Counter   = $_.Path
        ValueMB   = [Math]::Round($_.CookedValue / 1MB, 2)
        Timestamp = $_.Timestamp
    }
} | Format-Table -AutoSize

Pool Nonpaged Bytes değeri 100MB’ı geçtiyse ve artmaya devam ediyorsa, sürücü bazlı analiz yapmanız gerekiyor. Bu noktada WinDbg ile daha derine inmek gerekiyor ama bu ayrı bir yazı konusu.

PerfMon Uyarı Eşikleri Ayarlama

Sürekli ekran başında bekleyemezsiniz. PerfMon’da eşik değerleri aşıldığında otomatik uyarı almanızı sağlayan alerts özelliği var.

# PowerShell ile performance alert oluştur
# Kullanılabilir bellek 500MB altına düşünce uyar

logman create alert "LowMemoryAlert" `
    -th "MemoryAvailable MBytes<500" `
    -ex "C:Scriptsmemory_alert.bat" `
    -si 00:01:00

# Alert başlat
logman start "LowMemoryAlert"

Uyarı geldiğinde çalışacak script:

# C:Scriptsmemory_alert.ps1
# Bu script uyarı geldiğinde çalışır ve anlık snapshot alır

$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$reportPath = "C:PerfLogsAlertsAlert_$timestamp.txt"

New-Item -ItemType Directory -Path "C:PerfLogsAlerts" -Force | Out-Null

$report = @"
=== BELLEK UYARISI - $timestamp ===

Sistem Bellek Durumu:
"@

# Bellek bilgilerini al
$os = Get-WmiObject -Class Win32_OperatingSystem
$report += "`nToplam RAM: $([Math]::Round($os.TotalVisibleMemorySize / 1KB, 0)) MB"
$report += "`nKullanılabilir: $([Math]::Round($os.FreePhysicalMemory / 1KB, 0)) MB"
$report += "`nKullanım Oranı: $([Math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 1))%"

# En fazla bellek kullanan top 10 process
$report += "`n`n=== TOP 10 BELLEK TÜKETİCİSİ ===`n"
$topProcesses = Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 10

foreach ($proc in $topProcesses) {
    $report += "$($proc.Name) (PID: $($proc.Id)) - WS: $([Math]::Round($proc.WorkingSet64/1MB,1))MB - Private: $([Math]::Round($proc.PrivateMemorySize64/1MB,1))MB`n"
}

$report | Out-File -FilePath $reportPath -Encoding UTF8

# E-posta gönder (SMTP ayarlarınıza göre düzenleyin)
# Send-MailMessage -To "[email protected]" -Subject "UYARI: Sunucu Bellek Kritik Seviyede" -Body $report -SmtpServer "mail.sirket.com"

Write-Host "Rapor oluşturuldu: $reportPath"

Trend Analizi: Sızıntıyı Kanıtlamak

Veri topladınız, şimdi bu veriden anlamlı sonuç çıkarmanız lazım. Tek başına yüksek bellek kullanımı sızıntı değildir. Trend analizi yapmalısınız.

# PerfMon .blg dosyasından veri okuma ve analiz
# Önce relog ile CSV'e dönüştür

relog "C:PerfLogsMemoryLeakMemoryLeak.blg" -f CSV -o "C:PerfLogsMemoryLeakoutput.csv"

# Sonra PowerShell ile analiz et
$data = Import-Csv "C:PerfLogsMemoryLeakoutput.csv"

# Private Bytes sütununu bul ve trend hesapla
$processColumn = $data.PSObject.Properties.Name | Where-Object { $_ -like "*private bytes*" -and $_ -like "*w3wp*" }

if ($processColumn) {
    $values = $data | Where-Object { $_.$processColumn -ne "" } | ForEach-Object {
        [PSCustomObject]@{
            Time         = [DateTime]$_."(PDH-CSV 4.0)"
            PrivateMB    = [Math]::Round([double]$_.$processColumn / 1MB, 2)
        }
    }
    
    # İlk ve son değerleri karşılaştır
    $first = $values | Select-Object -First 1
    $last  = $values | Select-Object -Last 1
    
    $diff = $last.PrivateMB - $first.PrivateMB
    $hours = ($last.Time - $first.Time).TotalHours
    $ratePerHour = [Math]::Round($diff / $hours, 2)
    
    Write-Host "Analiz Sonucu:" -ForegroundColor Cyan
    Write-Host "Başlangıç: $($first.PrivateMB) MB ($($first.Time))"
    Write-Host "Bitiş: $($last.PrivateMB) MB ($($last.Time))"
    Write-Host "Süre: $([Math]::Round($hours, 1)) saat"
    Write-Host "Toplam artış: $diff MB"
    Write-Host "Saatlik artış hızı: $ratePerHour MB/saat" -ForegroundColor $(if ($ratePerHour -gt 10) {"Red"} else {"Green"})
    
    if ($ratePerHour -gt 10) {
        Write-Host "UYARI: Bellek sızıntısı şüphesi! Saatte $ratePerHour MB artış." -ForegroundColor Red
    }
}

Process’e Özgü Derin Analiz

Şüpheli process’i tespit ettikten sonra daha detaylı inceleme yapmanız gerekiyor. Handle sayısı ve thread sayısı da bellek sızıntısının yanında artıyorsa, sorun kesinleşiyor demektir:

# Belirli bir process'i derinlemesine izle
param(
    [Parameter(Mandatory=$true)]
    [string]$ProcessName,
    
    [int]$IntervalSeconds = 60,
    [int]$DurationMinutes = 60
)

$endTime = (Get-Date).AddMinutes($DurationMinutes)
$results = @()

Write-Host "Process izleniyor: $ProcessName" -ForegroundColor Cyan
Write-Host "Süre: $DurationMinutes dakika, Interval: $IntervalSeconds saniye" -ForegroundColor Cyan

while ((Get-Date) -lt $endTime) {
    $processes = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
    
    foreach ($proc in $processes) {
        $sample = [PSCustomObject]@{
            Timestamp       = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            PID             = $proc.Id
            WorkingSetMB    = [Math]::Round($proc.WorkingSet64 / 1MB, 2)
            PrivateBytesMB  = [Math]::Round($proc.PrivateMemorySize64 / 1MB, 2)
            VirtualMB       = [Math]::Round($proc.VirtualMemorySize64 / 1MB, 2)
            HandleCount     = $proc.HandleCount
            ThreadCount     = $proc.Threads.Count
            GDIObjects      = $proc.GdiObjects
            UserObjects     = $proc.UserObjects
        }
        
        $results += $sample
        
        Write-Host "$($sample.Timestamp) | PID: $($sample.PID) | Private: $($sample.PrivateBytesMB)MB | Handles: $($sample.HandleCount) | Threads: $($sample.ThreadCount)"
    }
    
    Start-Sleep -Seconds $IntervalSeconds
}

# Sonuçları dışa aktar
$outputFile = "C:PerfLogsProcessAnalysis_${ProcessName}_$(Get-Date -Format 'yyyyMMdd_HHmm').csv"
$results | Export-Csv -Path $outputFile -NoTypeInformation
Write-Host "`nSonuçlar kaydedildi: $outputFile" -ForegroundColor Green

# Özet istatistik
$privFirst = ($results | Select-Object -First 1).PrivateBytesMB
$privLast  = ($results | Select-Object -Last 1).PrivateBytesMB
$handleFirst = ($results | Select-Object -First 1).HandleCount
$handleLast  = ($results | Select-Object -Last 1).HandleCount

Write-Host "`n=== ÖZET ===" -ForegroundColor Yellow
Write-Host "Private Bytes: $privFirst MB --> $privLast MB (Fark: $([Math]::Round($privLast - $privFirst, 2)) MB)"
Write-Host "Handle Count: $handleFirst --> $handleLast (Fark: $($handleLast - $handleFirst))"

Geçici Çözümler ve Kalıcı Aksiyonlar

Sızıntıyı tespit ettikten sonra kısa vadeli ve uzun vadeli adımlar atmak gerekiyor.

Kısa vadeli (acil):

  • IIS uygulama havuzu için bellek limiti ayarlayın. App Pool ayarlarında “Maximum Memory (in KB)” değerini doldurun, belirli bir eşiği geçince havuz otomatik yeniden başlar.
  • Scheduled task ile problematik servisi belirli aralıklarla yeniden başlatın, köklü çözüme kadar zaman kazanırsınız.
  • Windows Task Scheduler üzerinden gece maintenance penceresi oluşturun.

Uzun vadeli (kalıcı):

  • Uygulamanın kaynak koduna erişiminiz varsa, geliştiricilere PerfMon verilerini gösterin. Somut veri olmadan “bellek sızıntısı var” demeniz yeterli ikna edici olmayabilir.
  • .NET uygulamaları için CLR sayaçlarını da inceleyin: “.NET CLR Memory# Bytes in all Heaps” ve “.NET CLR MemoryLarge Object Heap size” sayaçları .NET tarafındaki bellek yönetimini gösterir.
  • Production ortamında Application Pool recycling’i akıllıca yapılandırın, memory limit’i devreye alın.

PerfMon Verilerini Remote Sunuculardan Toplama

Birden fazla sunucunuz varsa merkezi izleme yapabilirsiniz:

# Uzak sunuculardan performans verisi topla
$servers = @("WebServer01", "WebServer02", "AppServer01")
$counters = @(
    "MemoryAvailable MBytes",
    "MemoryPool Nonpaged Bytes",
    "MemoryCommitted Bytes"
)

$allResults = @()

foreach ($server in $servers) {
    try {
        Write-Host "Bağlanıyor: $server" -ForegroundColor Cyan
        
        $remoteCounters = $counters | ForEach-Object { "\$server$_" }
        $samples = Get-Counter -Counter $remoteCounters -SampleInterval 5 -MaxSamples 3 -ErrorAction Stop
        
        foreach ($sample in $samples.CounterSamples) {
            $allResults += [PSCustomObject]@{
                Server    = $server
                Counter   = $sample.Path -replace "\\$server", ""
                ValueMB   = [Math]::Round($sample.CookedValue / 1MB, 2)
                Timestamp = $sample.Timestamp
            }
        }
        
        Write-Host "$server: Veri alındı." -ForegroundColor Green
    }
    catch {
        Write-Host "$server: Bağlantı hatası - $($_.Exception.Message)" -ForegroundColor Red
    }
}

# Rapor oluştur
$allResults | Group-Object Server | ForEach-Object {
    Write-Host "`n=== $($_.Name) ===" -ForegroundColor Yellow
    $_.Group | Where-Object { $_.Counter -like "*Available*" } | 
        Select-Object -Last 1 | 
        ForEach-Object { Write-Host "Kullanılabilir RAM: $($_.ValueMB) MB" }
}

Sonuç

Bellek sızıntısı tespiti sabır isteyen bir iş. Tek bir anlık bakışla sorunu bulmanız çoğunlukla mümkün değil. PerfMon’un gücü tam da burada ortaya çıkıyor: zaman içindeki trendi yakalamanızı sağlıyor.

Bu yazıda öğrendiklerinizi özetleyecek olursam:

  • Available MBytes, Pool Nonpaged Bytes ve Private Bytes sayaçları bellek sızıntısı tespitinin temel yapı taşları.
  • Anlık izleme yerine Data Collector Set kurarak saatler boyu veri toplamalısınız. Sızıntılar sinsi sinsi büyür.
  • IIS ortamında w3wp process’lerini Private Bytes ve Handle Count birlikte takip edin. Sadece bellek değil, handle sayısındaki artış da sızıntının belirtisi olabilir.
  • Pool Nonpaged Bytes sürekli artıyorsa sorunu uygulama değil sürücü tarafında arayın.
  • Topladığınız verileri trend analizi ile yorumlayın, mutlak değer değil değişim hızı önemli.
  • Geliştiricilere somut veri götürün. “Sunucu yavaş” yerine “IIS uygulama havuzu saatte 45MB büyüyor, 6 saatte 1.8GB’a ulaşıyor” demek çok daha ikna edici.

PerfMon öğrenmesi biraz zaman alan ama bir kez özümsediğinizde vazgeçemeyeceğiniz bir araç. Üçüncü parti izleme çözümlerine para harcamadan önce bu yerleşik aracın ne kadar güçlü olduğuna şaşıracaksınız.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir