Pengantar dan Ringkasan Solusi
Setelah traffic spike, backend Go Fiber kami mengalami lonjakan latency p95 hingga 2,5 detik walaupun CPU dan throughput tetap stabil. Gejala ini langsung mempengaruhi user experience API internal. Artikel ini menjelaskan bagaimana kami mereproduksi, menganalisis, dan memperbaiki bug eksponensial yang tersembunyi di handler goroutine, hingga mencegahnya dari sisi observability dan arsitektur.
Solusi berfokus pada kombinasi observability (profiling, tracing, metrik), debugging analitik, dan refactor handler agar tidak membuat goroutine tak terkendali atau channel yang menunggu indefinitely I/O tanpa timeout.
1. Gejala dan Observability
Langsung setelah traffic spike (~500 req/s), latency API melonjak, sementara:
- p95 latency meningkat dari 120ms menjadi >2s.
- CPU Node tetap di bawah 70%.
- Garbage collection frequency tidak berubah signifikan.
- Connection pool ke database tampak tidak penuh.
Metrik observability yang digunakan:
- Latency histogram dari Fiber middleware.
- Thread/ goroutine count via runtime/pprof.
- Channel queue length dari instrumentation internal.
- Trace distribusi menggunakan OpenTelemetry untuk melihat durasi handler dan downstream.
Gejala utamanya: latency naik seiring jumlah goroutine aktif juga meningkat secara eksponensial tanpa turun meskipun request selesai.
2. Reproduksi Kasus
Untuk memastikan masalah bukan faktor eksternal, kami menghimpun traffic control dengan wrk:
wrk -t4 -c100 -d60s http://api.internal/servicePerhatikan bahwa latency naik drastis pada 15 detik pertama traffic spike, dan goroutine count dari pprof terus naik. Kami menambahkan endpoint debug sederhana yang mengembalikan jumlah goroutine agar terus dipantau.
Reproduksi juga mencakup pengiriman payload besar karena handler melakukan I/O ke service downstream tanpa timeout. Saat service downstream slow, goroutine menunggu indefinitely.
3. Analisis Root Cause
Melakukan pprof heap dan goroutine mengungkap pola:
- Handler memulai goroutine baru per request dan mengirim data ke channel buffered kecil.
- Channel tersebut diproses oleh worker loop tanpa timeout atau back-pressure.
- Jika downstream tidak merespon cepat, goroutine tetap menunggu di channel
selectdengan blocking call.
Contoh simplifikasi handler yang bermasalah:
func handler(c *fiber.Ctx) error {
data := c.Body()
responseCh := make(chan result)
go func() {
res := callDownstream(data)
responseCh <- res
}()
select {
case res := <-responseCh:
return c.JSON(res)
case <-c.Context().Done():
return fiber.ErrRequestTimeout
}
}Problema adalah goroutine launch per request tanpa batas dan callDownstream bisa blocking lama. Jika callDownstream tidak responsif, goroutine tetap hidup dan channel menunggu, menimbulkan latency cumulative. Selain itu, tidak ada batas waktu sebelum panggilan goroutine berlangsung, sehingga spike traffic memicu ribuan goroutine.
4. Langkah Perbaikan Praktis
4.1 Profiling dan Tracing
Kami menggunakan:
- pprof goroutine untuk melihat stack goroutine yang tertahan.
- pprof block profile untuk mengetahui channel blocking.
- OpenTelemetry trace untuk melihat dependency tree per request.
Profiling mengkonfirmasi goroutine menumpuk pada select tanpa timeout, dan trace memastikan bottleneck ada di downstream call.
4.2 Refactor Handler
Perbaikan melibatkan:
- Menghindari goroutine baru: cukup jalankan panggilan I/O di goroutine utama handler karena Fiber sudah asynchronous.
- Menambahkan context dengan timeout dan membatalkan panggilan jika melebihi batas.
- Memanfaatkan worker pool terbatas jika asynchronous tetap dibutuhkan.
Perubahan kode:
func handler(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second)
defer cancel()
res, err := callDownstreamWithContext(ctx, c.Body())
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return fiber.ErrRequestTimeout
}
return fiber.ErrInternalServerError
}
return c.JSON(res)
}Panggilan callDownstreamWithContext sekarang menghormati ctx sehingga jika downstream lambat, handler menghentikan operasi dan tidak menciptakan goroutine menumpuk.
4.3 Pengaturan Timeout dan Back-pressure
Selain timeout handler, kami memasang:
- Rate limiter di Fiber middleware untuk mencegah spike langsung memicu worker pool.
- Channel buffered dengan kapasitas terbatas jika strategi worker pool digunakan, agar request ditolak saat queue penuh.
- Instrumentasi queue length ke Observability untuk alert ketika melebihi threshold.
5. Pencegahan dan Praktik Observability
Rekomendasi jangka panjang:
- Terapkan timeouts kaidan konteks di semua panggilan I/O, termasuk HTTP client dan database query.
- Gunakan profil goroutine secara berkala untuk mendeteksi goroutine leak.
- Pantau latency histogram dan queue depth untuk menandai pola eksponensial.
- Siapkan alert ketika goroutine count naik di luar baseline.
- Sediakan retry dengan jitter pada panggilan downstream yang bisa diulang.
Debugging kasual ternyata menunjukkan bahwa kenaikan latency bukan soal CPU atau DB, melainkan akumulasi goroutine yang menunggu. Dengan observability yang tepat, bug eksponensial ini dapat dideteksi lebih awal sebelum berdampak ke user.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!