Go'da Profiling ve Debugging — Derinlemesine, Uygulayıcı Seviyesinde Rehber

21 Ağustos 2026 · netologist · 21 dakika, 4465 kelime ·

Bu doküman, bir Go principal engineer bakış açısıyla hazırlanmıştır. Production ve geliştirme ortamlarındaki Go sistemlerinde CPU, bellek, concurrency ve gecikme (latency) problemlerini teşhis etmek için gereken tüm araç zincirini ve zihinsel modelleri kapsar.


İçindekiler

  1. Felsefe: Tahmin Etme, Ölç
  2. pprof Ekosistemi
  3. CPU Profiling
  4. Bellek (Memory) Profiling
  5. Goroutine Profiling ve Leak Tespiti
  6. Block ve Mutex Profiling (Contention)
  7. Execution Tracer (go tool trace)
  8. testing.B ve benchstat ile Benchmarking
  9. Escape Analysis ve Compiler Diagnostics
  10. Garbage Collector Tuning
  11. runtime/metrics Paketi
  12. Delve ile Debugging
  13. Race Detection
  14. Deadlock ve Goroutine Diagnostics
  15. Production’da Continuous Profiling
  16. Flame Graph ve Görselleştirme
  17. Yaygın Hatalar Kataloğu
  18. Vaka Analizleri (Case Studies)
  19. Kontrol Listesi: Production Olayı İncelemesi
  20. Referans Komutlar Özet Tablosu

1. Felsefe: Tahmin Etme, Ölç

Go, gözlemlenebilirliği (observability) dilin tasarımının bir parçası haline getirmiştir — runtime, pprof, trace ve runtime/metrics araçlarını doğrudan standart kütüphaneyle birlikte sunar. Bu bir tesadüf değildir: Go’nun tasarımcıları (Pike, Griesemer, Thompson ve sonraki concurrency-ağırlıklı runtime ekibi), goroutine’lerin, channel’ların ve GC’nin, geleneksel debugger’ların (gdb tarzı, adım adım ilerleme) iyi çözemeyeceği hata modları yaratacağını öngörmüştü.

Temel prensip: bir performans problemini asla sezgiyle “çözmeye” veya optimize etmeye kalkışma. Go size sampling profiler’lar, execution tracer’lar ve istatistiksel benchmarking araçları sunar; bunun amacı tahminlerin yerine veriyi koymaktır. Kıdemli bir Go mühendisinin refleksi şu olmalı: “X darboğaz olduğunu düşünüyorum” demeden önce “profil ne diyor?” sorusunu sormak.

Go production sorunlarında üç kategori baskındır:

Her birinin kendine özgü bir aracı vardır. Hangi aracın hangi soruyu yanıtladığını bilmek, işin %80’idir.


2. pprof Ekosistemi

pprof, Go’nun profiling formatı ve araç zinciridir; Google’ın dahili gperftools/pprof C++ aracından türetilip Go runtime’ına uyarlanmıştır. İki giriş noktası vardır:

2.1 runtime/pprof

Profil toplamayı doğrudan programın içine gömmek için düşük seviyeli bir API — CLI araçları, batch job’lar veya profili isteğe bağlı olarak diske yazmak istediğiniz kısa ömürlü süreçler için kullanışlıdır.

import (
    "os"
    "runtime/pprof"
)

func main() {
    f, _ := os.Create("cpu.prof")
    defer f.Close()

    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

    doWork()
}

Heap için:

f, _ := os.Create("heap.prof")
defer f.Close()
runtime.GC() // güncel istatistikleri almak için
pprof.WriteHeapProfile(f)

2.2 net/http/pprof

Uzun süre çalışan sunucular için çok daha yaygın kullanılan giriş noktasıdır. Yan etkisi için import edilmesi, /debug/pprof/ altında HTTP handler’ları kaydeder:

import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ... uygulamanın geri kalanı
}

Güvenlik notu: /debug/pprof/‘u asla dış dünyaya açık bir portta expose etmeyin. Bu, dahili kaynak kod yollarını (source path), bellek düzenini sızdırır ve bir DoS vektörü olabilir (CPU profil istekleri varsayılan olarak 30sn+ süreyle bir CPU çekirdeğini meşgul eder). Sadece localhost’a veya dahili mesh ağına bağlayın ya da auth middleware’i ile koruyun.

Mevcut endpoint’ler:

EndpointAçıklama
/debug/pprof/profile?seconds=30CPU profili (bloklayıcı, N saniye boyunca örnekleme yapar)
/debug/pprof/heapHeap allocation anlık görüntüsü
/debug/pprof/goroutineO anki tüm goroutine’lerin stack trace’leri
/debug/pprof/blockSenkronizasyon primitiflerinde bloklanan goroutine’ler
/debug/pprof/mutexMutex contention
/debug/pprof/threadcreateOS thread oluşturma stack’leri
/debug/pprof/allocsProgram başlangıcından beri yapılan tüm allocation’lar (sadece canlılar değil)
/debug/pprof/trace?seconds=5Execution trace (farklı bir araç: go tool trace)

2.3 Veri Çekme ve Analiz

go tool pprof http://localhost:6060/debug/pprof/heap
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

Bu sizi interaktif bir REPL’e sokar:

(pprof) top10
(pprof) list functionName
(pprof) web        # tarayıcıda SVG call graph açar (graphviz gerektirir)
(pprof) png > out.png
(pprof) traces
(pprof) peek regexPattern

Her Go mühendisinin ezberlemesi gereken kritik pprof komutları:


3. CPU Profiling

CPU profilleri istatistiksel örneklerdir, tam bir izleme (trace) değildir. Go runtime, yürütmeyi saniyede yaklaşık 100 kez (runtime.SetCPUProfileRate, varsayılan 100Hz) SIGPROF sinyaliyle keser ve o anki stack’i kaydeder. Bu şu anlama gelir:

3.1 Flat vs. Cumulative

Cumulative’i yüksek, flat’i düşük olan bir fonksiyon bir “yönlendiricidir” (router) — asıl maliyet aşağı akıştadır. Flat’i yüksek olan bir fonksiyon, gerçek CPU döngülerinin yandığı yerdir — algoritmik optimizasyon için önce buraya bakın.

3.2 Pratik İş Akışı

go test -bench=. -cpuprofile=cpu.prof -benchtime=5s
go tool pprof cpu.prof
(pprof) top20 -cum
(pprof) list HotFunction

Şunlara dikkat edin:

3.3 GOMAXPROCS Etkileşimi

CPU profil yüzdeleri, GOMAXPROCS × wall-clock süresine göre görecelidir. Container’ın cgroup limitinin izin verdiğinden daha fazla çekirdeğe sahip bir makinede, Go tarihsel olarak runtime.NumCPU()‘dan GOMAXPROCS‘u fazla tespit etmiştir; bu da kullanılabilir çekirdek sayısından fazla OS thread’i ve artan scheduling overhead’i doğurur. Go 1.5’ten itibaren bunu açıkça ayarlayabilirsiniz; Go 1.21+‘tan itibaren iyileştirilmiş (yine de go.uber.org/automaxprocs tarzı çözümlere veya Go 1.25+‘taki yerleşik cgroup-farkında GOMAXPROCS’a kadar kusursuz olmayan) bir işleme sahiptir. GOMAXPROCS‘un container’ınızın CPU kotasıyla eşleştiğinden her zaman emin olun — Kubernetes dağıtımlarında gecikme sıçramalarının çok yaygın, çok sessiz bir kaynağıdır.


4. Bellek (Memory) Profiling

Go’nun heap profiler’ı da varsayılan olarak örneklemeye dayalıdır (allocate edilen her 512KB için 1 örnek, runtime.MemProfileRate ile kontrol edilir). İki kritik profil “görünümü” vardır:

4.1 inuse_space vs alloc_space

go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap

Benzer şekilde, -inuse_objects ve -alloc_objects size byte yerine nesne sayısını verir — toplam byte küçük görünse bile patolojik küçük nesne churn’ünü bulmak için kullanışlıdır.

4.2 Bir Memory Leak’i Teşhis Etmek

Go garbage-collected olduğu için “leak” hemen hemen her zaman istemsizce tutulan referanslar anlamına gelir, free edilmemiş malloc değil. Yaygın nedenler:

  1. Goroutine leak’leri — bir channel gönderme/alma işleminde sonsuza kadar bloke olmuş bir goroutine, kendi tüm stack’ini ve yakaladığı tüm değişkenleri sonsuza kadar canlı tutar. Bu, production’da Go bellek sızıntılarının #1 nedenidir.
  2. Slice alt-slice’lamanın backing array’i tutmasısmallSlice := bigSlice[:10], smallSlice referans edildiği sürece bigSlice‘ın tüm backing array’ini canlı tutar. Çözüm: gerisini serbest bırakmanız gerekiyorsa, gereken veriyi append([]T(nil), bigSlice[:10]...) ile kopyalayın.
  3. Eviction’ı olmayan global cache/map’ler — hiç temizlenmeyen map[string]*Session; TTL eviction, LRU için container/list + map, veya groupcache/ristretto gibi bir kütüphane kullanın.
  4. Durdurulmamış time.Timer/time.Ticker — runtime’ın timer heap’i tarafından tutulur.
  5. Büyük struct’ları istemsizce referans olarak yakalayan closure’lar, örn. uzun ömürlü callback registry’leri içinde.

4.3 Zaman İçinde Heap Profillerini Karşılaştırma (Diff)

En güvenilir tek leak-avlama tekniği:

curl -o heap1.prof http://localhost:6060/debug/pprof/heap
# ... yük altında 10 dakika bekleyin ...
curl -o heap2.prof http://localhost:6060/debug/pprof/heap
go tool pprof -base heap1.prof heap2.prof
(pprof) top20

Bu, iki anlık görüntü arasında tam olarak neyin büyüdüğünü gösterir, steady-state gürültüsünü filtreler — tek bir anlık görüntüden çok daha kullanışlıdır.

4.4 debug.FreeOSMemory() ve GODEBUG=madvdontneed=1

Go, free edilen belleği her zaman hemen OS’a geri vermez (yüksek RSS’e karşı düşük canlı heap olarak görülür). debug.FreeOSMemory() bir GC + release döngüsünü zorlar — teşhis için kullanışlıdır, production çözümü değildir. Linux’ta GODEBUG=madvdontneed=1 bellek serbest bırakma syscall davranışını değiştirir (eski kernel’ler); modern Go (1.16+) her OS için uygun varsayılanı seçer.


5. Goroutine Profiling ve Leak Tespiti

go tool pprof http://localhost:6060/debug/pprof/goroutine

Veya ham, insan tarafından okunabilir bir döküm için (bir olay sırasında son derece kullanışlı, herhangi bir araç gerektirmez):

curl http://localhost:6060/debug/pprof/goroutine?debug=2

debug=2, goroutine başına gruplanmış tam stack trace’leri verir — bu genelde bir olay sırasında “1.400 goroutine aynı chan receive satırında takılı kaldı” durumunu fark etmenin en hızlı yoludur.

5.1 Çıktıyı Okumak

Goroutine’ler, sayı önekiyle birlikte özdeş stack trace’e göre gruplanır:

goroutine profile: total 1432
1400 @ 0x43a1b2 0x43f...
#   0x... internal/poll.runtime_pollWait+0x...
#   0x... net/http.(*persistConn).readLoop+0x...

Aynı yerde takılı kalmış 1400 goroutine sayısı neredeyse kesin bir leak imzasıdır — normal işleyişte durağan halde bu kadar stack-trace tekrarı nadiren görülür.

5.2 Yaygın Leak Desenleri

5.3 Programatik Goroutine Sayısı İzleme

runtime.NumGoroutine()

Bunu bir Prometheus gauge olarak export etmek ve sürekli yukarı doğru trendlerde alarm kurmak (baseline değiştiği için sadece mutlak değere değil) bir Go servisine eklenebilecek en yüksek değerli, en düşük maliyetli production güvenlik önlemlerinden biridir.


6. Block ve Mutex Profiling (Contention)

Bunlar overhead ekledikleri için varsayılan olarak kapalıdır; açıkça etkinleştirin:

import "runtime"

func init() {
    runtime.SetBlockProfileRate(1)       // her bloklama olayını örnekle
    runtime.SetMutexProfileFraction(1)   // her mutex contention olayını örnekle
}

Production ipucu: SetMutexProfileFraction(1) her olayı örnekler ve çok yüksek contention altında pahalı olabilir; production’da 100 gibi bir fraksiyonla (her 100. olay) başlayın, sadece hedefli bir diagnostik pencerede 1‘e düşürün.

6.1 Contention’ı Düzeltmek


7. Execution Tracer (go tool trace)

pprof “CPU/bellek nerede harcanıyor” sorusunu yanıtlarken, execution tracer “her goroutine, scheduler ve GC boyunca, mikrosaniye zaman çizelgesinde zaman içinde ne oldu” sorusunu yanıtlar. (Throughput/CPU problemlerinin aksine) gecikme (latency) problemlerini teşhis etmek için en iyi tek araçtır.

import (
    "os"
    "runtime/trace"
)

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()

    doWork()
}

Veya HTTP üzerinden: curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out

Sonra:

go tool trace trace.out

Bu, birkaç kritik görünüme sahip, tarayıcı tabanlı bir görselleştirme açar:

7.1 Nelere Dikkat Edilmeli

go tool trace, pprof‘a göre daha dik bir öğrenme eğrisine sahiptir ama yalnızca CPU profiling’in açıklayamadığı tail-latency (p99/p999) araştırmaları için vazgeçilmezdir.


8. testing.B ve benchstat ile Benchmarking

Profiling size nerede zaman harcandığını söyler; benchmarking ise bir değişikliğin gerçekten işe yarayıp yaramadığını, istatistiksel titizlikle söyler.

func BenchmarkParseJSON(b *testing.B) {
    data := loadTestData()
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = Parse(data)
    }
}

Şu şekilde çalıştırın:

go test -bench=BenchmarkParseJSON -benchmem -count=10 -cpuprofile=cpu.prof -memprofile=mem.prof > bench.txt

8.1 benchstat

go install golang.org/x/perf/cmd/benchstat@latest
go test -bench=. -count=10 ./... > old.txt
# değişikliğinizi yapın
go test -bench=. -count=10 ./... > new.txt
benchstat old.txt new.txt

benchstat, iki benchmark çalıştırma seti arasındaki istatistiksel anlamlılığı (parametrik olmayan bir testle) hesaplar ve gözlemlenen bir iyileşmenin gerçek mi yoksa sadece gürültü mü olduğunu söyler — “bu optimizasyon işe yaradı” demeden önceki kritik disiplindir.

8.2 Alt-benchmark’lar ve Tablo-Güdümlü Performans Testleri

func BenchmarkEncode(b *testing.B) {
    sizes := []int{10, 100, 1000, 10000}
    for _, n := range sizes {
        b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
            data := makeData(n)
            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                Encode(data)
            }
        })
    }
}

Bu, kodu okuyarak Big-O varsaymak yerine algoritmik karmaşıklığı ampirik olarak ortaya çıkarır (süre doğrusal mı, karesel mi ölçekleniyor?).


9. Escape Analysis ve Compiler Diagnostics

Bir şeyin neden stack yerine heap’e allocate edildiğini (ve dolayısıyla GC’ye baskı yaptığını) anlamak, allocation’a duyarlı sıcak yollar için elzemdir.

go build -gcflags="-m -m" ./...

Şuna benzer çıktı:

./main.go:12:6: moved to heap: x
./main.go:20:9: &y escapes to heap

İstenmeyen heap escape’lerinin yaygın nedenleri:

Aşırı sıcak yollar için (serialization kütüphaneleri, düşük gecikmeli trading, yüksek-QPS router’lar), mühendisler kasıtlı olarak küçük, kısa ömürlü değerleri stack’te tutmak için kodu yeniden yapılandırır — ama bu her zaman -gcflags="-m" ve benchmark’larla doğrulanmalı, varsayılmamalıdır.


10. Garbage Collector Tuning

Go’nun GC’si, concurrent, üç-renkli (tri-color) mark-and-sweep bir collector’dür. İki ana kontrol vardır:

10.1 GOGC (heap büyüme oranı)

Varsayılan 100: GC, heap son collection’daki canlı heap boyutundan bu yana %100 büyüdüğünde tetiklenir. Düşük değerler (GOGC=50) GC’yi daha sık tetikler, peak belleği azaltır ama daha sık collection’dan kaynaklanan CPU overhead’ini artırır. Yüksek değerler (GOGC=200 veya daha fazla) belleği daha az GC CPU overhead’i karşılığında feda eder — bellek fazlası olan CPU-bound batch job’lar için iyidir.

debug.SetGCPercent(50)

10.2 GOMEMLIMIT (Go 1.19+)

Runtime’ın, kullanım limite yaklaştıkça daha agresif GC tetiklemek için kullandığı yumuşak bellek limiti (byte cinsinden) — konteynerize ortamlarda cgroup bellek limitini aşarak OOM-kill olmaktan kaçınmak için kritiktir. GOGC‘yi körlemesine ayarlamanın modern, önerilen tamamlayıcısı (hatta yerine geçeni) budur.

debug.SetMemoryLimit(500 << 20) // 500 MiB yumuşak limit

Veya environment üzerinden: GOMEMLIMIT=500MiB.

Container’lar için en iyi pratik: GOMEMLIMIT‘i container’ın bellek limitinin yaklaşık %80-90’ına ayarlayın ve GOGC‘yi makul bir varsayılanda bırakın (veya 150-200 gibi biraz gevşetin) — bu, runtime’ın yaygın durumda mevcut belleği verimli kullanmasına izin verirken, bellek baskısı sıçramaları altında OOM kill’lere karşı sert bir güvence de sağlar.

10.3 GC Pacing ve Diagnostics

GODEBUG=gctrace=1 ./myapp

Her GC döngüsü için bir satır yazdırır:

gc 15 @6.001s 2%: 0.02+1.2+0.01 ms clock, 0.1+0.5/1.1/0+0.08 ms cpu, 4->5->3 MB, 6 MB goal, 8 P

Anahtar alanlar: kümülatif GC CPU yüzdesi (sağlıklı servisler için genelde %25’in oldukça altında kalmalı — Go ekibinin belgelenmiş, gayri resmi kuralı), heap-öncesi→heap-sonrası→canlı-heap ve heap hedefi. Sürekli %30’un üzerindeki GC CPU%‘si, ya aşırı allocation oranına (yalnızca GOGC üzerinden değil, allocation kaynağında düzeltin) ya da bir GOGC/GOMEMLIMIT yanlış yapılandırmasına güçlü şekilde işaret eder.


11. runtime/metrics Paketi

Go 1.16’dan beri runtime/metrics, dahili runtime metriklerinin onlarcasını çekmek için (yeni kod için eski, daha az yapılandırılmış runtime.MemStats‘ın yerini alan) kararlı, yapılandırılmış bir API sağlar — Prometheus/OpenTelemetry exporter’larına bağlamak için idealdir.

import "runtime/metrics"

samples := []metrics.Sample{
    {Name: "/gc/heap/allocs:bytes"},
    {Name: "/sched/goroutines:goroutines"},
    {Name: "/gc/pauses:seconds"},
}
metrics.Read(samples)
for _, s := range samples {
    fmt.Println(s.Name, s.Value)
}

Go sürümünüzde mevcut her metrik adını keşfetmek için metrics.All()‘ı çalıştırın — bu set sürümden sürüme önemli ölçüde büyümüştür (scheduler gecikme histogramları, GC duraklama dağılımları, bellek-sınıfı dökümleri).

Bunları ad-hoc araştırma için sadece /debug/pprof/‘a güvenmek yerine mevcut gözlemlenebilirlik yığınınıza bağlamak, profiling verisini sürekli izlenen, alarm kurulabilir bir sinyale dönüştürür — bir leak’i bir olay sırasında bir profilden öğrenmekle, bir hafta önceden bir dashboard’dan trendi yakalamak arasındaki farktır.


12. Delve ile Debugging

Delve (dlv), Go’nun fiili standart debugger’ıdır — Go’nun runtime dahili yapılarını (goroutine’ler, channel’lar, defer/panic/recover) çok az anlayan genel amaçlı gdb‘nin aksine, özellikle Go runtime’ı için tasarlanmıştır.

12.1 Temel Kullanım

dlv debug ./cmd/myapp -- --flag=value

Debugger içinde:

(dlv) break main.processRequest
(dlv) continue
(dlv) print req
(dlv) locals
(dlv) goroutines
(dlv) goroutine 12 stack
(dlv) next
(dlv) step
(dlv) stepout

12.2 Çalışan Bir Sürece Bağlanma

dlv attach <pid>

Yeniden başlatmadan takılı/donmuş bir production sürecini teşhis etmek için son derece kullanışlıdır — sadece statik bir stack trace değil, canlı bir goroutine dökümü alır ve değişken durumunu inceleyebilirsiniz.

12.3 Koşullu Breakpoint’ler ve Tracepoint’ler

(dlv) break main.go:45 if userID == 12345
(dlv) trace main.HandleRequest

trace, yürütmeyi durdurmadan giriş/çıkışı loglayan bir tracepoint ayarlar — tam adım-adım ilerlemenin overhead’i olmadan, çalışan bir sistemde çağrı sıklığını/argümanlarını anlamak için kullanışlıdır.

12.4 Core Dump’ları Debug Etme

GOTRACEBACK=crash ./myapp   # crash'te bir core dump üretir
dlv core ./myapp core_file

GOTRACEBACK=crash ve ulimit -c unlimited ile birleştirildiğinde, bu, fatal hatanın tam anındaki goroutine durumunu inceleyerek çökmüş bir production binary’sinin tam post-mortem analizini yapmanızı sağlar.

12.5 Uzaktan Debugging (headless mod)

dlv debug --headless --listen=:2345 --api-version=2 ./cmd/myapp

Sonra IDE’nizden (VS Code, GoLand) Delve DAP/JSON-RPC protokolü üzerinden bağlanın — container’lar/Kubernetes pod’ları içinde kubectl port-forward üzerinden debug etmek için standarttır.


13. Race Detection

Go’nun race detector’ü (ThreadSanitizer üzerine inşa edilmiştir), data race’leri yakalamak için bellek erişimlerini enstrümante eder — aynı bellek konumuna, en az birinin yazma olduğu, senkronize edilmemiş concurrent erişim.

go test -race ./...
go build -race -o myapp-race .
./myapp-race

Kritik gerçekler:

13.1 Yaygın Race Desenleri

// 1.22-öncesi hata deseni:
for _, item := range items {
    go func() {
        process(item) // paylaşılan döngü değişkenini yakalar
    }()
}
// Düzeltme (1.22-öncesi): item'ı açıkça geçirin
for _, item := range items {
    go func(item Item) {
        process(item)
    }(item)
}

14. Deadlock ve Goroutine Diagnostics

14.1 Runtime Deadlock Tespiti

Go’nun runtime’ı tüm-goroutine’ler-uykuda deadlock’larını otomatik olarak tespit eder ve şununla çöker:

fatal error: all goroutines are asleep - deadlock!

Bu, yalnızca global deadlock’ları yakalar (her tek goroutine bloke) — bir alt küme takılıp kalırken bazı goroutine’lerin çalışabilir kaldığı kısmi deadlock’ları tespit etmez. Bunlar SIGQUIT dökümleri veya pprof‘un goroutine profili aracılığıyla manuel araştırma gerektirir.

14.2 Canlı Bir Süreçte Tam Stack Dökümü Tetiklemek

kill -QUIT <pid>

Varsayılan olarak bu, tüm goroutine stack’lerini stderr’e yazdırır ve süreci sonlandırır (debug.SetTraceback‘i ayarlamadıysanız veya özel bir sinyal handler’ınız yoksa). Sonlandırmayan canlı bir döküm için, bunun yerine pprof endpoint’ine gidin:

curl http://localhost:6060/debug/pprof/goroutine?debug=2

14.3 GOTRACEBACK Environment Değişkeni

Çökme çıktısının ayrıntı düzeyini kontrol eder:

Post-mortem çökme diagnostiği önemli olan servisler için production’da GOTRACEBACK=all (veya derin runtime debugging için system) ayarlayın.


15. Production’da Continuous Profiling

Nokta-zamanlı profiling (SSH ile bağlanma, /debug/pprof/‘a istek atma, indirme, analiz etme) aralıklı sorunlar veya filo-genelinde analiz için ölçeklenmez. Continuous profiling araçları, production süreçlerini düşük overhead’le sürekli olarak örnekler ve loglar/metrikler gibi sorgulanabilir geçmiş profil verisini saklar.

Yaygın yaklaşımlar:

Anlamlı production ölçeğinde Go çalıştıran her ekip için, continuous profiling’i devreye almak en yüksek kaldıraçlı gözlemlenebilirlik yatırımlarından biridir — “profillemek için bunu yeniden üretmemiz gerekir"i “olay sırasında profiler’ın zaten kaydettiğine bakalım"a dönüştürür.


16. Flame Graph ve Görselleştirme

go tool pprof -http=:8080 cpu.prof, aşağıdakilere sahip tam bir yerel web arayüzü başlatır (eski -web/yalnızca-Graphviz akışının halefi):

go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30

Bu genellikle bir profili interaktif olarak keşfetmenin en hızlı, en ergonomik yoludur — keşif amaçlı çalışma için düz REPL yerine bunu tercih edin, REPL’i scripted/CI-güdümlü kontroller için saklayın (örn. bir perf-regression testinde en üst fonksiyonun X%‘in altında kaldığını doğrulamak gibi).


17. Yaygın Hatalar Kataloğu

BelirtiOlası NedenDoğrulama Aracı
Yükselen RSS, sabit istek oranıGoroutine leak’i veya sınırsız cachegoroutine profil diff’i, inuse_space heap diff’i
Yüksek p99 gecikmesi, normal ortalamaGC STW duraklamaları, scheduler contentiongo tool trace
Yüksek CPU, düşük throughputAşırı allocation → GC baskısıalloc_space profili, -benchmem
Ani çökme: “all goroutines asleep”Global deadlockÇökme stack trace’inin kendisi
Aralıklı yavaş isteklerLock contentionmutex/block profili
Kubernetes’te OOM-killGOMEMLIMIT yok, GOGC çok yüksek veya gerçek bir leakGOMEMLIMIT + heap diff
fatal error: concurrent map writesSenkronize edilmemiş map erişimiKod incelemesi + ilgili test yolunda -race
gctrace=1‘de yüksek GC CPU%Allocation-ağırlıklı sıcak yolalloc_objects profili, escape analysis
Goroutine sayısı isteklerle doğrusal büyüyor, hiç düşmüyorEksik cancel(), kapatılmamış response body’leriZaman içinde goroutine?debug=2 diff’i

18. Vaka Analizleri (Case Studies)

18.1 Sessiz Goroutine Leak’i

Belirti: Bir servisin belleği sabit yük altında saatte ~50MB büyüdü, sonunda yaklaşık her 18 saatte bir OOM-kill oldu.

İnceleme: 30 dakika arayla alınan iki anlık görüntü arasındaki inuse_space heap diff’i, büyümenin uygulama struct’larında değil bufio.Reader buffer’larında ve HTTP transport dahili yapılarında yoğunlaştığını gösterdi. goroutine?debug=2, net/http.(*persistConn).readLoop‘ta takılı kalmış binlerce goroutine ortaya çıkardı.

Kök neden: Downstream bir HTTP client, erken-dönüş hata yollarında resp.Body‘yi kapatmıyor/tüketmiyordu; bu da connection’ı (ve ilişkili read-loop goroutine’ini) süresiz olarak canlı bırakıyordu — connection asla pool’a geri döndürülmüyor ya da temizlenmiyordu.

Çözüm: HTTP çağrısından gelen hatayı kontrol ettikten hemen sonra her zaman defer resp.Body.Close() yapın ve keep-alive connection’ların yeniden kullanılması isteniyorsa kapatmadan önce body’yi io.Copy(io.Discard, resp.Body) ile tüketin.

18.2 p99 Gecikme Uçurumu

Belirti: p50 gecikmesi iyiydi (5ms), ama p99 periyodik olarak, kabaca her 2 dakikada bir 800ms’ye sıçradı.

İnceleme: Yakalanan bir pencere sırasında go tool trace, tüm P’ler boyunca yaklaşık 600ms süren, bir GC döngüsüyle ilişkili net, senkronize bir boşluk gösterdi. gctrace=1, her collection’dan önce heap’in 400MB’tan 900MB’a büyüdüğü büyük GC döngülerini doğruladı.

Kök neden: GOGC, doğal olarak dalgalı bir allocation deseniyle (her 2 dakikada bir büyük batch processing) varsayılan 100’de bırakılmıştı; bu da GC, mutator’ın allocation oranının gerisinde kaldığında büyük mark-assist duraklamalarına neden oluyordu.

Çözüm: GOMEMLIMIT‘i uygun şekilde ayarladık ve GOGC‘yi 50’ye düşürdük; ortalama GC CPU’sunda hafif bir artış karşılığında çok daha küçük, daha sık (ve dolayısıyla daha kısa) bireysel duraklamalar elde ettik — periyodik uçurumu ortadan kaldırdı.


19. Kontrol Listesi: Production Olayı İncelemesi

  1. Yeniden üretin veya canlı yakalayın: Devam ediyorsa, durum değişmeden önce hemen /debug/pprof/goroutine?debug=2, /debug/pprof/heap ve 30sn’lik bir CPU profilinin anlık görüntüsünü alın.
  2. runtime.NumGoroutine() trendini kontrol edin — sınırsız şekilde mi yükseliyor?
  3. Bellekle ilgiliyse, olay penceresi boyunca heap anlık görüntülerini karşılaştırın (diff).
  4. GC CPU% ve duraklama süresi trendleri için GODEBUG=gctrace=1 çıktısını (veya continuous profiler eşdeğerini) kontrol edin.
  5. Belirti gecikmeyse (throughput değil), go tool trace ile ilişkilendirin.
  6. GOMAXPROCS‘u gerçek container CPU kotasıyla kontrol edin.
  7. GOMEMLIMIT‘i gerçek container bellek kotasıyla kontrol edin.
  8. Yeni allocation-ağırlıklı kod yolları, yeni kilitler veya karşılık gelen temizleme olmadan yeni goroutine-doğurma mantığı için son deploy’ları gözden geçirin.
  9. Bir düzeltmeyi deploy etmeden önce staging’de bir benchmark + -race + profil ile doğrulayın — gerçek yakalanmış profile karşı doğrulama yapmadan “muhtemelen düzelttim” tarzı bir patch göndermeyin.

20. Referans Komutlar Özet Tablosu

# Çalışan sunucudan CPU profili
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30

# Heap kullanımdaki anlık görüntü
go tool pprof -http=:8080 -inuse_space http://localhost:6060/debug/pprof/heap

# Heap allocation (kümülatif) anlık görüntü
go tool pprof -http=:8080 -alloc_space http://localhost:6060/debug/pprof/heap

# Goroutine dökümü, insan tarafından okunabilir
curl http://localhost:6060/debug/pprof/goroutine?debug=2

# İki heap anlık görüntüsünü karşılaştırma
go tool pprof -base heap_before.prof heap_after.prof

# Execution trace
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.out

# Allocation istatistikleriyle benchmark, 10 çalıştırma
go test -bench=. -benchmem -count=10 ./...

# Benchmark çalıştırmalarını istatistiksel olarak karşılaştırma
benchstat old.txt new.txt

# Escape analysis
go build -gcflags="-m -m" ./...

# Race detector
go test -race ./...

# GC trace
GODEBUG=gctrace=1 ./myapp

# Canlı süreç debugging
dlv attach <pid>

# Core dump analizi
GOTRACEBACK=crash ./myapp
dlv core ./myapp core_file

# Tam goroutine dökümü + sonlandırma (production'da dikkat!)
kill -QUIT <pid>

Kapanış Notları

Go’nun diagnostik araçları, bir dil runtime’ı için alışılmadık derecede eksiksizdir — pprof, trace, runtime/metrics, race detection ve Delve’nin kombinasyonu esasen her sınıf production sorununu kapsar: CPU, bellek, concurrency ve gecikme. İyi bir Go mühendisini harika bir Go mühendisinden ayıran disiplin, her aracın var olduğunu bilmek değildir — belirtinin şekline göre doğru araca uzanma alışkanlığıdır (throughput problemi → CPU/alloc profili; gecikme problemi → trace; zaman içinde büyüme problemi → heap/goroutine diff’leme; çökme → core dump + Delve) ve her optimizasyon iddiasını sezgiyle ileri sürülen bir şey olarak değil, benchstat ile doğrulanması gereken bir hipotez olarak ele almaktır.