Menjawab langsung masalah
Connection leak terjadi ketika aplikasi Go Fiber gagal mengembalikan koneksi ke pool SQLx setelah query gagal atau terjadi panic di middleware, sehingga akhirnya terlihat gejala timeout 504, antrian request panjang, dan log "connection pool is full". Fokus artikel ini adalah menunjukkan gejala nyata, data observability yang perlu diperiksa, akar masalah di middleware, dan langkah perbaikan konkret sampai verifikasi fix dan mitigasi otomatis.
Gejala nyata yang memicu debugging
- Timeout 504 di gateway dan reverse proxy setelah traffic tinggi, meskipun database masih responsif saat diuji langsung.
- Log database mencatat "connection pool is full" atau "timeout acquiring connection" secara berkala.
- Antrian request Fiber menumpuk karena goroutine menunggu koneksi yang tidak dikembalikan.
- Turunan Metrik seperti active connections terus meningkat sementara idle tetap nol.
Observability & metrics yang membantu diagnosis
Untuk mempersempit masalah connection leak, pantau metrik-metrik ini:
- Active vs idle connection dari SQLx (biasanya sumber log driver atau ekspor via Prometheus).
- Latency p95/p99 handler untuk melihat di mana timeout terjadi.
- Jumlah goroutine atau Fiber queue depth menunjukkan blocking yang menunggu koneksi.
- Trace request untuk melihat channel pipeline mana yang tidak menutup db.Rows atau tx.
Dengan data tersebut, Anda dapat membuktikan bahwa walau pool dikonfigurasi cukup besar, koneksi tetap habis karena tidak dikembalikan.
Root cause: pool SQLx habis saat rows/tx tidak ditutup
Go Fiber middleware yang memanggil SQLx secara langsung sering kali menangani error tanpa memastikan rows atau transaksi ditutup. Ini menyebabkan koneksi tetap terbuka sampai GC menutupnya, yang bisa sangat lambat di load tinggi.
Contoh middleware bermasalah
func handleRequest(c *fiber.Ctx) error {
tx, err := db.Beginx(c.Context())
if err != nil {
return c.Status(500).SendString("gagal mulai transaksi")
}
rows, err := tx.QueryxContext(c.Context(), "SELECT ...")
if err != nil {
return c.Status(500).SendString("query gagal") // rows belum ditutup
}
for rows.Next() {
var item Item
rows.StructScan(&item)
// proses
}
tx.Commit()
return c.JSON(...)
}
Jika QueryxContext mengembalikan error, rows bernilai nil dan tidak pernah secara eksplisit ditutup. Dalam kasus lain, panic sebelum rows.Close() atau tx.Commit() akan menyebabkan koneksi menunggu timeout.
Konfigurasi pool SQLx yang mendukung
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(15 * time.Minute)
Walau konfigurasi di atas valid, pool tetap akan habis jika handler tidak mengembalikan koneksi. Perlu ada batasan yang konservatif agar tidak terlalu banyak koneksi terbuka bersamaan.
Langkah perbaikan konkret
- Gunakan
deferuntuk menutup rows/tx: Selalu panggildefer rows.Close()segera setelah berhasil membuka hasil query. Sama halnya dengandefer tx.Rollback()untuk memastikan transaksi tidak menggantung. - Tangani error dengan memastikan defer dieksekusi: Misalnya, setelah
rows, err := tx.QueryxContext(...), tulisif err != nil { return err }tapi sebelum itu, pastikandefer rows.Close()dipanggil. - Pisahkan logika middleware: Jika middleware memanggil beberapa query, pertimbangkan mengekstrak fungsi kecil agar defer tetap terbaca dan tidak terlewat.
- Tambahkan health check database: Endpoint health check harus memanggil
db.PingContextdan mengembalikan status degradasi jika tidak bisa membuka koneksi baru. - Monitoring alert: Buat alert ketika ratio active/MaxOpenConns mendekati 100% atau ketika request Fiber menunggu lebih dari timeout standar.
Patern yang lebih aman
func queryItems(ctx context.Context, tx *sqlx.Tx) ([]Item, error) {
rows, err := tx.QueryxContext(ctx, "SELECT ...")
if err != nil {
return nil, err
}
defer rows.Close()
var items []Item
for rows.Next() {
var item Item
if err := rows.StructScan(&item); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func handleRequest(c *fiber.Ctx) error {
tx, err := db.Beginx(c.Context())
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "gagal mulai transaksi")
}
defer tx.Rollback()
items, err := queryItems(c.Context(), tx)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return c.JSON(items)
}
Dengan pola di atas, setiap fungsi bertanggung jawab menutup resource-nya sendiri, membuat connection leak lebih sulit terjadi.
Verifikasi perbaikan dan mitigasi otomatis
Setelah memperbaiki middleware, lakukan langkah-langkah berikut:
- Uji stres terbatas dengan beban simulasi dan pantau metrik connection pool untuk memastikan idle kembali normal setelah request selesai.
- Periksa log untuk memastikan tidak ada lagi pesan "connection pool is full" dan tidak ada goroutine menunggu terlalu lama.
- Tambahkan health check berkala yang memanggil
db.PingContextdan melaporkan kegagalan ke monitoring system yang sama. - Otomatis mitigasi: Buat alert yang memicu rollback otomatis atau circuit breaker ketika active connection melebihi threshold 90% dari MaxOpenConns, sehingga Anda dapat menolak request baru sementara pool pulih.
Dengan kombinasi pengecekan observability, penutupan resource yang konsisten, dan mekanisme mitigation otomatis, kejadian connection leak dapat dihindari atau dideteksi lebih awal sebelum memicu incident besar.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!