# Goroutine sızıntılarını uzun süre çalışan bir Go servisinde nasıl tespit edip önlerim?

> NumGoroutine()'i dışa açıp zirvede pprof goroutine dökümü alın, binlerce kopyalı park etmiş stack'in spawn noktasını ctx.Done()'a uydurun, goleak ekleyin.

- Soruldu: 2026-07-06
- Yanıtlandı: 2026-07-10
- Soran: Kerem
- Etiketler: go, concurrency
- Kaynak: https://muhammetsafak.com/tr/sor-bakalim/goroutine-sizintilarini-uzun-sure-calisan-bir-go-servisinde-nasil-tespit-edip/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
**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?


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.

## Kısa cevap

Ö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. Buradaki channel ve `select` sözleşmesinin temelini [Go'da eşzamanlılık yazısında](/tr/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/) anlatmıştım; sızıntı, o sözleşmenin tutulmadığı yerde ortaya çıkar.

## Neden

1. **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ş.
2. **Takılmış bir ağ okuması goroutine'i süreç ölene dek tutar.** Context aşağı taşınmadığında ve ağ çağrılarında timeout olmadığında `select` hiçbir zaman geri dönmez; goroutine park eder ve orada kalır.
3. **Sınırsız spawn modelinde sızıntının tavanı yoktur.** Event başına sınırsız `go func()` fırlatan bir servis, sızıntı başladığında hiçbir üst sınıra çarpmaz — sayı yükle birlikte tırmanmaya devam eder.

## Ne yapmalı

1. **Ö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.
2. **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 da `debug=1` (gruplanmış) çıktısını alın. Binlerce kopyası olan stack, hepsi aynı channel recv/send veya `select` üzerinde park etmiş hâlde, tam da sızan noktadır.

   ```bash
   # Üretimde zirve anında park etmiş stack'i bul:
   curl -s 'http://localhost:6060/debug/pprof/goroutine?debug=2' | less
   ```

3. **Context'i her yere taşıyın.** `context.Context`'i aşağıya geçirin, `select` iç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.

   ```go
   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)
           }
       }
   }
   ```

4. **Sızıntıyı test edin.** `go.uber.org/goleak`'i `TestMain` iç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.
5. **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.

**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

- [Go'da eşzamanlılık: goroutine ve channel pratiği](/tr/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/) — Blog
- [Kanal tampon boyutu seçimi backpressure sorunlarını gizler mi, nasıl karar vermeliyim?](https://muhammetsafak.com/tr/sor-bakalim/kanal-tampon-boyutu-backpressure-sorunlarini-gizler-karar-vermeliyim/) — Sor Bakalım
- [Birbirine bağımlı birden fazla işi, ilk hata hepsini iptal edecek şekilde errgroup ile mi yürütmeliyim?](https://muhammetsafak.com/tr/sor-bakalim/birbirine-bagimli-birden-fazla-isi-ilk-hata-hepsini-iptal-edecek-sekilde/) — Sor Bakalım
- [İç içe geçmiş servis çağrılarında context iptalini doğru şekilde nasıl yayarım?](https://muhammetsafak.com/tr/sor-bakalim/ic-ice-gecmis-servis-cagrilarinda-context-iptalini-dogru-sekilde-nasil-yayarim/) — Sor Bakalım
