Membuka satu koneksi PostgreSQL itu mahal: proses TCP handshake, autentikasi, dan negosiasi SSL memakan waktu bermilidetik. Bayangkan biaya tersebut dibayar ulang untuk setiap query — aplikasi akan lumpuh oleh biaya administratif semata. database/sql menjawabnya dengan connection pool yang sudah menyatu di dalam *sql.DB: koneksi dibuat sekali, dipinjam bergantian oleh query, lalu dikembalikan untuk dipakai lagi. Masalahnya, pengaturan bawaan pool tidak cocok untuk semua aplikasi. Chapter ini membahas keempat tombol pengaturan pool, cara memperkirakan angkanya, cara memantau kondisinya, serta dua kecelakaan klasik yang membuat pool mati perlahan.
Nilai Bawaan Pool #
Tanpa pengaturan apa pun, pool database/sql bekerja dengan batasan longgar: MaxOpenConns tak terbatas, MaxIdleConns hanya 2, dan umur koneksi selamanya. Konsekuensinya dua arah: saat beban melonjak, aplikasi bisa membuka ratusan koneksi sekaligus dan membuat server PostgreSQL kehabisan slot; sebaliknya, hanya dua koneksi menganggur yang disimpan, sehingga aplikasi sibuk terus-menerus membuka dan menutup koneksi. Kedua gejala tersebut mendorong setiap aplikasi produksi untuk menyetel pool secara eksplisit.
Empat Pengaturan Pool #
Konfigurasi dilakukan sekali setelah sql.Open, sebelum pool dipakai:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)| Method | Mengatur | Nilai 0 berarti |
|---|---|---|
SetMaxOpenConns | Jumlah koneksi aktif maksimum | Tanpa batas |
SetMaxIdleConns | Jumlah koneksi menganggur yang disimpan | Tidak menyimpan koneksi idle |
SetConnMaxLifetime | Umur maksimum sebuah koneksi | Koneksi hidup selamanya |
SetConnMaxIdleTime | Lama maksimum koneksi menganggur | Koneksi idle tidak pernah kedaluwarsa |
Mengapa umur koneksi perlu dibatasi? Infrastruktur di sekitar database diam-diam memutus koneksi lama: load balancer menjatuhkan koneksi yang melewati batas waktu, firewall membersihkan sesi diam, dan server restart saat pemeliharaan. SetConnMaxLifetime yang dipasang sedikit di bawah batas infrastruktur membuat pool menutup koneksi secara terjadwal, bukan mendadak di tengah query.
Memperkirakan Angka dengan Hukum Little #
Angka pool tidak harus hasil tebakan. Hukum Little dari teori antrean memberi perkiraan awal: jumlah koneksi yang sibuk rata-rata ≈ laju permintaan per detik × durasi rata-rata query dalam detik. Berkas contoh.go pada bagian bawah halaman menghitung perkiraan tersebut untuk tiga skenario beban, lalu mencetak metrik pool dalam bentuk struct sql.DBStats:
package main
import (
"database/sql"
"fmt"
)
// Skenario menggambarkan beban sebuah aplikasi terhadap database.
type Skenario struct {
nama string
qps float64
durasiQuery float64 // dalam detik
}
func main() {
// sql.DBStats menampung seluruh metrik pool; sebelum pool
// dipakai, seluruh nilainya masih nol.
var statistik sql.DBStats
fmt.Printf("DBStats awal: %+v\n", statistik)
skenario := []Skenario{
{nama: "API ringan", qps: 50, durasiQuery: 0.002},
{nama: "API sibuk", qps: 400, durasiQuery: 0.010},
{nama: "laporan berat", qps: 20, durasiQuery: 0.750},
}
fmt.Println("perkiraan konkurensi = QPS x durasi query:")
for _, s := range skenario {
konkurensi := s.qps * s.durasiQuery
fmt.Printf("%-15s QPS=%6.0f durasi=%6.3fs koneksi=%5.2f\n",
s.nama, s.qps, s.durasiQuery, konkurensi)
}
}Output yang Dihasilkan:
DBStats awal: {MaxOpenConnections:0 OpenConnections:0 InUse:0 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
perkiraan konkurensi = QPS x durasi query:
API ringan QPS= 50 durasi= 0.002s koneksi= 0.10
API sibuk QPS= 400 durasi= 0.010s koneksi= 4.00
laporan berat QPS= 20 durasi= 0.750s koneksi=15.00Mari kita telaah baris demi baris program di atas:
- Baris 9-13 (
type Skenario): satu skenario menyimpan nama beban, laju query per detik (QPS), dan durasi rata-rata query. - Baris 18-19 (
var statistik sql.DBStats): structsql.DBStatsmilik standard library dicetak dengan format%+v, memperlihatkan seluruh nama field-nya — field yang sama persis dikembalikan methoddb.Stats(). - Baris 21-25 (
skenario := ...): tiga profil beban: API ringan, API sibuk, dan laporan berat yang query-nya lama. - Baris 29 (
konkurensi := s.qps * s.durasiQuery): inti perhitungan Hukum Little — hasilnya adalah perkiraan koneksi yang sibuk secara rata-rata.
Angka hasil hitungan menjadi titik awal: pasang MaxOpenConns sedikit di atasnya sebagai ruang untuk lonjakan, samakan MaxIdleConns dengan MaxOpenConns agar koneksi siap pakai tidak dibuang, lalu ukung dengan metrik nyata. Ingat juga bahwa server PostgreSQL sendiri punya batas total koneksi (bawaannya 100) yang dibagi semua aplikasi penghuninya.
Memantau Pool dengan Stats() #
Pemanggilan db.Stats() mengembalikan sql.DBStats berisi potret pool saat itu juga. Tiga field paling sering diperiksa: InUse (koneksi yang sedang dipakai query), Idle (koneksi siap pakai), dan WaitCount — jumlah permintaan yang perlu menunggu karena semua koneksi sedang sibuk.
Kebocoran Koneksi dan Pool Habis #
Pool berhenti bekerja saat koneksi tidak pernah kembali. Penyebab nomor satu di aplikasi Go: hasil QueryContext yang tidak ditutup. Setiap *sql.Rows memegang satu koneksi pinjaman; tanpa rows.Close(), koneksi tidak kembali ke pool selamanya. Sedikit demi sedikit, InUse merambat mendekati MaxOpenConns, lalu setiap query baru menunggu di antrean sampai context-nya kehabisan waktu. Pola pencegahannya satu baris dan wajib: defer rows.Close() segera setelah memeriksa error — pola lengkapnya kita praktikkan di chapter Query dan Parameter
.
Pool yang sehat membuat eksekusi query tinggal urusan kecepatan server. Chapter berikutnya masuk ke inti pekerjaan sehari-hari: menjalankan query dengan QueryContext, QueryRowContext, ExecContext, dan parameter binding yang aman dari SQL injection.
Referensi #
- Managing connections — panduan resmi mengatur connection pool.
- Dokumentasi package database/sql
— spesifikasi seluruh method
Set*danStats. - DB.SetMaxOpenConns — detail method pembatas koneksi aktif.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.go
Unduh berkaspackage main
import (
"database/sql"
"fmt"
)
// Skenario menggambarkan beban sebuah aplikasi terhadap database.
type Skenario struct {
nama string
qps float64
durasiQuery float64 // dalam detik
}
func main() {
// sql.DBStats menampung seluruh metrik pool; sebelum pool
// dipakai, seluruh nilainya masih nol.
var statistik sql.DBStats
fmt.Printf("DBStats awal: %+v\n", statistik)
skenario := []Skenario{
{nama: "API ringan", qps: 50, durasiQuery: 0.002},
{nama: "API sibuk", qps: 400, durasiQuery: 0.010},
{nama: "laporan berat", qps: 20, durasiQuery: 0.750},
}
fmt.Println("perkiraan konkurensi = QPS x durasi query:")
for _, s := range skenario {
konkurensi := s.qps * s.durasiQuery
fmt.Printf("%-15s QPS=%6.0f durasi=%6.3fs koneksi=%5.2f\n",
s.nama, s.qps, s.durasiQuery, konkurensi)
}
}