Masalah utama: worker Go Fiber kehabisan memori saat retry sebuah job background. Dalam dua paragraf pertama ini dijelaskan bahwa gejala memori tinggi dan OOM muncul di worker Go Fiber yang mengandalkan mekanisme retry otomatis, kemudian diikuti pendekatan observability untuk diagnosis dan perbaikan.
Gejala Awal dan Bukti Observability
Operator menemui out-of-memory (OOM) dan panic: runtime: out of memory di log worker setiap kali job dengan retry berjalan lebih dari satu kali. Observability memetakan dua metrik utama:
- runtime_memstats_heap_alloc_bytes naik terus dari ~30MB ke >150MB selama satu menit retry intensive.
- go_goroutines bertambah dari 200 ke 1200 seiring retry, padahal traffic tidak naik signifikan.
Log Fiber worker juga menunjukkan pattern ini:
2024-10-03T10:11:12Z ERROR worker: retry attempt=3 job=SendNotification err=timeout
2024-10-03T10:11:12Z WARN worker: goroutine leak detected for job=SendNotification pending=646
Ringkasnya, job dengan retry memicu goroutine yang tidak pernah selesai dan buffer channel bertambah, menyebabkan heap terus membengkak.
Diagnosis Root Cause
Model Retry dan Goroutine yang Tidak Pernah Exit
Worker Fiber mendesain retry dengan goroutine baru per job bersamaan dengan channel untuk menerima status:
func (w *Worker) processWithRetry(ctx context.Context, job Job) {
statusCh := make(chan Result, 1)
for attempt := 1; attempt <= maxRetries; attempt++ {
go w.runJob(ctx, job, statusCh)
select {
case res := <-statusCh:
if res.Success {
return
}
case <-time.After(attemptTimeout):
// retry
}
}
}
Masalahnya: setiap goroutine runJob memasukkan status ke channel, tapi pada timeout goroutine tetap berjalan di background karena channel tidak pernah dijaga. Ketika retry berjalan, goroutine lama menumpuk, channel buffer tidak dijaga ulang, dan pekerjaan pindah ke attempt baru tanpa menutup context goroutine sebelumnya.
Konfirmasi dengan Observability
Tabulasi goroutine dengan pprof.Lookup("goroutine").WriteTo menunjukkan stack trace menggantung pada w.runJob. Heap snapshot memperlihatkan buffer slice besar akibat pending response dan data job yang tidak pernah dibebaskan. Korelasi metrik latency dan goroutine menyimpulkan retry loop sebagai pemicu utama.
Perbaikan Praktis dan Penguatan Retry
Menghindari Goroutine Leak
Perbaikan pertama adalah menghindari goroutine per retry dan menggunakan context.WithCancel untuk menghentikan job lama:
func (w *Worker) processWithRetry(ctx context.Context, job Job) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
statusCh := make(chan Result)
for attempt := 1; attempt <= maxRetries; attempt++ {
go w.runJob(ctx, job, statusCh)
select {
case res := <-statusCh:
if res.Success {
return
}
case <-time.After(attemptTimeout):
cancel()
ctx, cancel = context.WithCancel(ctx)
}
}
}
Dengan memanggil cancel() sebelum menyiapkan retry berikutnya, goroutine runJob bisa keluar lebih deterministik. Channel tidak perlu buffered besar jika hanya satu goroutine aktif per siklus, sehingga bisa diganti dengan non-buffered.
Membersihkan Buffer dan Menghindari Cache Job
Job sebelumnya menyimpan data payload dan header ke dalam buffer global untuk retry. Jika retry gagal, buffer belum dikosongkan dan terus disimpan di heap. Solusinya:
- Gunakan
sync.Pooluntuk buffer sehingga reuse tidak meninggalkan referensi permanen. - Setelah job selesai (berhasil/gagal), panggil
buffers.Release()sebelum lanjut ke retry berikutnya. - Gunakan profiler heap untuk memastikan tidak ada referensi residual (
pprof.WriteHeapDump).
Langkah Verifikasi
Jalankan beban spike dengan job yang dipaksa gagal pada attempt pertama dan mengamati memory usage via Prometheus/Grafana; heap_alloc harus flat setelah retry selesai.
Hitung
go_goroutines{job="SendNotification"}sebelum dan sesudah perbaikan; seharusnya tidak ada ratusan goroutine leftover.Gunakan
pprofuntuk memverifikasi tidak ada goroutine yang stuck diw.runJob. Pastikan goroutine selesai dalam lima detik setelah retry timeout.Deploy ke staging dengan worker yang sama, dan jalankan load test dengan
heyatauwrksambil memantau OOM killer log di kernel (`dmesg | grep -i oom`).
Setelah konfirmasi, rolling upgrade ke produksi dapat dilakukan dengan memantau grafik memory dan goroutine selama satu jam pertama.
Catatan Tambahan
- Trade-off: Menambahkan cancelable context memperbesar kompleksitas state retry; perlu menangani error context.Canceled di runJob.
- Kesalahan umum: Mengabaikan channel close, membuat retry goroutine terus menulis ke channel saat worker dihentikan.
- Debugging tip: Gunakan
go test -run TestWorker -count=1 -racedi paket worker untuk mendeteksi data race saat cancel diterapkan.
Dengan pendekatan observability yang kaya, koreksi goroutine management, dan verifikasi sistematis, worker Go Fiber dapat menjalankan retry tanpa kehabisan memori.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!