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 goroutine

Analisis 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 Shutdown dipanggil.
  • 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.