Mert Tosun
← Yazılar
Go'da Memory ve CPU Profiling: Uygulamanın Nefesini Dinlemek

Go'da Memory ve CPU Profiling: Uygulamanın Nefesini Dinlemek

Mert TosunGo

Kod çalışıyor. Testler geçiyor. Deploy başarılı.

Ama production'da bir şeyler yavaş. Memory artmaya devam ediyor. CPU aniden spike yapıyor. Restart atıyorsunuz, birkaç saat iyi gidiyor, sonra yine aynı şey.

İşte tam bu noktada profiling devreye giriyor.

Go'nun standart kütüphanesi pprof adında güçlü bir araç içeriyor. Uygulamanızın neyi ne kadar tükettiğini, hangi fonksiyonun ne kadar CPU aldığını, bellekte neyin biriktiğini flame graph'larla gösteriyor. Debugger gibi kodu durdurmaz; çalışan sistemi dinler.

pprof Nedir?

pprof, Go runtime'ının topladığı profil verilerini görselleştiren bir araçtır. İki parçadan oluşur:

  1. runtime/pprof — profil verisini toplayan paket
  2. go tool pprof — toplanan veriyi analiz eden CLI

Üç temel profil türü vardır:

Profil Ne ölçer?
cpu Hangi fonksiyonlar CPU'yu ne kadar kullanıyor
heap Bellekte ne kadar nesne yaşıyor, ne kadar tahsis ediliyor
goroutine Şu an kaç goroutine var, nerede bloklandılar

1. HTTP Endpoint ile Canlı Profiling (En Kolay Yol)

Servisiniz HTTP sunuyorsa, tek satırla profiling endpoint'lerini açabilirsiniz:

package main

import (
    "net/http"
    _ "net/http/pprof" // Bu import yeterli — otomatik kaydolur
    "log"
)

func main() {
    // Profiling endpoint'leri /debug/pprof/ altında açılır
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // Asıl uygulamanız
    http.ListenAndServe(":8080", yourRouter)
}

Artık şu adresler aktif:

http://localhost:6060/debug/pprof/          → özet
http://localhost:6060/debug/pprof/heap      → bellek
http://localhost:6060/debug/pprof/goroutine → goroutine'ler
http://localhost:6060/debug/pprof/profile   → 30 sn CPU profili

Güvenlik notu: Bu endpoint'leri sadece internal ağda veya localhost'ta açın. Production'da dış trafiğe asla expose etmeyin.


2. CPU Profiling: Nerede Zaman Harcıyorsunuz?

Programatik yöntem (test/benchmark için ideal):

package main

import (
    "os"
    "runtime/pprof"
    "log"
)

func main() {
    // CPU profil dosyasını oluştur
    f, err := os.Create("cpu.prof")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    // Profiling başlat
    if err := pprof.StartCPUProfile(f); err != nil {
        log.Fatal(err)
    }
    defer pprof.StopCPUProfile()

    // Analiz etmek istediğiniz iş burada
    yavasIslem()
}

func yavasIslem() {
    toplam := 0
    for i := 0; i < 1_000_000_000; i++ {
        toplam += i
    }
    _ = toplam
}

Çalıştırın, cpu.prof dosyası oluşacak. Sonra analiz edin:

go tool pprof cpu.prof

pprof CLI'da en kullanışlı komutlar:

(pprof) top10          → En çok CPU tüketen 10 fonksiyon
(pprof) list yavasIslem → Satır satır CPU tüketimi
(pprof) web            → Flame graph (tarayıcıda açılır)

HTTP endpoint'ten gerçek zamanlı:

# 30 saniye CPU profili al
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

3. Memory Profiling: Bellek Nerede Birikiyoru?

package main

import (
    "os"
    "runtime"
    "runtime/pprof"
    "log"
)

func main() {
    bellekSizdir()

    // GC'yi zorla çalıştır — anlık durumu daha net görmek için
    runtime.GC()

    // Heap profilini kaydet
    f, err := os.Create("mem.prof")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    if err := pprof.WriteHeapProfile(f); err != nil {
        log.Fatal(err)
    }
}

func bellekSizdir() {
    // Büyük slice'lar oluşturup tutuyoruz
    tutuldu := make([][]byte, 0)
    for i := 0; i < 1000; i++ {
        blok := make([]byte, 1024*1024) // 1 MB
        tutuldu = append(tutuldu, blok)
    }
    _ = tutuldu
}

Analiz:

go tool pprof -alloc_space mem.prof   # Toplam tahsis edilen bellek
go tool pprof -inuse_space mem.prof   # Şu an kullanımda olan bellek

-alloc_space vs -inuse_space farkı kritik:

  • alloc_space: Programın ömrü boyunca toplam ne kadar tahsis edildi (GC temizlediklerini de sayar)
  • inuse_space: Şu an bellekte duran nesneler

Memory leak arıyorsanız inuse_space kullanın.


4. Flame Graph ile Görsel Analiz

Komut satırı yeterince sezgisel değilse, flame graph çok daha hızlı anlık kavrama sağlar.

# Graphviz kurulu olması gerekiyor
go tool pprof -http=:8081 cpu.prof

Tarayıcıda localhost:8081 açılır. "Flame Graph" sekmesine gidin:

  • Geniş bloklar = çok CPU tüketen fonksiyonlar
  • Derin yığınlar = derin call stack'ler
  • Bir bloğa tıklamak = o fonksiyona zoom yapar

5. Benchmark ile Profiling (En Hassas Yöntem)

Production benzeri yük altında ölçüm yapmak için:

// profil_test.go
package main

import "testing"

func BenchmarkYavasIslem(b *testing.B) {
    for i := 0; i < b.N; i++ {
        yavasIslem()
    }
}
# CPU profili ile benchmark çalıştır
go test -bench=BenchmarkYavasIslem -cpuprofile=cpu.prof -memprofile=mem.prof

# Sonuçları analiz et
go tool pprof cpu.prof
go tool pprof mem.prof

6. Goroutine Sızıntısı Tespiti

Uygulama yavaş yavaş bellek yiyorsa ve goroutine sayısı artmaya devam ediyorsa, büyük ihtimalle goroutine leak var.

# Tüm aktif goroutine'leri dump et
curl http://localhost:6060/debug/pprof/goroutine?debug=2

Çıktıda aynı stack trace'in onlarca kez tekrarlandığını görürseniz, o fonksiyon goroutine sızdırıyordur.

Programatik kontrol:

import (
    "runtime"
    "fmt"
)

func goroutineSayisiniYazdir() {
    fmt.Printf("Aktif goroutine: %d\n", runtime.NumGoroutine())
}

Test sırasında başlangıç ve bitiş sayısını karşılaştırın:

func TestGoroutineLeak(t *testing.T) {
    onceki := runtime.NumGoroutine()

    // Test edilen kod
    islemYap()

    runtime.GC()
    sonraki := runtime.NumGoroutine()

    if sonraki > onceki {
        t.Errorf("Goroutine sızıntısı: %d → %d", onceki, sonraki)
    }
}

Gerçek Dünya: Tipik Bulgular

Profiling yaptığımda sık karşılaştığım sorunlar:

JSON marshal/unmarshal domination
Büyük struct'ları sürekli serialize etmek CPU'da görünür. Çözüm: sync.Pool ile encoder/decoder nesnelerini yeniden kullan, veya sık değişmeyen yanıtları cache'le.

fmt.Sprintf'in maliyeti
Log satırlarında fmt.Sprintf("user: %d", id) gibi çağrılar beklenmedik CPU tüketebilir. Yapılandırılmış bir logger (zap, zerolog) çok daha verimli.

Slice büyümesi
append her büyümede yeni bellek tahsis eder. Boyutu bilinen slice'larda make([]T, 0, kapasite) ile başlayın.

String + String birleştirme
Döngü içinde s += "bir şey" her iterasyonda yeni string tahsis eder. strings.Builder kullanın.


Özet

# HTTP endpoint aç (bir kez import et, yeter)
_ "net/http/pprof"

# CPU profili al (30 sn)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Bellek profili al
go tool pprof http://localhost:6060/debug/pprof/heap

# Flame graph aç
go tool pprof -http=:8081 cpu.prof

# Benchmark + profil
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof

Profiling bir defaya mahsus bir şey değil — production'da periyodik olarak, yük testlerinde ve performans regresyonu şüphelendiğinizde çalıştırın. Go'nun bu aracı build'e dahil etmesi, performans sorunlarını "tahmin etmek" yerine "ölçmek" kültürünün bir parçası.

Kodu değil, veriyi okuyun.