Go'da Memory ve CPU Profiling: Uygulamanın Nefesini Dinlemek
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:
runtime/pprof— profil verisini toplayan paketgo 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.