GO PPROF PERFORMANS ANALİZİ

Go pprof CPU profilini görselleştiren yatay flame graph blokları ve sıcak fonksiyon yığını

Production'da çalışan Go servislerinde pprof ile yapılan ölçümler şunu gösteriyor: tek bir CPU profili sonrasında bulunan darboğazlar genelde p99 gecikmeyi %30-60 arasında düşürüyor, allocation odaklı düzeltmeler ise GC duraklamalarını üçte birine kadar indirebiliyor. Bu sayılar tesadüf değil — bir Go servisi yazıldıktan sonra ilk pprof seansı, çoğu zaman kodun gözden kaçan bir döngüsünü, gereksiz bir json.Marshal çağrısını veya kontrol edilmeyen bir mutex bekleyişini ortaya çıkarır. Asıl mesele aracın varlığı değil, doğru profilden doğru sinyali okuyabilmektir.

pprof Nedir, Hangi Profilleri Toplar?

pprof, Go'nun standart kütüphanesiyle birlikte gelen örnekleme tabanlı bir profilleme aracıdır. Çalışan programdan periyodik olarak yığın izleri (stack trace) toplar ve bunları çağrı grafiği olarak analiz etmenizi sağlar. Topladığı temel profil türleri şunlardır:

  • CPU profili: Hangi fonksiyon, ne kadar CPU zamanı harcıyor.
  • Heap profili: Bellekte yaşayan ve ayrılan nesnelerin dağılımı.
  • Goroutine profili: O an çalışan veya bloklanmış goroutine'lerin yığın izleri.
  • Block profili: Senkronizasyon primitiflerinde harcanan bekleme süresi.
  • Mutex profili: Lock contention'a neden olan çağrı noktaları.

Her profil farklı bir soruya cevap verir. "Servis neden bu kadar CPU yiyor?" sorusunun cevabı CPU profilindedir; "RAM neden sürekli şişiyor?" sorusunun cevabı ise heap profilinde aranır. Yanlış profile bakmak, doğru soruyu sormamak kadar zaman kaybettiricidir.

pprof'u Servise Bağlamak

HTTP servisi için en hızlı yol net/http/pprof paketini blank import etmektir:

import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ana servis kodu
}

Bu adım sonrası localhost:6060/debug/pprof/ altındaki uç noktalardan profilleri çekebilirsiniz. CPU profili için:

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

Heap profili için ?seconds parametresi gerekmez, anlık snapshot alınır. CLI yerine tarayıcı arayüzünü tercih ediyorsanız -http=:8080 bayrağıyla flame graph, çağrı grafiği ve top listesini görsel olarak inceleyebilirsiniz. Go diline genel bir altyapı kazandırmak isteyenler Go eğitimi içeriğinden yararlanabilir, ardından bu profilleri çok daha bilinçli okuyabilir.

Heap profilinde inuse_space ve alloc_space dağılımını gösteren karşılaştırmalı bar grafik

CPU Profili: Sıcak Fonksiyonları Bulmak

CPU profili açıldığında pprof CLI'da en kullanışlı iki komut top ve list'tir. top komutu en çok CPU harcayan fonksiyonları sıralar; iki sütun önemlidir:

  • flat: Fonksiyonun kendi gövdesinde geçirdiği süre.
  • cum: Fonksiyon ve onun çağırdığı her şeyin toplam süresi.

Yüksek flat değeri olan bir fonksiyon, optimize edilebilecek gerçek bir aday demektir. Yüksek cum ama düşük flat ise sorun büyük ihtimalle çağırılan alt fonksiyonlardadır. list FonksiyonAdi komutu, fonksiyonun satır satır CPU dökümünü gösterir; bu görünüm "tek bir regexp.Compile her istek için yeniden derleniyor" gibi gerçek hayat hatalarını kabaca beş dakikada ortaya çıkarır.

Tipik darboğazlar şunlar olur: her istekte yeniden derlenen regexler, byte slice yerine string concatenation, reflect kullanımının saklı maliyeti ve önbelleğe alınmamış JSON encoder'lar. Aracın çıkış noktasını ve tasarım gerekçesini merak edenler için konuya ilişkin resmi yazıyı incelemek, flame graph'ı okurken karşılaşılan kavramların neden bu şekilde isimlendirildiğini anlamayı kolaylaştırır.

Heap Profili ve Bellek Allocation Analizi

Heap profili iki farklı bakış sunar: inuse_space şu an yaşayan bellek, alloc_space ise programın başından beri toplam ayrılan bellek. GC baskısını anlamak için alloc_objects de kritiktir — büyük objelerden çok, çok sayıda küçük objenin sürekli ayrılması GC için daha ağırdır.

Bellek darboğazlarının büyük kısmı şu kalıplara düşer:

  1. Slice'ların kapasite ön ayarlaması yapılmadan büyütülmesi.
  2. Map'lerin ön boyutlandırılmadan oluşturulup defalarca rehash'lenmesi.
  3. Hot path üzerinde fmt.Sprintf kullanımı.
  4. Byte buffer'ların pool'lanmaması (sync.Pool yokluğu).
  5. Kapatılmayan response body'leri ve sızdıran goroutine'ler.

Bu kalıplardan birinin düzeltilmesi tek başına bile ortalama allocation oranını ciddi ölçüde düşürebilir; sync.Pool eklenen bir JSON encode yolunda allocation/op sayısının onda bire indiği gerçek vakalar nadir değildir.

Goroutine, Block ve Mutex Profilleri

Servis takılıp kalıyor ama CPU düşükse sorun genelde eşzamanlılık tarafındadır. Goroutine profili o an kaç goroutine'in nerede beklediğini gösterir — sayı zaman içinde artıyorsa goroutine sızıntınız vardır. Block profili kanal okuma/yazmasında veya sync.Cond.Wait üzerinde geçen süreyi, mutex profili ise lock contention'ı raporlar.

Block ve mutex profilleri varsayılan olarak kapalıdır; örnekleme oranını programatik olarak açmanız gerekir:

runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)

Production'da bu oranları daha düşük tutmak gerekir, aksi halde profilin kendisi gözlemlenen sistemi yavaşlatır. Bu üç profil birlikte okunduğunda "tek bir mutex tüm istekleri seri hale getiriyor" gibi mimari hataları açığa çıkarır.

Tipik Darboğazlardan Beklenen Hız Kazanımı

pprof'un sağladığı en somut katkı, müdahale öncesi ve sonrası ölçülebilir farkı görmek. Saha gözlemlerine göre belirli kalıpların düzeltilmesi şu mertebede iyileşme getirir:

  • Hot path üzerinde regex derlemesinin sync.Once'a alınması: ilgili endpoint'te 2-4 kat hızlanma.
  • JSON yerine kod üretimli serileştirme: tipik olarak %40-70 daha az CPU.
  • Slice/map ön boyutlandırma: %15-30 daha az allocation, GC duraklamalarında belirgin düşüş.
  • sync.Pool ile buffer geri dönüşümü: alloc/op 5-10 katına kadar azalır.
  • Gereksiz mutex bölgelerinin daraltılması: yüksek RPS altında p99 yarıya inebilir.

Bu rakamlar elbette koda ve yüke göre değişir; ancak ölçüm yapmadan tahminle yapılan "optimizasyonların" çoğu zaman hiçbir şey değiştirmediği, hatta kodu bozduğu da bir gerçektir. pprof, "neyi düzelteceğim?" sorusunu disipline eden bir araçtır.

Optimizasyon öncesi ve sonrası p99 gecikme çubuklarını yan yana gösteren karşılaştırma grafiği

Profil Sonuçlarını Karşılaştırma

pprof'un en az kullanılan ama en güçlü özelliklerinden biri profil karşılaştırmasıdır. İki ayrı profil dosyasını -base bayrağıyla birbirinden çıkararak değişimi tek bir görünümde okuyabilirsiniz:

go tool pprof -base before.pprof after.pprof

Bu yaklaşım, bir PR'ın gerçekten iyileştirme mi yoksa regresyon mu getirdiğini sayısal olarak gösterir. Özellikle release öncesi smoke benchmark'ları varsa, profil karşılaştırması "iyimser tahmin" yerine kanıt sunar. Go ekosisteminde benchmark yazımı ve go test -bench akışını derinleştirmek için Go eğitimi kaynağını inceleyebilirsiniz.

Pratik Bir Profilleme Akışı

pprof'u tek seferlik bir araç olarak değil, bir disiplin olarak işletmek için akış şu şekilde kurulabilir:

  1. Servise net/http/pprof bağlanır, ama uç nokta dış dünyaya kapalı tutulur.
  2. Load test sırasında 30 saniyelik CPU ve heap profili alınır.
  3. Top listesindeki ilk üç sıcak fonksiyon listelenip list ile satır bazlı incelenir.
  4. Tek bir değişiklik yapılır, değişiklik sonrası yeni profil çekilir.
  5. -base ile karşılaştırma yapılır, kazanım belgelenir.
  6. Goroutine sayısı zaman içinde izlenir; ani artışlar sızıntı işaretidir.

Birçok ekibin yaptığı hata, optimizasyonu sezgisel olarak yapıp sonucu ölçmemektir. pprof'un asıl değeri, "şu değişiklik şu kadar fayda getirdi" cümlesinin sayılarla bitebilmesidir — performans işinin diğer her tarafını bu cümle taşır.