Go'da GC baskısını sync.Pool ve escape analizi ile nasıl azaltırım?
Soru
Go ile yazdığımız bir veri ingest servisi saniyede 50.000 küçük JSON paketi alıp işleyip veritabanına yazıyor. Yoğun yükte GC devreye girince CPU tavan yapıyor ve "Stop-the-World" duraklamaları API yanıt sürelerinde spike yaratıyor. Bellekteki nesne tahsisini azaltmak için `sync.Pool` ve escape analysis'i bu senaryoda nasıl uygularım?
Cevap
Kısa cevap: GC duraklamaları bir semptom; asıl düşman tahsis hızı (allocation rate). Önce tahsisleri kesin, GC zaten rahatlar.
Kısa cevap
Yaşadığınız spike’lar GC’nin “kötü” olmasından değil — saniyede 50k paket, saniyede yüz binlerce kısa ömürlü nesne demek; GC bu çöpü toplamak için sürekli ve agresif çalışıyor, Stop-the-World duraklamaları da yanıt süresine yansıyor. Çözüm GC’yi kapatmak değil, ona daha az iş vermek. Bu yolun eşzamanlılık tarafını Go’da goroutine ve channel pratiği yazısında ele almıştım.
Neden
-
Duraklamanın kaynağı tahsis sıklığıdır. GC ne kadar çöp üretirseniz o kadar sık koşar; ayarı değiştirmek üretimi değiştirmez.
-
En ucuz tahsis, hiç yapılmayandır. Bir değeri stack’te tutabiliyorsanız GC o nesneyi hiç görmez — havuzlamaktan da ucuzdur.
-
Pooling’in bir ters tepme riski var. Başka heap verisine pointer tutan bir nesneyi havuzlarsanız, havuz onu canlı tuttuğu için işaret ettiği büyük graf da serbest kalmaz; bellek beklediğinizden çok şişer.
Ne yapmalı
-
Kör gitmeyin:
pprofile önce/sonra ölçün.alloc_spaceprofili en çok tahsis eden satırı söyler;GODEBUG=gctrace=1GC sıklığını ve duraklama süresini gösterir. Aynı yükle önce ve sonra ölçün. -
Tahsisi kökten azaltın. Slice/map’leri biliyorsanız baştan
make([]T, 0, n)ile boyutlandırın;bytes.Bufferve JSON decoder’ı tekrar kullanın; gereksiz[]byte↔stringkopyalarından kaçının. Tek bir kopya 50k kez çarpınca dağ olur. -
Escape analizini çalıştırın, stack’te tutun.
go build -gcflags=-mçıktısı hangi değerin heap’e kaçtığını söyler. Bir pointer’ı fonksiyondan dışarı kaçırmak ya da bir değeri interface’e koymak çoğu kaçışın nedenidir. -
Hot path’te
sync.Poolile yeniden kullanın. Ingest yolundaki kısa ömürlü buffer’ları, struct’ları ve decoder’ları havuzdan al-geri verin.Putetmeden önce nesneyi resetleyin ki eski veri sızmasın. Havuzda yalnızca düz, kendi içinde kapalı buffer/struct’ları tutun. -
GOGC/GOMEMLIMITayarını en sona bırakın. GC’yi seyrekleştirirler ama saniyede 50k tahsis yapan bir yolu düzeltmezler; son ince ayar, ilk hamle değil.
Sonuç: Sıralama net: profilleyin → en çok tahsis edeni öldürün → hayatta kalanları havuzlayın → sonra GOGC/GOMEMLIMIT ayarlayın. Asıl kazanç, GC’ye en sıcak yolda hiç çöp üretmemekten gelir.
İlgili Yazılar
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.