Memory Leak Go Fiber saat Hit Burst API terjadi ketika worker queue backend terus menumpuk referensi objek selama lonjakan permintaan. Dalam dua paragraf pertama ini, langsung dijelaskan bahwa kita akan menganalisis gejala (GC spike, OOM), cara mereproduksi burst traffic, serta solusi praktis untuk memperbaiki goroutine transaksi yang menahan memori.
1. Observasi Gejala di Lapangan
Tim operasi mencatat beberapa indikator tepat saat API Go Fiber menerima burst: heap allocation terus naik tanpa turun, GC berdampak signifikan (jumlah GC spike meningkat dan waktu pause melonjak), lalu akhirnya muncul OOM kill dari container orchestrator.
Metric server menunjukkan rata-rata Alloc naik 2x dalam hitungan menit, sedangkan log Fiber mencatat penundaan request handling dan worker goroutine yang menunggu channel. Contoh log nyata yang diperiksa:
2024-10-10T14:05:12Z DEBUG server.go:132 | processing /transactions, worker=17
2024-10-10T14:05:13Z WARN queue.go:54 | queue length=320, busy=true
2024-10-10T14:05:15Z ERROR oom-killer: process killed due to memory pressure
Penjelasan kuat: GC spike menunjukkan heap tumbuh cepat karena referensi tidak dilepas, log worker queue membuktikan ada backlog, dan OOM menandakan sistem sudah kehabisan memori.
2. Cara Reproduksi Beban Tinggi
Untuk memvalidasi issue secara lokal, jalankan skrip load test dengan vegeta atau wrk yang mengirimkan burst request di endpoint transaksi. Misalnya:
wrk -t4 -c200 -d30s http://localhost:8080/api/transactions
Perhatikan bahwa setelah 10-15 detik, heap usage naik terus walau traffic tetap konstan. Untuk membantu observasi, aktifkan pprof pada aplikasi Fiber:
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
Profiling ini memastikan bahwa kita bisa memotret heap dan melihat alokasi yang menyebabkan kebocoran.
3. Root Cause: Goroutine Worker Queue yang Menahan Referensi
Investigasi source code mengungkap bahwa setiap request transaksi diproses dengan goroutine dan buffer khusus yang disimpan di struct worker. Saat load tinggi, goroutine menerima objek transaksi, memasukkannya ke channel, lalu dieksekusi satu per satu.
Masalahnya:
- Channel tidak pernah ditutup sehingga goroutine menunggu, mengakumulasi referensi.
- Buffer per request dialokasikan ulang tanpa reuse, jadi GC bekerja ekstra keras.
- Goroutine tidak dibatasi sehingga jumlahnya bertambah tajam selama burst.
Contoh snippet worker queue (disederhanakan):
type TransactionWorker struct {
queue chan *Transaction
}
func (w *TransactionWorker) Start() {
for req := range w.queue {
process(req)
// Tidak ada mekanisme release buffer yang eksplisit
}
}
func (w *TransactionWorker) Enqueue(tx *Transaction) {
w.queue <- tx
}
Proses request mengalokasikan Transaction baru, dan setiap goroutine menunggu channel tanpa pernah menutupnya, sehingga referensi terus aktif walau request selesai.
4. Langkah Perbaikan
Berikut pendekatan praktis untuk memperbaiki memory leak:
- Profiling heap secara berkelanjutan: Jalankan
go test -run TestName -bench . -benchmemtidak relevan; gunakango tool pprofdari endpoint/debug/pprof/heapuntuk memetakan alokasi terbanyak dan melihat apakah worker queue bertanggung jawab.
Ukur durasi retention object sebelum dan sesudah request selesai untuk memastikan heap benar-benar berkurang. - Reuse buffer: Daripada membuat object baru untuk setiap request, gunakan
sync.Pooluntuk mengelola buffer transaksi. Dengan begini GC tidak dipaksa bekerja keras tiap burst. - Batasi goroutine: Implementasikan worker pool tetap dengan channel buffered yang menampung maksimal konfigurasi. Dengan limit, goroutine tidak terus tumbuh saat API kena spike.
- Release resource eksplisit: Setelah proses request selesai, kosongkan referensi (misalnya
tx.Data = nil) sebelum mengembalikan objek ke pool. Ini membuat GC tahu bahwa memori bisa direcycle.
Contoh penyesuaian worker queue dengan sync.Pool dan limit goroutine:
var txPool = sync.Pool{
New: func() any { return new(Transaction) },
}
func worker(queue <-chan *Transaction) {
for tx := range queue {
process(tx)
tx.reset()
txPool.Put(tx)
}
}
func enqueue(queue chan<- *Transaction) {
tx := txPool.Get().(*Transaction)
tx.fillFromRequest()
queue <- tx
}
Detail reset() membuat fields nil, memastikan GC bisa mereclaim tanpa ada residual data.
5. Checklist Pencegahan Jangka Panjang
- Monitoring berkelanjutan: Pasang alert untuk heap usage dan GC pause yang tiba-tiba naik.
- Profiling rutin: Gunakan pprof setelah deploy untuk memastikan tidak ada goroutine tersembunyi atau channel blocking.
- Limit worker: Tentukan batas goroutine maksimal berdasarkan hardware dan throughput API.
- Review channel lifecycle: Pastikan channel ditutup saat tidak diperlukan, atau gunakan context dengan timeout untuk menghindari goroutine menunggu selamanya.
- Gunakan lint atau go vet: Deteksi kemungkinan data race serta memory leak akibat pointer asing.
Kesimpulan
Memory leak saat hit burst API di Go Fiber biasanya berakar dari worker queue yang tidak melepaskan referensi objek transaksi sekaligus goroutine yang tidak terbatas. Dengan langkah observasi metric, profiling heap, reuse buffer via sync.Pool, membatasi goroutine, serta checklist preventif, Anda bisa menanggulangi kebocoran memori dan menjaga stabilitas backend di puncak trafik.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!