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:

go
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
MethodMengaturNilai 0 berarti
SetMaxOpenConnsJumlah koneksi aktif maksimumTanpa batas
SetMaxIdleConnsJumlah koneksi menganggur yang disimpanTidak menyimpan koneksi idle
SetConnMaxLifetimeUmur maksimum sebuah koneksiKoneksi hidup selamanya
SetConnMaxIdleTimeLama maksimum koneksi menganggurKoneksi 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:

go
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:

text
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.00

Mari kita telaah baris demi baris program di atas:

  1. Baris 9-13 (type Skenario): satu skenario menyimpan nama beban, laju query per detik (QPS), dan durasi rata-rata query.
  2. Baris 18-19 (var statistik sql.DBStats): struct sql.DBStats milik standard library dicetak dengan format %+v, memperlihatkan seluruh nama field-nya — field yang sama persis dikembalikan method db.Stats().
  3. Baris 21-25 (skenario := ...): tiga profil beban: API ringan, API sibuk, dan laporan berat yang query-nya lama.
  4. 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.go

Unduh berkas
go
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)
	}
}