Gejala Nyata: Payload Tertukar Antar-User
Pada aplikasi Go Fiber dengan beban lalu lintas tinggi, bug konkurensi sering kali muncul bukan sebagai panic langsung, melainkan sebagai korupsi data yang tersembunyi (silent corruption). Gejala umum yang sering dilaporkan:
- User A menerima respon atau notifikasi berisi data milik User B.
- Payload yang disimpan ke database melalui background task terpotong atau berisi fragmen payload request lain.
- Log aplikasi menampilkan header
Authorizationatau field JSON yang tidak konsisten secara acak saat traffic melonjak.
Penyebab utama dari anomali ini adalah passing pointer fiber.Ctx atau memory slice bawaannya langsung ke dalam background goroutine tanpa isolasi memori.
Root Cause: Arsitektur Zero-Allocation fasthttp
Go Fiber dibangun di atas engine fasthttp, bukan standar net/http. Perbedaan arsitektur fundamental keduanya terletak pada alokasi memori:
net/http mengalokasikan struct http.Request baru untuk setiap request masuk dan membiarkan Garbage Collector (GC) membersihkannya. Sebaliknya, fasthttp mengejar performa zero-allocation dengan memanfaatkan sync.Pool secara agresif untuk me-recycle instance context dan buffer byte pendukungnya.
Siklus hidup memori pada Go Fiber berjalan sebagai berikut:
- Koneksi TCP masuk,
fasthttpmengambil objekRequestCtxdan buffer memori darisync.Pool. - Handler Fiber mengeksekusi request. Semua operasi pembacaan seperti
c.Body(),c.Params(), danc.Query()mengembalikan slice byte yang merujuk langsung ke buffer memori internal milikfasthttp, bukan salinan baru. - Segera setelah fungsi handler utama me-return nilai (misalnya
return c.SendStatus(200)), Fiber menganggap siklus request selesai. - Instance context dan buffer memori terkait di-reset (di-zero out) dan dikembalikan ke
sync.Pool. - Buffer yang sama dialokasikan kembali untuk melayani request HTTP berikutnya dari koneksi atau client lain.
Jika handler mengeksekusi background goroutine yang masih memegang pointer *fiber.Ctx atau slice dari c.Body(), goroutine tersebut akan membaca area memori yang sedang ditulis ulang oleh request baru. Hal ini memicu race condition dan data cross-contamination antar-request.
Contoh Kasus Buggy Code
Kode di bawah ini menunjukkan kesalahan umum: memproses data di background goroutine dengan mempertahankan referensi ke c *fiber.Ctx.
package main
import (
"log"
"time"
"github.com/gofiber/fiber/v2"
)
func RegisterRoutes(app *fiber.App) {
app.Post("/api/audit", func(c *fiber.Ctx) error {
// BUG: c.Body() mengembalikan slice langsung ke buffer fasthttp.
// c *fiber.Ctx juga di-recycle segera setelah handler return.
go func() {
time.Sleep(50 * time.Millisecond) // Mensimulasikan I/O / DB latency
payload := c.Body()
log.Printf("Processed payload: %s\n", string(payload))
}()
return c.SendStatus(fiber.StatusAccepted)
})
}
Reproduksi Bug Menggunakan go test -race
Bug ini dapat dibuktikan secara deterministik menggunakan pengujian konkurensi dengan Go race detector bawaan runtime.
package main
import (
"bytes"
"net/http/httptest"
"sync"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestDataRaceContext(t *testing.T) {
app := fiber.New()
RegisterRoutes(app)
var wg sync.WaitGroup
concurrentRequests := 10
for i := 0; i < concurrentRequests; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
req := httptest.NewRequest("POST", "/api/audit", bytes.NewBufferString("payload-user-"+string(rune(id))))
resp, err := app.Test(req, -1)
if err != nil {
t.Errorf("Request failed: %v", err)
}
resp.Body.Close()
}(i)
}
wg.Wait()
}
Jalankan pengujian menggunakan flag -race:
go test -race -v -run TestDataRaceContextRuntime Go akan langsung mendeteksi pelanggaran akses memori dan mencetak trace data race:
WARNING: DATA RACE
Write at 0x00c0001a4000 by goroutine 8:
github.com/valyala/fasthttp.(*RequestCtx).Reset()
...
Previous Read at 0x00c0001a4000 by goroutine 12:
main.RegisterRoutes.func1.1()
...
Found 1 data race(s)
FAIL
Solusi dan Pola Perbaikan
1. Menggunakan c.Copy() untuk Context Duplication
Fiber menyediakan method c.Copy() yang mengembalikan kloningan context independen yang aman dari siklus recycle sync.Pool.
app.Post("/api/audit", func(c *fiber.Ctx) error {
// Deep copy context sebelum masuk ke goroutine
cSafe := c.Copy()
go func() {
time.Sleep(50 * time.Millisecond)
log.Printf("Processed payload: %s\n", string(cSafe.Body()))
}()
return c.SendStatus(fiber.StatusAccepted)
})
Peringatan performa: c.Copy() menduplikasi seluruh context. Menggunakan cara ini secara masif membatalkan keuntungan zero-allocation dari Fiber dan menambah beban kerja Garbage Collector.2. Deep Copy Byte Slice dan String Utility
Jika background process hanya membutuhkan payload atau query string tertentu, salin hanya data yang diperlukan sebelum return. Hindari menyalin seluruh objek context.
import (
"bytes"
"github.com/gofiber/fiber/v2/utils"
)
app.Post("/api/audit", func(c *fiber.Ctx) error {
// Kloning byte slice (Go 1.20+)
bodyPayload := bytes.Clone(c.Body())
// Salin string immutable jika mengambil dari c.Params atau c.Query
actionType := utils.CopyString(c.Query("action"))
go func(payload []byte, action string) {
time.Sleep(50 * time.Millisecond)
log.Printf("Action: %s, Payload: %s\n", action, string(payload))
}(bodyPayload, actionType)
return c.SendStatus(fiber.StatusAccepted)
})
3. Best Practice: Worker Pool Decoupled dari Lifecycle HTTP
Pendekatan arsitektur yang paling bersih dan efisien adalah memisahkan HTTP layer dengan asynchronous processing menggunakan domain DTO dan buffered channel / worker pool. Request langsung diurai ke struct Go sebelum dilempar ke worker.
type AuditJob struct {
UserID string
Payload []byte
}
type JobQueue struct {
queue chan AuditJob
}
func (jq *JobQueue) Submit(job AuditJob) {
jq.queue <- job
}
func StartWorker(queue chan AuditJob) {
go func() {
for job := range queue {
// Processing logic berjalan terisolasi dari memori fasthttp
log.Printf("Processing job user: %s, payload: %s\n", job.UserID, string(job.Payload))
}
}()
}
func SetupSecureRoute(app *fiber.App, jobs *JobQueue) {
app.Post("/api/audit", func(c *fiber.Ctx) error {
// 1. Ekstraksi data dan lepas dari buffer fasthttp
job := AuditJob{
UserID: utils.CopyString(c.Get("X-User-ID")),
Payload: bytes.Clone(c.Body()),
}
// 2. Kirim ke decoupled worker
jobs.Submit(job)
return c.SendStatus(fiber.StatusAccepted)
})
}
Panduan Pemilihan Solusi
- Gunakan
bytes.Clone()/utils.CopyString()jika payload kecil dan pemrosesan async bersifat lokal pada satu fungsi. - Gunakan Decoupled Worker (Channel/Queue) jika pemrosesan async merupakan bagian dari core pipeline bisnis (logging, database write, event dispatcher). Ini adalah standar produksi yang paling aman.
- Gunakan
c.Copy()hanya untuk backward compatibility atau debugging cepat ketika refactoring signature fungsi belum memungkinkan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!