Masalah Utama: Memory Leak Worker Queue Go Fiber saat Traffic Tinggi

Pada layanan Go Fiber yang menggunakan worker queue untuk menangani job paralel, memory leak muncul ketika worker menampung dan mengeksekusi pekerjaan lebih cepat daripada kapasitasnya. Gejala paling langsung adalah penggunaan heap terus naik walaupun jumlah request stabil, sementara latency worker dan GC pause time memburuk. Untuk mengatasi dan mencegahnya, kita harus menjawab tiga hal: apa yang bocor, mengapa goroutine atau channel tidak dilepas, dan bagaimana menutup siklus data secara benar.

Studi Kasus Debugging

Tim kami menangani kasus backend yang memproses batch data melalui queue di Go Fiber. Awalnya muncul error out-of-memory di lingkungan staging saat throughput mencapai 600 req/detik. Request tidak langsung gagal, tapi waktu respons membengkak, heap live terus naik, dan goroutine dump menunjukkan ribuan worker menunggu data di channel.

Gejala Utama

  • Heap naik terus tanpa turun walaupun load tidak meningkat.
  • GC triggered jadi sering dan memperpanjang pause time.
  • Worker queue tetap memegang referensi data lama, sehingga hard limit Go runtime terlampaui.

Langkah Reproduksi Singkat

  1. Siapkan endpoint yang menerima payload ukuran besar dan menaruhnya ke task channel.
  2. Jalankan benchmark load dengan hey atau wrk hingga queue backlog bertambah.
  3. Observasi heap dengan pprof atau runtime.ReadMemStats.

Dengan setup ini, heap profile secara konsisten menunjukkan objek TaskData tetap hidup padahal seharusnya dilepas.

Identifikasi Penyebab dan Observability

Heap Profiling dan Monitoring GC

Kami mengaktifkan profiling heap melalui net/http/pprof dan mengumpulkan data setiap 30 detik. Graf heap allocation menunjukkan dominasi struktur jobPayload serta slice buffer yang terus tumbuh. Monitoring GC (melalui runtime.MemStats.NumGC) memperlihatkan frekuensi GC naik drastis, menandakan banyak objek yang tidak mencapai threshold death.

Menelusuri Root Cause

Hasil profiling memperlihatkan tiga penyebab utama:

  • Goroutine tidak terbatas: setiap request memulai goroutine baru yang membaca dari channel tanpa batas, sehingga saat beban naik goroutine menumpuk.
  • Channel tak tertutup: worker channel tidak di-close saat shutdown; job pada channel tetap dirujuk sampai GC.
  • Data retention: slice buffer atau map untuk caching job disimpan lebih lama dari siklus eksekusi karena tidak dibersihkan.

Juga terungkap bahwa beberapa goroutine memegang referensi ke struktur global jobState sampai job selesai, tetapi ketika worker gagal, data ini tidak di-nil-kan.

Perbaikan Praktis dan Refactor

1. Desain Ulang Worker Queue

Kita refactor queue menjadi konfigurasi bounded worker pool yang hanya memulai MAX_WORKERS goroutine. Task disimpan di channel buffered dengan saturasi limit. Contoh pembentukan worker:

const maxWorkers = 50
func startWorkerPool(tasks <-chan Job) {
    for i := 0; i < maxWorkers; i++ {
        go func(id int) {
            for job := range tasks {
                process(job)
            }
        }(i)
    }
}

Pembedaan ini memastikan goroutine tidak tumbuh tanpa batas dan pekerjaan ditahan pada channel ketika worker penuh.

2. Batasi Lifespan Goroutine dan Tangani Shutdown

Tambahkan context cancel dan sync.WaitGroup untuk memastikan goroutine keluar saat aplikasi dimatikan. Jangan lupa close(tasks) sehingga worker bisa menyelesaikan loop. Dengan cara ini, GC bisa melepaskan goroutine dan channel dari memori.

3. Validasi dan Bersihkan Data

Setelah job selesai, hapus referensi ke data besar: set payload.Data = nil atau gunakan pool buffer (sync.Pool) untuk meminimalkan alokasi konsisten. Jangan menyimpan data di map global tanpa pengecekan lifecycle agar tidak terjadi retention.

4. Tambahkan Monitoring dan Alert

Integrasikan metrik seperti len(tasks), runtime.NumGoroutine(), dan garbage collection pause time ke Prometheus atau Grafana. Alert saat goroutine mencapai threshold membantu menangkap lonjakan sebelum OOM.

Ringkasan Tindakan Preventif

  • Gunakan worker pool bounded agar goroutine tidak tak terbatas.
  • Tutup channel dan goroutine saat shutdown demi mendukung GC.
  • Kelola lifecycle data: bersihkan referensi besar segera setelah diproses.
  • Pantau heap, GC, dan jumlah goroutine secara rutin.
  • Lakukan stress test dengan scenario trafik tinggi untuk memastikan queue tetap stabil.

Dengan pendekatan ini, layanan Go Fiber bisa mengatasi memory leak worker queue saat beban tinggi tanpa mengorbankan throughput.