Goroutine sızıntılarını uzun süre çalışan bir Go servisinde nasıl tespit edip önlerim?
Soru
Go ile yazdığım bir veri ingestion servisi birkaç gün içinde yavaş yavaş on binlerce goroutine'e tırmanıyor. Servis event başına `go func()` ile iş fırlatıyor, channel'lar ve birkaç dış HTTP çağrısı var. Sorun şu: hangi spawn noktasının sızdırdığını bir türlü çıkaramıyorum. Bu sızıntıyı üretimde nasıl tespit ederim, kök nedenini nasıl bulurum ve bir daha olmaması için nasıl önlerim?
Cevap
Kısa cevap: Sayının istikrarlı biçimde tırmanması, bir channel/lock/IO üzerinde sonsuza dek bloklanmış ve iptal yolu olmayan goroutine’ler demektir. Önce ölçün ve dökümü alın, sonra spawn noktasının sözleşmesini düzeltin.
İçgüdü, artan sayıyı yüke bağlamaktır; ama sızıntının bir spike’ta olmayan bir imzası vardır: asla geri inmez. Koda dokunmadan önce bu ikisini ayırın, çünkü tamamen farklı çözümler gerektirirler.
- Önce sayıyı doğrulayın.
runtime.NumGoroutine()değerini bir metrik olarak dışa açın. Sayı yükle birlikte monoton artıyor ve boşta bile geri inmiyorsa, bu bir spike değil sızıntıdır. Bu ayrım tüm teşhisin temelidir. - pprof goroutine dökümü kanıttır.
net/http/pprof’u bağlayın ve/debug/pprof/goroutine?debug=2(ham stack’ler) ya dadebug=1(gruplanmış) çıktısını alın. Binlerce kopyası olan stack, hepsi aynı channel recv/send veyaselectüzerinde park etmiş hâlde, tam da sızan noktadır. - Kök neden neredeyse her zaman eksik iptaldir. Okuyucusu gitmiş bir channel’a yazmaya çalışan goroutine, hiç kapanmayan bir
for range ch, ya da context/timeout’suz bir HTTP çağrısı. Her goroutine’in bir çıkış yolu olmalı:ctx.Done(), kapanan bir channel veya sınırlı iş. - Context’i her yere taşıyın.
context.Context’i aşağıya geçirin,selectiçinde ona uyun ve tüm ağ çağrılarına timeout koyun (http.Client.Timeout, DB için query context). Takılmış bir ağ okuması üzerinde park eden goroutine, süreç ölene dek sızar. - Sızıntıyı test edin.
go.uber.org/goleak’iTestMainiçinde kullanın; test bitince hayatta kalan goroutine varsa test’i düşürsün. Yeni sızıntıları spawn noktası bazında, daha üretime çıkmadan yakalamanın en ucuz yolu budur. - Eşzamanlılığı sınırlayın. Event başına sınırsız
go func()yerine worker pool / semaphore kullanın. Sınırsız spawn modelindeki bir sızıntının tavanı yoktur; havuz, hasarı en baştan sınırlar.
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done(): // her goroutine'in çıkış yolu
return
case j, ok := <-jobs:
if !ok {
return
}
process(ctx, j)
}
}
}
# Üretimde zirve anında park etmiş stack'i bul:
curl -s 'http://localhost:6060/debug/pprof/goroutine?debug=2' | less
Sonuç: Ben olsam NumGoroutine()’i hemen bir dashboard’a ve pprof endpoint’ini servise bağlardım; zirvede bir goroutine?debug=2 dökümü alıp binlerce kopyalı park etmiş stack’i bulur, o spawn noktasının ctx.Done()’a uymasını sağlardım. Sonra goleak’i ekleyip bir daha regresyon olmamasını garantilerdim. Sızıntı bir “gizem” değildir; döküm size satırı gösterir.
İlgili Yazılar
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.