Pendahuluan
Memory Leak Goroutine Go Fiber dapat menurunkan stabilitas sistem backend secara perlahan: jumlah goroutine dan konsumsi heap terus naik tanpa batas, sementara respon HTTP menjadi lebih lambat. Artikel ini menjawab akar penyebabnya, bagaimana mereproduksi kasus nyata, serta langkah teknis untuk memperbaiki dan mencegah kebocoran tersebut.
Dengan fokus pada kasus channel processing queue yang tidak dilepas, kita akan membahas gejala awal, langkah debugging menggunakan pprof dan log, serta pola release channel yang memastikan goroutine tidak tertinggal. Pendekatan ini membantu tim backend memahami mengapa goroutine leak terjadi dan bagaimana memonitornya secara sistematis.
Gejala Awal dan Reproduksi
Gejala Awal
Tim observability pertama kali melihat peningkatan memori heap secara progresif dan log goroutine leak setelah update handler Fiber yang memproses event dalam queue internal. Heatmap heap menunjukkan aliran goroutine baru setiap beberapa detik tanpa ada yang selesai.
Respond time juga meningkat karena worker goroutine terus menunggu data di channel yang tidak pernah ditutup, memblokir GC dari mengembalikan memori.
Langkah Reproduksi
Reproduksi dilakukan dengan aplikasi Fiber sederhana dan worker queue yang berjalan di background goroutine. Berikut potongan kode ringkas reproduksi:
func main() {
app := fiber.New()
jobCh := make(chan string)
go func() {
for job := range jobCh {
process(job)
}
// Goroutine berhenti bila channel ditutup
}()
app.Post("/enqueue", func(c *fiber.Ctx) error {
select {
case jobCh <- c.Body():
default:
return c.Status(503).SendString("queue penuh")
}
return c.SendStatus(202)
})
log.Fatal(app.Listen(":3000"))
}
Tanpa penutupan channel saat server dimatikan, setiap restart menambah goroutine listener baru yang terus menunggu, memicu leak.
Investigasi: Profiling dan Log
Langkah utama adalah menjalankan pprof untuk menangkap heap snapshot dan melihat jumlah goroutine. Gunakan net/http/pprof sementara server berjalan untuk memeriksa stack trace goroutine yang menunggu di range jobCh.
Contoh perintah:
go test -run TestProfile -cpuprofile cpu.out -memprofile mem.out
go tool pprof http://localhost:6060/debug/pprof/heap
(pprof) top goroutineAnalisis heap menunjukkan heap dengan objek channel dan buffer yang tidak dibebaskan, sedangkan goroutine profile menampilkan goroutine stuck pada operasi baca dari channel.
Log tambahan di handler dan worker menunjukkan aliran permintaan terus masuk tanpa sinkronisasi shutdown, mempertegas channel tidak dilepas.
Root Cause dan Perbaikan
Akar masalah adalah penggunaan channel statis yang tidak pernah ditutup, dipicu oleh mode hot-reload Fiber yang memulai ulang handler tanpa menghentikan goroutine worker lama. Goroutine baru selalu dibuat, sedangkan yang lama tetap menunggu data. Hal ini menyebabkan buffer channel yang terus penuh berefek menahan alokasi memory.
Perbaikan dilakukan dengan pattern release channel dan cleanup eksplisit:
type JobQueue struct {
jobs chan string
done chan struct{}
}
func NewJobQueue(cap int) *JobQueue {
jq := &JobQueue{
jobs: make(chan string, cap),
done: make(chan struct{}),
}
go jq.worker()
return jq
}
func (jq *JobQueue) worker() {
defer close(jq.done)
for job := range jq.jobs {
process(job)
}
}
func (jq *JobQueue) Enqueue(payload string) bool {
select {
case jq.jobs <- payload:
return true
default:
return false
}
}
func (jq *JobQueue) Shutdown(ctx context.Context) {
close(jq.jobs)
select {
case <-jq.done:
case <-ctx.Done():
}
}
Dengan menutup channel jq.jobs dan menunggu jq.done, kita memastikan worker goroutine selesai sebelum aplikasi restart. Penambahan context membantu memaksa berhenti jika shutdown tertunda.
Terakhir, panggil Shutdown dari handler Fiber saat server menutup (misalnya via app.Hooks().OnShutdown) untuk memastikan aliran cleanup dijalankan.
Observability dan Monitoring
Untuk mencegah regresi, kombinasi metrik dan log sangat penting:
- Goroutine count: pantau dengan expvar/Prometheus untuk mendeteksi lonjakan tiba-tiba.
- Heap usage dan GC pause: memantau melalui exporter Go runtime metrics menunjukkan jika GC tidak bisa mengejar karena goroutine leak.
- Log Lifecycle: catat saat job queue dimulai/berhenti beserta status channel untuk memastikan
Shutdowndipanggil. - Alert: trigger bila queue length atau goroutine count melewati ambang, menandakan worker tidak pernah mengakhiri loop.
Dengan observability lengkap, tim dapat mengaitkan lonjakan memory dengan event lifecycle, bukan hanya membuat tebakan belaka.
Pelajaran untuk Tim
- Pastikan setiap channel yang di-range ditutup saat goroutine menunggu, terutama untuk worker queue di background.
- Ukur lifecycle goroutine dalam lingkungan staging sebelum production release untuk menangkap leak dini.
- Desain shutdown sebagai bagian dari arsitektur: jika server Fiber restart, worker harus berhenti lebih dulu.
- Gunakan context untuk memaksa stop worker jika penutupan normal tidak terjadi dalam batas waktu tertentu.
Kesimpulan
Memory Leak Goroutine Go Fiber terjadi ketika worker queue terus menunggu di channel tanpa mekanisme release. Dengan reproduksi yang jelas, profiling pprof, dan pola release channel, kita dapat menghentikan goroutine terbengkalai dan memastikan GC mengembalikan memori. Observability dan pelajaran tim membantu mencegah kasus serupa di masa depan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!