Go'da Profiling ve Debugging — Derinlemesine, Uygulayıcı Seviyesinde Rehber
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
- Felsefe: Tahmin Etme, Ölç
pprofEkosistemi- CPU Profiling
- Bellek (Memory) Profiling
- Goroutine Profiling ve Leak Tespiti
- Block ve Mutex Profiling (Contention)
- Execution Tracer (
go tool trace) testing.Bvebenchstatile Benchmarking- Escape Analysis ve Compiler Diagnostics
- Garbage Collector Tuning
runtime/metricsPaketi- Delve ile Debugging
- Race Detection
- Deadlock ve Goroutine Diagnostics
- Production’da Continuous Profiling
- Flame Graph ve Görselleştirme
- Yaygın Hatalar Kataloğu
- Vaka Analizleri (Case Studies)
- Kontrol Listesi: Production Olayı İncelemesi
- 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:
- CPU-bound sıcak yollar (hot path) — verimsiz algoritmalar, GC’ye baskı yapan aşırı allocation, reflection-ağırlıklı serialization.
- Bellek sorunları — leak’ler (genelde klasik C-tarzı
freeunutma değil, goroutine tarafından tutulan referanslardır), sınırsız büyüyen cache’ler, slice-retention hataları. - Concurrency sorunları — goroutine leak’leri, lock contention, channel deadlock’ları, scheduler gecikmesi (GOMAXPROCS yanlış yapılandırması).
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:
| Endpoint | Açıklama |
|---|---|
/debug/pprof/profile?seconds=30 | CPU profili (bloklayıcı, N saniye boyunca örnekleme yapar) |
/debug/pprof/heap | Heap allocation anlık görüntüsü |
/debug/pprof/goroutine | O anki tüm goroutine’lerin stack trace’leri |
/debug/pprof/block | Senkronizasyon primitiflerinde bloklanan goroutine’ler |
/debug/pprof/mutex | Mutex contention |
/debug/pprof/threadcreate | OS thread oluşturma stack’leri |
/debug/pprof/allocs | Program başlangıcından beri yapılan tüm allocation’lar (sadece canlılar değil) |
/debug/pprof/trace?seconds=5 | Execution 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ı:
top— fonksiyonları flat/cumulative kaynak kullanımına göre sıralı liste.list <func>— satır başına maliyeti gösteren, açıklamalı kaynak kod.web/svg— call-graph görselleştirmesi (Graphviz gerektirir:apt install graphviz).traces— ham örnek stack’ler.peek <func>— belirli bir fonksiyonun caller/callee’leri.-basebayrağı — iki profili karşılaştırır (go tool pprof -base old.prof new.prof), A/B karşılaştırması için elzemdir.
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:
- Çok kısa ömürlü fonksiyonlar yetersiz örneklenebilir veya tamamen kaçırılabilir.
- Profillerin istatistiksel olarak anlamlı olması için yeterli süre (genelde 30sn+) gerekir.
- Inline edilmiş fonksiyonlar,
-gcflags="-l"ile inlining kapatılmadıkça ayrı frame’ler olarak görünmeyebilir (bu sadece diagnostik build’ler için, asla production için değil).
3.1 Flat vs. Cumulative
- Flat: o fonksiyonun kendi kodunda geçen süre, çağırdığı fonksiyonlar hariç.
- Cumulative: o fonksiyonda artı çağırdığı her şeyde geçen süre.
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:
- Sıcak döngülerin içinde beklenmedik allocation’lar (profildeki
runtime.mallocgc‘nin yüksek çıkması sadece “CPU işi” değil, allocation baskısı anlamına gelir). runtime.memmove,runtime.typedmemmove— genelde büyük struct’ların gereksiz kopyalanmasından kaynaklanır; bunun yerine pointer geçirin.- Reflection kullanımı (
reflect.Value.*) — JSON/encoding-ağırlıklı kod yollarında yaygındır; sıcak yollar için codegen (örn.easyjson,ffjson) veya manuel marshaling düşünün. runtime.mapaccess*/runtime.mapassignbaskınsa — map-ağırlıklı algoritmalar demektir; bilinen indeksli array/slice’ları veya sadece kullanım deseni gerçekten uyuyorsa (çoğunlukla okuma, goroutine başına ayrık key’ler)sync.Map‘i düşünün.
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
inuse_space(varsayılan görünüm): şu anda canlı/erişilebilir olan bellek — leak’leri veya beklenmedik derecede büyük canlı heap’leri bulmak için kullanın.alloc_space: process başlangıcından beri allocate edilmiş toplam kümülatif byte, free edilmiş olsun olmasın — bellek sızdırmasa bile GC’ye baskı yapan allocation-ağırlıklı sıcak yolları bulmak için kullanın.
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:
- 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.
- Slice alt-slice’lamanın backing array’i tutması —
smallSlice := bigSlice[:10],smallSlicereferans edildiği sürecebigSlice‘ın tüm backing array’ini canlı tutar. Çözüm: gerisini serbest bırakmanız gerekiyorsa, gereken veriyiappend([]T(nil), bigSlice[:10]...)ile kopyalayın. - Eviction’ı olmayan global cache/map’ler — hiç temizlenmeyen
map[string]*Session; TTL eviction, LRU içincontainer/list+ map, veyagroupcache/ristrettogibi bir kütüphane kullanın. - Durdurulmamış
time.Timer/time.Ticker— runtime’ın timer heap’i tarafından tutulur. - 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
- Alıcısı olmayan buffered olmayan channel gönderimi — artık kimsenin okumadığı bir channel’a yazan bir goroutine (örn. okuyucu hata verip erken döndü) sonsuza kadar bloke olur.
- Hiç iptal edilmeyen
context.Context— hiç tetiklenmeyen<-ctx.Done()‘ı bekleyen alt goroutine’ler, çünkü parentcancel()çağırmayı unuttu.context.WithCancel/WithTimeout/WithDeadline‘dan hemen sonra her zamandefer cancel()çağırın. - Fire-and-forget bir goroutine’de
default‘u olmayan ve timeout yolu bulunmayanselect. - Kapatılmamış/tüketilmemiş HTTP response body’leri — altta yatan connection’ın goroutine mekanizmasını sızdırır ve connection reuse’u engeller.
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
}
- Block profile (
/debug/pprof/block): goroutine’lerin channel işlemlerinde,sync.Mutex.Lock‘ta,sync.WaitGroup.Wait‘te, network I/O beklemesinde vb. bloke olarak geçirdiği süre. Görünüşte concurrent olan kodda serileşme noktalarını bulmak için harikadır. - Mutex profile (
/debug/pprof/mutex): özellikle çekişmeli (contended) mutex’ler — birden fazla goroutine’in aynı kilit için mücadele ettiği yerler. Buradaki yüksek bir sayı genelde şu anlama gelir: kilit granularity’si çok kaba (tek bir dev mutex tüm struct’ı korurken, alan-başına veya sharded kilitler yerine), veya sıcak bir yol kilidi gereğinden uzun tutuyor (örn. kilit tutarken I/O yapmak).
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
- Kritik bölge (critical section) boyutunu azaltın — pahalı işi kilidin dışında yapın, sadece paylaşılan durumu kilidin içinde değiştirin.
- Tek bir global kilit yerine kilitleri parçalayın (shard) (örn. key’e göre hash’lenmiş, her biri kendi mutex’ine sahip 16 veya 32 bucket) — bu,
sync.Map‘in ve birçok yüksek-throughput cache’in dahili olarak kullandığı desendir. - Basit sayaçlar için mutex-korumalı bir int yerine
sync/atomic‘i düşünün. - Okumalar yazmalardan çok fazlaysa
sync.RWMutex‘i düşünün — ama dikkat:RWMutex, düşük contention’daMutex‘e göre kilit başına daha yüksek overhead’e sahiptir, yani her zaman bedava bir kazanç değildir.
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:
- View trace — mikrosaniye çözünürlükle goroutine yürütmesini, GC duraklamalarını, syscall’ları ve network beklemelerini gösteren P (processor) başına zaman çizelgesi. GC STW (stop-the-world) duraklamalarını, goroutine zamanlama gecikmelerini ve syscall bloklanmasını görsel olarak fark edebilirsiniz.
- Goroutine analysis — goroutine başına yürütme/bekleme/blok/syscall süresi dökümü.
- Network/Sync/Syscall blocking profiles — pprof’un block profile’ıyla aynı veri, ama zamansal görünümle.
- Minimum mutator utilization (MMU) — kayan zaman pencereleri boyunca CPU zamanının ne kadarının gerçek program kodunuza vs. GC’ye ayrıldığını gösterir. Gecikmeye duyarlı servisler (trading sistemleri, real-time API’ler) için kritiktir.
7.1 Nelere Dikkat Edilmeli
- Goroutine’in “runnable” ile “running” arası uzun boşluklar — scheduler contention’ına işaret eder, genelde eşzamanlı işe göre çok düşük
GOMAXPROCS‘tan veya çok fazla goroutine’in sınırlı P’ler için mücadele etmesinden kaynaklanır. - Sık, uzun GC STW duraklamaları — tüm P’ler boyunca senkronize boşluklar olarak görünür. Modern Go GC’si (1.5’in concurrent collector’ından beri) normal koşullarda STW fazlarını mikrosaniye aralığında tutar; sürekli olarak çoklu milisaniyelik STW görüyorsanız, heap boyutunu, allocation oranını veya
GOGCayarlarını inceleyin. - P’leri bloke eden syscall-ağırlıklı goroutine’ler — bloklayan bir syscall’daki bir goroutine kendi P’sini ayırır (başka bir M/thread’e devredilir) ama aşırı syscall’lar (örn. buffered olmayan dosya/network I/O) yine de zamanlama baskısı olarak görünebilir.
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
-benchmem,allocs/opveB/opraporlar — geneldens/op‘tan daha önemlidir, çünkü allocation oranı GC baskısını belirler.-count=10benchmark’ı 10 kez çalıştırır, böylece tek bir gürültülü çalışmaya güvenmek yerine istatistik uygulayabilirsiniz.-cpuprofile/-memprofile, profiling’i doğrudan benchmark çalışmasına ekler; bu sayede kontrollü koşullar altında tam olarak sıcak yolugo tool pprofile inceleyebilirsiniz (canlı, gürültülü bir production sunucusunu profillemekten çok daha temiz bir sinyaldir).
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:
- Lokal bir değişkene pointer döndürmek (çoğu durumda gerekli ve normaldir, ama heap allocation’ı zorunlu kıldığının farkında olun).
- Bir değeri
interface{}/anyparametresine geçirmek — bu, aksi ispatlanamadıkça genelde değeri heap’e “boxing” eder. - Bir pointer’ı, mevcut stack frame’inden daha uzun ömürlü olan bir struct/slice/map’e depolamak.
- Closure’ın kendisi escape ediyorsa (örn. bir struct’ta saklanıyorsa veya
go func(){}‘e geçiriliyorsa), değişkenleri referans olarak yakalayan closure’lar. - Derleme zamanında bilinmeyen değişken boyutu (örn. bir değişkenden gelen slice uzunluğu) stack allocation’ını engelliyorsa.
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:
- Race detector, yalnızca yürütme sırasında gerçekten oluşan race’leri yakalar — statik analiz değildir. Nadiren tetiklenen bir kod yolundaki bir race, o yol enstrümante edilmiş binary altında çalışmadıkça yakalanmaz.
- Önemli CPU (~2-20x) ve bellek (~5-10x) overhead’i ekler —
-racebinary’lerini asla gerçek trafik için production’da çalıştırmayın; bunun yerine CI’da, load testing’de ve temsili trafikle staging’de kullanın. - CI’da entegrasyon/E2E test paketlerini her zaman
-raceile çalıştırın — burada bulunan race’ler, production’da bulunanlardan (genelde deterministik olmayan, yeniden üretilmesi zor bozulma veya çökmeler olarak ortaya çıkar) çok daha ucuzdur.
13.1 Yaygın Race Desenleri
- Bir mutex olmadan birden fazla goroutine’den paylaşılan bir map’e yazmak (Go map’leri açıkça concurrent okuma+yazma için güvenli değildir; çıplak bir concurrent yazma, runtime’ı bile bozabilir ve fatal, kurtarılamaz bir çökmeye neden olabilir —
fatal error: concurrent map writes— bunun-racetespitiyle aynı şey olmadığını not edin; bu fatal hata-raceolmadan da tetiklenir). - Bir goroutine’de döngü değişkenini referans olarak yakalama (Go 1.22+‘da düzeltildi, döngü değişkenleri artık varsayılan olarak iterasyon-başınadır — ama eski Go sürümlerinde çalışıyorsanız veya legacy kod okuyorsanız bunun farkında olun):
// 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)
}
- “Salt okunur görünse” bile (örn.
sync.Onceolmadan lazy-initialization desenleri), birden fazla goroutine’den senkronizasyon olmadan bir struct alanını okumak/yazmak.
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:
GOTRACEBACK=none— traceback yok (en sıkı güvenlik kısıtlı bağlamlar dışında asla kullanmayın).GOTRACEBACK=single(varsayılan) — yalnızca çöken goroutine’in traceback’i.GOTRACEBACK=all— tüm kullanıcı goroutine’leri.GOTRACEBACK=system— runtime-dahili olanlar dahil tüm goroutine’ler.GOTRACEBACK=crash—systemgibi, artı bir core dump tetikler.
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:
- Google Cloud Profiler, Datadog Continuous Profiler, Grafana Pyroscope (açık kaynak, CNCF), Parca (açık kaynak, eBPF-tabanlı, birçok dil için kod değişikliği olmadan profil çekebilir).
- Overhead düşük olacak şekilde tasarlanmıştır (genelde <%2-5 CPU) — azaltılmış örnekleme oranları ve verimli aggregation ile — manuel olarak
SetMutexProfileFraction(1)‘i açmanın aksine, her zaman-açık production kullanımı için güvenlidir. - Ana değer: bir profil anlık görüntüsünü, olay penceresi sırasında manuel olarak bir profil yakalamaya gerek kalmadan, olaydan sonra belirli bir olay zaman damgasıyla ilişkilendirmektir.
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):
- Flame graph görünümü — genişlik göreceli zaman/kaynak maliyetini temsil eder, derinlik call stack derinliğini temsil eder. Stack’in üstündeki geniş platolar optimizasyon hedeflerinizdir.
- Graph görünümü — ağırlıklı kenarlara sahip klasik call-graph.
- Peek/Source/Disassembly görünümleri — satır-seviyesinde ve hatta assembly-seviyesinde maliyet atfı.
- Top/Flat/Cumulative sıralanabilir tablolar.
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
| Belirti | Olası Neden | Doğrulama Aracı |
|---|---|---|
| Yükselen RSS, sabit istek oranı | Goroutine leak’i veya sınırsız cache | goroutine profil diff’i, inuse_space heap diff’i |
| Yüksek p99 gecikmesi, normal ortalama | GC STW duraklamaları, scheduler contention | go tool trace |
| Yüksek CPU, düşük throughput | Aşırı allocation → GC baskısı | alloc_space profili, -benchmem |
| Ani çökme: “all goroutines asleep” | Global deadlock | Çökme stack trace’inin kendisi |
| Aralıklı yavaş istekler | Lock contention | mutex/block profili |
| Kubernetes’te OOM-kill | GOMEMLIMIT yok, GOGC çok yüksek veya gerçek bir leak | GOMEMLIMIT + heap diff |
fatal error: concurrent map writes | Senkronize edilmemiş map erişimi | Kod incelemesi + ilgili test yolunda -race |
gctrace=1‘de yüksek GC CPU% | Allocation-ağırlıklı sıcak yol | alloc_objects profili, escape analysis |
| Goroutine sayısı isteklerle doğrusal büyüyor, hiç düşmüyor | Eksik cancel(), kapatılmamış response body’leri | Zaman 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
- Yeniden üretin veya canlı yakalayın: Devam ediyorsa, durum değişmeden önce hemen
/debug/pprof/goroutine?debug=2,/debug/pprof/heapve 30sn’lik bir CPU profilinin anlık görüntüsünü alın. runtime.NumGoroutine()trendini kontrol edin — sınırsız şekilde mi yükseliyor?- Bellekle ilgiliyse, olay penceresi boyunca heap anlık görüntülerini karşılaştırın (diff).
- GC CPU% ve duraklama süresi trendleri için
GODEBUG=gctrace=1çıktısını (veya continuous profiler eşdeğerini) kontrol edin. - Belirti gecikmeyse (throughput değil),
go tool traceile ilişkilendirin. GOMAXPROCS‘u gerçek container CPU kotasıyla kontrol edin.GOMEMLIMIT‘i gerçek container bellek kotasıyla kontrol edin.- 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.
- 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.