Trafik ani artışları için Go bellek profilleme: pprof adım adım kılavuz
Go bellek profilleme ani trafik artışlarını yönetmenize yardımcı olur. JSON, DB taramaları ve middleware'deki tahsis yoğun noktalarını bulmak için uygulamalı bir pprof rehberi.

Trafikte ani artışların bir Go servisine yaptığı bellek etkileri
Prodüksiyonda bir “bellek sıçraması” nadiren tek bir sayının yükselmesi demektir. RSS (process memory) hızlıca tırmanırken Go heap neredeyse sabit kalabilir, ya da heap GC çalıştıkça keskin dalgalarla büyüyüp düşebilir. Aynı zamanda, runtime daha fazla temizleme yaptığı için gecikmeler genelde artar.
Metriklerde görülen ortak desenler:
- RSS beklenenden hızlı yükselir ve bazen sıçramadan sonra tam olarak düşmez
- Heap in-use artar, sonra GC daha sık çalıştıkça keskin döngülerle düşer
- Tahsisat hızı artar (saniyede tahsis edilen byte)
- Her duraklama küçük olsa bile GC duraklama süresi ve GC CPU zamanı artar
- İstek gecikmeleri yükselir ve kuyruk (tail) gecikme değerleri gürültülü olur
Trafik sıçramaları, isteğe düşen tahsisatları büyütür çünkü “küçük” israf yükle lineer ölçeklenir. Bir istek fazladan 50 KB ayırıyorsa (geçici JSON buffer'ları, satır başına scan nesneleri, middleware context verisi), 2.000 RPS'te allocator'a saniyede yaklaşık 100 MB veriyorsunuz demektir. Go çok şey kaldırabilir, ama GC yine de bu kısa ömürlü nesneleri izleyip serbest bırakmak zorundadır. Tahsisat temizlemeyi geçerse, heap hedefi büyür, RSS takip eder ve bellek sınırlarına takılabilirsiniz.
Belirtiler tanıdıktır: orkestratörünüzden OOM kill'ler, ani gecikme zıplamaları, GC'de daha fazla zaman harcanması ve CPU sabitlenmediği hâlde servis "meşgul" gözükmesi. Ayrıca GC thrash de olabilir: servis ayakta kalır ama sürekli tahsis edip collect ettiği için verim düşer tam ihtiyaç duyduğunuz anda.
pprof hızlıca bir soruya yanıt bulmaya yardımcı olur: hangi kod yolları en çok tahsis ediyor ve bu tahsisatlar gerekli mi? Bir heap profili şu an tutulana işaret eder. Tahsisata odaklı görünümler (ör. alloc_space) ise hangi nesnelerin yaratılıp atıldığını gösterir.
pprof her bir RSS baytını açıklamaz. RSS Go heap'ten daha fazlasını içerir (stack'ler, runtime meta, OS eşlemeleri, cgo tahsisleri, parçalanma). pprof, Go kodunuzdaki tahsis yoğun noktalarını işaret etmekte iyidir, konteyner düzeyindeki kesin bellek toplamını kanıtlamakta değil.
pprof'ı güvenli şekilde kurma (adım adım)
pprof HTTP uç noktaları olarak kullanmak en kolay yoldur, ama bu uç noktalar servisinize dair çok şey açığa çıkarabilir. Bunları herkese açık bir API değil, yönetici özelliği olarak düşünün.
1) pprof uç noktalarını ekleyin
Go'da en basit kurulum pprof'ı ayrı bir admin sunucusunda çalıştırmaktır. Bu, profilleme rotalarını ana router ve middleware'den uzak tutar.
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
// Admin only: bind to localhost
log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
}()
// Your main server starts here...
// http.ListenAndServe(":8080", appHandler)
select {}
}
Eğer ikinci bir port açamıyorsanız, pprof rotalarını ana sunucuya monte edebilirsiniz fakat bunu kazara açmak daha kolaydır. Ayrı bir admin portu varsayılan olarak daha güvenlidir.
2) Dağıtmadan önce kilitleyin
Başlangıçta yanlış yapması zor kontrollerle başlayın. Localhost'a bağlamak, uç noktaların internetten erişilememesini sağlar (birisi ayrıca o portu açmadığı sürece).
Hızlı kontrol listesi:
- pprof'ı ana kullanıcı portunda değil admin portunda çalıştırın
- Prodüksiyonda
127.0.0.1(veya özel bir arayüz) üzerine bağlayın - Ağ kenarında bir allowlist ekleyin (VPN, bastion veya iç subnet)
- Kenar doğrulaması mümkünse auth isteyin (basic auth veya token)
- Gerçekten kullanacağınız profilleri çekebildiğinizi doğrulayın: heap, allocs, goroutine
3) Güvenli şekilde build ve rollout yapın
Değişikliği küçük tutun: pprof ekleyin, gönderin ve yalnızca beklediğiniz yerden erişilebilir olduğunu doğrulayın. Staging varsa, orada önce yük simüle edip heap ve allocs profili yakalayarak test edin.
Prodüksiyon için kademeli deploy (tek bir instance veya küçük bir trafik dilimi) yapın. Eğer pprof yanlış yapılandırıldıysa, patlama yarıçapı küçük kalır ve düzeltirsiniz.
Sıçrama sırasında doğru profilleri yakalama
Bir sıçrama sırasında tek bir anlık görüntü nadiren yeterlidir. Küçük bir zaman çizelgesi yakalayın: sıçramadan birkaç dakika önce (baseline), sıçrama sırasında (impact) ve birkaç dakika sonra (recovery). Bu, gerçek tahsis değişikliklerini normal ısınma davranışından ayırmayı kolaylaştırır.
Sıçramayı kontrol edilebilir yük ile çoğaltabiliyorsanız, production'a mümkün olduğunca yakın eşleştirin: istek karışımı, payload boyutları ve eşzamanlılık. Küçük isteklerin sıçraması büyük JSON yanıtların sıçramasından çok farklı davranır.
Hem bir heap profili hem de tahsisata odaklı bir profil alın. Farklı sorulara yanıt verirler:
- Heap (inuse) şu anda canlı tutulanları gösterir
- Tahsisatlar (alloc_space veya alloc_objects) sık oluşturulup serbest bırakılanları gösterir
Pratik bir yakalama örüntüsü: bir heap profili alın, sonra bir allocs profili alın, ve 30–60 saniye sonra tekrarlayın. Sıçrama sırasında iki nokta, şüpheli yolun sabit mi yoksa hızlanıyor mu olduğunu görmenize yardımcı olur.
# examples: adjust host/port and timing to your setup
curl -o heap_during.pprof "http://127.0.0.1:6060/debug/pprof/heap"
curl -o allocs_30s.pprof "http://127.0.0.1:6060/debug/pprof/allocs?seconds=30"
pprof dosyalarına ek olarak, GC'nin o anda ne yaptığını açıklayabilecek birkaç runtime istatistiğini kaydedin. Heap büyüklüğü, GC sayısı ve duraklama süresi genelde yeterlidir. Her yakalama zamanında kısa bir log satırı bile “tahsisatlar arttı” ile “GC sürekli çalışmaya başladı”yı ilişkilendirmenize yardımcı olur.
Olay notları tutun: build versiyonu (commit/tag), Go versiyonu, önemli flag'ler, konfigürasyon değişiklikleri ve hangi trafiğin olduğu (endpoint'ler, tenant'lar, payload boyutları). Bu detaylar, profilleri karşılaştırdığınızda istek karışımının değiştiğini farkettiğinizde önem kazanır.
Heap ve tahsisat profillerini nasıl okunur
Heap profili görünümü bağlama göre farklı sorulara yanıt verir.
Inuse space yakalama anında hâlâ bellekte tutulanları gösterir. Sızıntılar, uzun ömürlü cache'ler veya nesneleri geride bırakan istekler için kullanın.
Alloc space (toplam tahsisatlar) zaman içinde nelerin oluşturulduğunu, hatta hızlıca serbest bırakılanları gösterir. Sıçramalar GC işini artırıyorsa, gecikme atlamaları veya OOM'lar için buna bakın.
Sampling önemlidir. Go her tahsisi kaydetmez; tahsisleri örnekler (kontrol: runtime.MemProfileRate) bu yüzden küçük, sık tahsisatlar düşük temsil edilebilir ve sayılar tahmindir. Yine de en büyük failerler, özellikle sıçrama koşullarında öne çıkar. Mükemmel muhasebe yerine eğilimlere ve en çok katkıda bulunanlara bakın.
En kullanışlı pprof görünümleri:
- top: inuse veya alloc'ta kimin domine ettiğine hızlı bakış (flat ve cumulative her ikisini de kontrol edin)
- list
: sıcak bir fonksiyon içindeki satır bazlı tahsis kaynakları - graph: oraya nasıl ulaşıldığını gösteren çağrı yolları
Diff yapmak pratik olan yerdir. Normal trafik (baseline) profili ile sıçrama profilini karşılaştırın; böylece arka plan gürültüsü yerine değişenleri vurgularsınız.
Bulguları büyük refaktora gitmeden önce küçük bir değişiklikle doğrulayın:
- Sıcak yolda bir buffer'ı yeniden kullanın (veya küçük bir
sync.Poolekleyin) - İstek başına oluşturulan nesne sayısını azaltın (ör. JSON için ara map'ler oluşturmayın)
- Aynı yük altında yeniden profilleyin ve diff'in beklendiği gibi küçüldüğünü doğrulayın
Sayısal göstergeler doğru yönde hareket ediyorsa, gerçek bir neden bulmuşsunuzdur; sadece korkutucu bir rapor değil.
JSON kodlamasında tahsis yoğun noktalarını bulma
Sıçramalar sırasında JSON işlemleri her istekte çalıştığı için büyük bir bellek faturası olabilir. JSON sıcak noktaları genelde GC'yi zorlayan çok sayıda küçük tahsisat olarak görünür.
pprof'ta dikkat edilmesi gereken kırmızı bayraklar
Heap veya tahsis görünümü encoding/json'e işaret ediyorsa, içine ne verdiğinize dikkat edin. Aşağıdaki kalıplar genelde tahsisatı şişirir:
- Yanıtlar için
map[string]any(veya[]any) kullanmak yerine tipli struct'lar kullanmamak - Aynı nesneyi birden çok kez marshal etmek (ör. loglamak ve ayrıca döndürmek)
- Prodüksiyonda
json.MarshalIndentile pretty print yapmak - Ara string'ler oluşturarak JSON inşa etmek (
fmt.Sprintf, string birleştirme) ve sonra marshal etmek - Büyük
[]byte'ı API'yle uyum içinstring'e dönüştürmek (veya tersi) sadece uyum için
json.Marshal her zaman tam çıktı için yeni bir []byte ayırır. json.NewEncoder(w).Encode(v) genelde o tek büyük buffer'ı kaçınır çünkü bir io.Writer'a yazar, ama eğer v any, map'lar veya pointer-ağırlıklı yapılarla doluysa içsel tahsisatlar yapabilir.
Hızlı düzeltmeler ve deneyler
Yanıt şekliniz için tipli struct'larla başlayın. Bunlar reflection işini azaltır ve alan başına interface boxing'ten kaçınır.
Sonra gereksiz isteğe özel geçicileri kaldırın: bytes.Buffer'ı sync.Pool ile tekrar kullanın (dikkatli), prodüksiyonda indent yapmayın ve log için tekrar-marshal etmeyin.
JSON'in fail olduğunu doğrulayan küçük deneyler:
- Sıcak bir endpoint için
map[string]anyyerine struct kullanıp profilleri karşılaştırın Marshalyerine yanıt yazıcısına doğrudan yazanEncoder'a geçinMarshalIndentveya debug formatını kaldırın ve aynı yük altında yeniden profilleyin- Değişmemiş önbelleğe alınmış yanıtlar için JSON kodlamasını atlayın ve düşüşü ölçün
Sorgu taramasında tahsis yoğun noktalarını bulma
Bir sıçrama sırasında bellek yükseldiğinde, veritabanı okumaları sık karşılaşılan bir sürpriz olabilir. SQL süresine odaklanmak kolaydır, ama scan adımı satır başına ciddi tahsisat yapabilir, özellikle esnek tiplere tarıyorsanız.
Yaygın failerler:
interface{}(veyamap[string]any) içine scan yapmak ve sürücünün tipleri belirlemesine izin vermek- Alan başına
[]byte'ıstring'e dönüştürmek - Büyük sonuç kümelerinde
sql.NullString,sql.NullInt64gibi nullable sarmalayıcılar kullanmak - Her zaman ihtiyaç duymadığınız büyük text/blob kolonlarını çekmek
Sessizce belleği yakan bir kalıp, satır verilerini geçici değişkenlere tarayıp sonra gerçek struct'a kopyalamaktır (veya her satır için bir map oluşturmak). Eğer doğrudan somut alanlara tarayabiliyorsanız, ekstra tahsisatlardan ve tip kontrollerinden kaçınırsınız.
Batch boyutu ve sayfalama bellek şeklini değiştirir. 10.000 satırı bir slice'a çekmek slice büyümesi için ve her satır için tahsisatlar yaratır. Handler sadece bir sayfa gerekiyorsa, sorguya sayfalama ekleyin ve sayfa boyutunu sabit tutun. Çok satırı işlemeniz gerekliyse, hepsini saklamak yerine akışla işleyin ve küçük özetler toplayın.
Büyük text alanları özel dikkat ister. Birçok sürücü text'i []byte olarak döndürür. Bunu string'e dönüştürmek veriyi kopyalar, dolayısıyla her satır için bunu yapmak tahsisatları patlatabilir. Değere sadece bazen ihtiyacınız varsa dönüştürmeyi erteleyin veya o endpoint için daha az kolon tarayın.
Sürücünün mi yoksa kodunuzun mı daha çok tahsis ettiğini doğrulamak için profilde neyin baskın olduğuna bakın:
- Frame'ler sizin mapping kodunuza işaret ediyorsa, tarama hedefleri ve dönüştürmelere odaklanın
- Frame'ler
database/sqlveya sürücüye işaret ediyorsa önce satır/kolon sayısını azaltın, sonra sürücüye özgü seçenekleri düşünün - Hem
alloc_spacehem dealloc_objects'u kontrol edin; birçok küçük tahsis birkaç büyük tahsisten daha kötü olabilir
Örnek: “siparişleri listele” endpoint'i SELECT * ile []map[string]any'ye tarama yapıyordu. Sıçrama sırasında her istek binlerce küçük map ve string oluşturuyordu. Sorguyu sadece gerekli kolonları seçecek ve []Order{ID int64, Status string, TotalCents int64} gibi tipli struct'lara tarayacak şekilde değiştirmek genelde tahsisatı hemen düşürür. AppMaster tarafından üretilen Go backend'lerde de benzer şekilde; sıcak nokta genellikle sonuç verisini nasıl şekillendirdiğinizde ve taradığınızda olur, veritabanı kendisi değil.
Middleware kalıpları ve istek başına gizli tahsisatlar
Middleware ucuzmuş gibi gelir çünkü “sadece bir sarmalayıcıdır”, ama her istekte çalışır. Sıçrama sırasında küçük istek başına tahsisatlar hızla toplanır ve tahsisat hızında yükseliş olarak görünür.
Logging middleware yaygın bir kaynaktır: string formatlama, alan map'leri oluşturma veya daha güzel çıktı için header'ları kopyalama. Request ID yardımcıları ID üretirken tahsisat yapabilir, onu string'e çevirip context'e ekleyebilir. Hatta context.WithValue her istek için yeni nesneler (veya yeni string'ler) depoluyorsa tahsisat yaratır.
Sıkıştırma ve gövde (body) işleme başka bir sık karşılaşılan suçludur. Middleware isteğin tüm gövdesini “peep” veya doğrulama için okursa, her istek için büyük bir buffer ile sonuçlanabilirsiniz. Gzip middleware her seferinde yeni okuyucu ve yazıcı oluşturuyorsa ve buffer'ları yeniden kullanmıyorsa çok tahsisat yaratabilir.
Auth ve session katmanları benzer olabilir. Her istek token parse ediyorsa, base64 çözüyorsa veya session blob'larını yeni struct'lara yüklüyorsa hafif handler iş yükünde bile sabit churn elde edersiniz.
Tracing ve metrikler dinamik etiketler oluşturduğunda beklenenden fazla tahsisat yapabilir. Route isimleri, user-agent veya tenant ID'lerini yeni string'lere eklemek klasik gizli maliyettir.
"Binlerce küçük yara" olarak görünen kalıplar:
- Her istek için
fmt.Sprintfile log satırları oluşturmak ve yenimap[string]anydeğerleri üretmek - Loglama veya imzalama için header'ları yeni map/slice'lara kopyalamak
- Yeni gzip buffer'ları ve reader/writer'ları oluşturmaktansa pool kullanmamak
- Yüksek kardinaliteli metrik etiketleri üretmek (çok sayıda benzersiz string)
- Her istekte context'e yeni struct'lar koymak
Middleware maliyetini izole etmek için iki profil karşılaştırın: tam zincir açıkken ve middleware geçici olarak devre dışı veya no-op ile değiştirilmiş hal. Basit bir test, neredeyse tahsisatsız olması gereken bir health endpoint'idir. Eğer /health sıçrama sırasında fazla tahsisat yapıyorsa, sorun handler değil middleware demektir.
AppMaster tarafından üretilen Go backend'lerde de aynı kural geçerli: loglama, auth, tracing gibi çapraz-kesme özelliklerini ölçülebilir tutun ve istek başına tahsisatı bir bütçe olarak denetleyin.
Genelde işe yarayan düzeltmeler
Heap ve allocs görünümlerini aldıktan sonra, isteğe düşen tahsisatları azaltacak değişikliklere öncelik verin. Amaç zekice numaralar değil; sıcak yolun daha az kısa ömürlü nesne üretmesini sağlamak, özellikle yük altında.
Güvenli, sıkıcı ama etkili başlangıçlar
Boyutlar öngörülebilirse önceden ayırın. Bir endpoint genelde ~200 öğe döndürüyorsa slice'ı kapasite 200 ile oluşturun ki birkaç kez büyüyüp kopyalanmasın.
Sıcak noktalarda string inşa etmekten kaçının. fmt.Sprintf kullanışlıdır ama genelde tahsisat yapar. Loglama için yapılandırılmış alanları tercih edin ve uygun yerlerde küçük buffer'ları yeniden kullanın.
Büyük JSON yanıtları üretiyorsanız onları tek büyük []byte veya string oluşturmak yerine akışla gönderin. Yaygın bir sıçrama deseni: istek gelir, büyük bir body okursunuz, büyük bir yanıt oluşturursunuz, bellek GC yakalayana kadar zıplar.
Genelde profil karşılaştırmasında net görünen hızlı değişiklikler:
- Boyut aralığını biliyorsanız slice ve map'ları önceden ayırın
fmtağırlıklı formatlamayı istek işleme hattında daha ucuz alternatiflerle değiştirin- Büyük JSON yanıtlarını akışla yazın (encode'u direkt response writer'a yapın)
- Aynı şekilli yeniden kullanılabilir nesneler (buffer, encoder) için
sync.Poolkullanın ve tutarlı şekilde geri verin - En kötü durumları sınırlamak için istek limitleri koyun (body boyutu, payload, sayfa boyutu)
sync.Pool'ı dikkatli kullanın
sync.Pool aynı şeyi tekrar tekrar allocate etmenin faydalı olduğu durumlarda yardımcı olur (ör. her istek için bytes.Buffer). Ancak tahmin edilemeyen boyutlu nesneleri pool'lamak ya da resetlemeyi unutmak büyük backing array'lerin canlı kalmasına sebep olarak zarar verebilir.
Aynı iş yüküyle önce ve sonra ölçün:
- Sıçrama penceresinde bir allocs profili yakalayın
- Her seferinde tek bir değişiklik uygulayın
- Aynı istek karışımını yeniden çalıştırıp toplam allocs/op'u karşılaştırın
- Sadece belleğe bakmayın, kuyruk gecikmesini de izleyin
AppMaster tarafından oluşturulan Go backend'lerde, bu düzeltmeler handler'lar, entegrasyonlar ve middleware çevresindeki özel kod için de geçerlidir. Sıçrama kaynaklı tahsisatlar genelde orada saklanır.
Yaygın pprof hataları ve yanlış alarmlar
Yanlış şeyi optimize etmek bir günü boşa harcamanın en hızlı yoludur. Servis yavaşsa önce CPU'ya bakın. Servis OOM ile ölüyorsa önce heap'e bakın. Servis ayakta kalıyor ama GC sürekli çalışıyorsa tahsisat hızına ve GC davranışına bakın.
Bir diğer tuzak sadece “top”a bakıp işi bitmiş saymaktır. “Top” bağlamı saklar. Daima çağrı yığınlarını (veya flame graph) inceleyin; allocator'ı çağıranın kim olduğunu görün. Düzeltme genelde sıcak fonksiyondan bir veya iki çerçeve yukarıdadır.
Ayrıca inuse ile churn'ı karıştırmamaya dikkat edin. Bir istek 5 MB kısa ömürlü nesne ayırıp ek GC tetikler ve sonunda sadece 200 KB inuse bırakabilir. Sadece inuse'e bakarsanız churn'u kaçırırsınız. Sadece toplam tahsisata bakarsanız, gerçekte kalıcı olmayan ve OOM riski olmayan bir şeyi optimize ediyor olabilirsiniz.
Kod değiştirmeden önce hızlı kontroller:
- Doğru görünümde olduğunuza emin olun: retention için heap inuse, churn için alloc_space/alloc_objects
- Sadece fonksiyon isimlerine değil, yığınlara bakın (
encoding/jsongenelde bir semptomdur) - Trafiği gerçekçi şekilde yeniden üretin: aynı endpoint'ler, payload boyutları, header'lar, eşzamanlılık
- Baseline ve spike profili yakalayın, sonra diff alın
Gerçekçi olmayan yük testleri yanlış alarmlara neden olur. Testiniz küçük JSON body'ler gönderiyor ama prod 200 KB payload gönderiyorsa yanlış yolu optimize edersiniz. Test tek bir satır dönüyorsa 500 satırla ortaya çıkan tarama davranışını asla göremezsiniz.
Gürültüyü kovalamayın. Eğer bir fonksiyon sadece sıçrama profilinde görünüyorsa (baseline'da değil) güçlü bir ipucudur. Eğer her ikisinde de aynı seviyede görünüyorsa normal arka plan işi olabilir.
Gerçekçi bir olay örnek yürüyüşü
Pazartesi sabahı bir promosyon gönderilir ve Go API'niz normal trafiğin 8 katı trafik almaya başlar. İlk belirti çökme değildir. RSS tırmanır, GC daha meşgul olur ve p95 gecikme zıplar. En sıcak endpoint GET /api/orders çünkü mobil uygulama her ekranda bunu yeniliyor.
Sakin bir andan (baseline) ve sıçrama sırasında bir snapshot alırsınız. Karşılaştırmanın adil kalması için aynı tip heap profilini her iki zamanda da yakalayın.
İzlenecek akış:
- Baseline heap profili alın ve o anki RPS, RSS ve p95 gecikmeyi not edin
- Sıçrama sırasında başka bir heap profili ve aynı 1–2 dakikalık pencerede bir allocs profili alın
- İki profil arasındaki en büyük tahsisat yapanları karşılaştırın ve en çok büyüyenlere odaklanın
- En büyük fonksiyondan çağıranlarına doğru gidin, handler yolunuza ulaşana dek
- Tek küçük bir değişiklik yapın, tek bir instance'a deploy edin ve yeniden profilleyin
Bu örnekte, sıçrama profili yeni tahsisatların çoğunun JSON kodlamasından geldiğini gösterdi. Handler satırları map[string]any olarak inşa ediyor, sonra bir map dilimini json.Marshal ediyordu. Her istek çok sayıda kısa ömürlü string ve interface değeri oluşturuyordu.
En küçük güvenli düzeltme haritalar inşa etmeyi bırakmaktı. Veritabanı satırlarını doğrudan tipli struct'lara tarayıp o dilimi encode ettiler. Başka hiçbir şey değişmedi: aynı alanlar, aynı yanıt şekli, aynı status kodları. Değişikliği bir instance'a aldıktan sonra JSON yolundaki tahsisatlar düştü, GC süresi azaldı ve gecikme stabil hale geldi.
Sadece sonra değişikliği kademeli olarak tüm servise yayarsınız ve bellek, GC ve hata oranlarını izlersiniz. AppMaster gibi kodsuz platformlarda servis inşa ediyorsanız, yanıt modellerini tipli ve tutarlı tutmak gizli tahsisat maliyetlerinden kaçınmaya yardımcı olur.
Bir sonraki bellek sıçramasını önlemek için sonraki adımlar
Sıçramayı stabilize ettikten sonra bir sonraki olayı sıkıcı hale getirin. Profillemeyi tekrarlanabilir bir tatbikat gibi görün.
Ekip yorgunken bile takip edebilecek kısa bir runbook yazın. Ne yakalanacağı, ne zaman yakalanacağı ve bilinen iyi baseline ile nasıl karşılaştırılacağı açıkça yazsın. Pratik olsun: kesin komutlar, profillerin nereye gideceği ve en iyi alıcılar için "normal" ne demek.
OOM'a varmadan önce tahsis baskısı için hafif izleme ekleyin: heap büyüklüğü, saniye başına GC döngüleri ve istek başına tahsis edilen byte'lar. "İstek başına tahsisler %30 arttı" gibi erken uyarılar sert bellek alarmından daha faydalıdır.
CI'da temsil edici bir endpoint üzerinde kısa bir yük testi çalıştırıp değişiklikleri daha erken tespit edin. Küçük yanıt değişiklikleri ekstra kopyalar tetiklerse tahsisleri iki katına çıkarabilir ve bunu üretim trafiğinden önce bulmak daha iyidir.
Eğer üretilmiş bir Go backend çalıştırıyorsanız, kaynağı dışa aktarın ve aynı şekilde profilleyin. Üretilmiş kod yine Go kodudur ve pprof gerçek fonksiyonlara ve satırlara işaret edecektir.
Gereksinimler sık sık değişiyorsa, AppMaster (appmaster.io) uygulama evrilirken temiz Go backend'leri yeniden oluşturmak ve dışa aktarılan kodu gerçekçi yük altında profillemek için pratik bir yol olabilir.
SSS
Bir trafik sıçraması genellikle beklenenden çok daha fazla tahsisat hızına yol açar. Her istekte oluşan küçük geçici nesneler bile RPS ile lineer olarak toplanır; bu da GC'nin daha sık çalışmasına ve canlı heap büyük olmasa bile belleğin zıplamasına neden olabilir.
Heap metrikleri Go tarafından yönetilen belleği izler, ama RSS çok daha fazlasını kapsar: goroutine yığınları, runtime meta verisi, OS eşlemeleri, parçalanma ve bazı cgo/yerel tahsisatlar. Sıçramalar sırasında RSS ile heap farklı hareket edebilir; kesin eşleşme aramak yerine pprof ile tahsis yoğun noktalarını bulmak daha faydalıdır.
Retention (bir şeylerin yaşamaya devam etmesi) şüphesi varsa önce bir heap profili alın; churn (kısa ömürlü çok sayıda nesne) şüphesi varsa allocs/alloc_space gibi tahsis odaklı profilleri alın. Trafik sıçramalarında genellikle churn gerçek sorundur çünkü GC CPU süresini ve kuyruk gecikmesini yükseltir.
En basit güvenli kurulum, pprof'u 127.0.0.1 üzerinde bağlı olan ayrı bir admin sunucusunda çalıştırmaktır ve sadece iç erişimle ulaşılmasını sağlamaktır. pprof servis içi detayları açığa çıkarabileceği için onu kamuya açık bir API gibi değil, yönetici arayüzü gibi davranın.
Kısa bir zaman çizgisi yakalayın: sıçramadan birkaç dakika önce bir profil (baseline), sıçrama sırasında bir profil (impact) ve sonrasında bir profil (recovery). Bu, hangi değişikliklerin gerçekten olayla ilişkili olduğunu ayırmanıza yardımcı olur.
inuse görünümü yakalama anında gerçekten bellekte tutulanları bulmak için kullanılır; alloc_space ise yoğun olarak hangi nesnelerin oluşturulduğunu bulmak için kullanılır. Sıçramalarda GC parçalanma ve churn kritik olduğundan her ikisini de anlamak önemlidir.
Eğer encoding/json profilde baskın çıkıyorsa, genellikle suçlu paket değil veri yapınızdır. map[string]any yerine tipli struct'lar kullanmak, json.MarshalIndent'i kaldırmak ve ara stringler oluşturmaktan kaçınmak genelde tahsisatı hemen düşürür.
Veritabanı taraması sırasında interface{} veya map[string]any gibi esnek hedeflere taramak, []byte'ı string'e dönüştürmek ve çok fazla satır/kolon çekmek her istekte büyük tahsisatlar yaratabilir. Sadece gerektiği kadar kolon seçmek, sayfalamak ve doğrudan somut struct alanlarına taramak yüksek etkili düzeltmelerdir.
Middleware her istekte çalıştığı için küçük tahsisatlar bile yük altında hızla toplanır. Yeni stringler oluşturmak, yüksek kardinaliteli etiketler üretmek, her istek için gzip okuyucu/yazıcı oluşturmak veya context'e her istekte yeni bir nesne koymak bu tür gizli churn kaynaklarıdır.
Evet. Oluşturulmuş veya el yazısı fark etmeksizin aynı Go kodu için aynı profilleme yaklaşımı geçerlidir. Oluşturulan backend kaynağını dışa aktarıp profilleyerek tahsis yapan çağrı yollarını tespit edebilir ve modelleri, handler'ları ve çapraz-kesme mantığını buna göre düzeltebilirsiniz.


