Ringkasan Utama
PgBouncer punya tiga mode pooling. Session pool memegang satu backend untuk seluruh sesi klien dan mempertahankan semua fitur Postgres. Transaction pool memakai ulang backend antar transaksi demi konkurensi jauh lebih tinggi. Statement pool mendaur ulang per statement. Pilih transaction mode untuk aplikasi web, lalu ukur default_pool_size dari jumlah transaksi konkuren nyata, bukan jumlah klien.
Postgres membuka satu proses OS penuh per koneksi, jadi beberapa ratus klien idle diam-diam bisa memakan gigabyte RAM dan membuat klien yang benar-benar bekerja kelaparan. PgBouncer duduk di depan Postgres sebagai connection pooler ringan: ribuan klien terhubung ke sana, dan ia melakukan multiplexing ke sekumpulan kecil koneksi backend nyata. Satu setting yang menentukan seberapa agresif multiplexing itu adalah pool_mode, dan salah memilihnya membuat Anda menyia-nyiakan seluruh pool atau merusak hasil query lintas pengguna.
Saya menjalankan PgBouncer di depan setiap instance Postgres yang saya miliki di VPS, dan pooling mode adalah tombol pertama yang saya atur. Berikut apa yang sebenarnya dilakukan tiap mode, jebakan prepared statement yang dulu membuat transaction mode menyakitkan, dan metode sizing yang bertahan saat lonjakan trafik.
Apa yang sebenarnya dikendalikan pooling mode
Pooling mode menentukan satu hal: berapa lama koneksi server tetap ditugaskan ke satu koneksi klien sebelum kembali ke pool untuk dipakai orang lain. Keputusan tunggal itu merambat ke fitur Postgres mana yang boleh Anda pakai, karena fitur seperti prepared statement, variabel sesi, dan tabel temporer hidup di koneksi server, bukan koneksi klien. Semakin lama penugasannya, semakin banyak session state yang bertahan, dan semakin sedikit koneksi yang bisa dibagi.
Session pooling: default yang aman
Dalam session mode sebuah backend diberikan ke klien saat ia terhubung dan baru dikembalikan ketika klien itu memutus koneksi. Perilakunya persis seperti berbicara langsung ke Postgres, jadi semua fitur bekerja: perintah SET, LISTEN/NOTIFY, tabel temporer berlingkup sesi, advisory lock, dan PREPARE level teks semuanya bertahan. Biayanya, konkurensi dibatasi oleh ukuran pool. Jika default_pool_size 20, hanya 20 klien yang bisa terhubung dan aktif sekaligus; klien ke-21 menunggu. Session mode tepat ketika klien memegang koneksi sebentar dan Anda bergantung pada session state, tapi ia memboroskan backend untuk klien web idle yang menahan koneksi antar request.
Transaction pooling: yang diinginkan aplikasi web
Dalam transaction mode sebuah backend ditugaskan hanya selama durasi satu transaksi. Begitu transaksi Anda commit atau rollback, PgBouncer mengembalikan backend itu ke pool, meski klien tetap terhubung. Ini mode yang memungkinkan 2000 klien web berbagi 25 backend, karena sebagian besar klien itu idle antar request. Ini rekomendasi default untuk aplikasi request/response tanpa state: layanan NestJS, fungsi serverless, apa pun yang membuka koneksi, menjalankan query atau transaksi singkat, lalu lanjut.
Transaction mode merusak apa pun yang mengasumsikan backend sama antar transaksi. Perintah SET di luar transaksi, tabel temporer berlingkup sesi, LISTEN/NOTIFY, dan cursor tanpa nama semuanya bisa mendarat di backend berbeda pada waktu berikutnya dan salah berperilaku diam-diam. Jika ORM Anda menyetel variabel sesi per koneksi, pastikan itu berjalan di dalam transaksi atau gunakan SET LOCAL.
Statement pooling: hanya autocommit
Statement mode mengembalikan backend setelah setiap statement tunggal. Ia membawa semua keterbatasan session state dari transaction mode dan menambah satu lagi: transaksi multi-statement dilarang total, karena statement kedua sebuah transaksi bisa mendarat di backend berbeda. PgBouncer pada dasarnya memaksa autocommit. Mode ini ada untuk setup khusus seperti router query fan-out di depan Postgres yang di-shard. Untuk aplikasi biasa ini hampir tidak pernah yang Anda inginkan; jika Anda meraihnya, periksa dulu pola query Anda.
| Properti | Session | Transaction | Statement |
|---|---|---|---|
| Backend dilepas setelah | Klien putus | Tiap transaksi | Tiap statement |
| Konkurensi vs pool_size | Rendah (1 klien = 1 backend) | Tinggi (banyak klien berbagi) | Tertinggi |
| Transaksi multi-statement | Ya | Ya | Tidak |
| Var sesi / tabel temp / LISTEN | Aman | Tidak aman kecuali berlingkup transaksi | Tidak aman |
| Prepared statement | Native | Butuh max_prepared_statements greater than 0 | Butuh max_prepared_statements greater than 0 |
| Paling cocok | Klien stateful berumur panjang | Web / API / serverless | Router fan-out shard |
Jebakan prepared statement di transaction mode
Inilah kegagalan yang mengajari saya membaca changelog. Driver Postgres modern memakai prepared statement level protokol secara default. Di PgBouncer lama dengan transaction mode, klien akan PREPARE statement di backend A, mendapat backend B berbeda pada transaksi berikutnya, mencoba EXECUTE, dan Postgres melempar error prepared statement tidak ada. Solusi klasiknya adalah mematikan prepared statement di driver sepenuhnya, mengorbankan plan caching.
PgBouncer 1.21 memperbaikinya dengan benar. Ketika max_prepared_statements bukan nol, PgBouncer melacak tiap prepare level protokol, memberinya nama internal, dan menyiapkannya ulang di backend mana pun tempat klien mendarat sebelum meneruskan execute. Sejak 1.24.1 ini aktif secara default dengan max_prepared_statements diset 200. Perangkapnya, dan ini nyata: ini hanya mencakup prepare level protokol dari driver. Perintah SQL mentah PREPARE foo AS ... yang Anda ketik sendiri tidak terlihat oleh PgBouncer karena ia tidak mengurai teks query, dan DEALLOCATE yang dikirim sebagai SQL mentah sama tidak terlihatnya.
; pgbouncer.ini
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb
[pgbouncer]
pool_mode = transaction
; enable prepared-statement tracking in transaction mode (default 200 since 1.24.1)
max_prepared_statements = 200
; total client connections PgBouncer will accept
max_client_conn = 2000
; backends opened to Postgres per (user, database) pair
default_pool_size = 25
; small headroom for admin / bursty queries
reserve_pool_size = 5
reserve_pool_timeout = 3Periksa versi yang benar-benar Anda jalankan dengan SHOW VERSION di konsol admin PgBouncer sebelum berasumsi prepared statement bekerja di transaction mode. Versi di bawah 1.21 sama sekali tidak punya fitur ini, dan 1.21 hingga 1.24.0 butuh max_prepared_statements diset eksplisit.
Menghitung default_pool_size untuk aplikasi nyata
Kesalahan yang semua orang buat adalah mengukur pool berdasarkan jumlah klien. Dalam transaction mode Anda mengukurnya berdasarkan transaksi konkuren, yang jauh lebih kecil. Pakai Hukum Little: jumlah backend yang dibutuhkan sama dengan laju transaksi puncak dikali durasi transaksi rata-rata. Jika Anda melayani 500 transaksi per detik dan tiap transaksi memegang backend selama 20 milidetik, Anda butuh 500 kali 0,02, yaitu 10 backend. Tambahkan sekitar 50 persen headroom untuk jitter dan Anda mendarat di sekitar 15.
Ada batas keras dari arah sebaliknya juga. Postgres sendiri paling baik dengan sedikit backend aktif, kira-kira jumlah core CPU dikali 2 ditambah 1 untuk kerja CPU-bound. Menumpuk lebih banyak koneksi melewati titik itu membuat throughput lebih buruk, bukan lebih baik, karena backend berebut core dan lock yang sama. Jadi pool Anda punya lantai dari permintaan konkurensi dan langit-langit dari paralelisme Postgres, dan jumlah semua pool lintas setiap database harus tetap di bawah max_connections server.
- Ukur transaksi per detik puncak dan waktu transaksi rata-rata dari pg_stat_statements atau APM Anda.
- Kalikan keduanya untuk lantai, lalu tambahkan headroom 30 sampai 50 persen.
- Batasi itu di sekitar (core CPU kali 2) ditambah 1 per database untuk beban CPU-bound.
- Pastikan jumlah setiap pool tetap di bawah max_connections Postgres, sisakan ruang untuk koneksi superuser dan pemeliharaan.
- Pantau SHOW POOLS untuk cl_waiting yang bertumbuh; itu sinyal bahwa lantainya terlalu rendah.
# PgBouncer admin console: connect and watch live pool state
psql -p 6432 -U pgbouncer pgbouncer
-- backends busy/idle and clients waiting per pool
SHOW POOLS;
-- if cl_waiting is consistently > 0, default_pool_size is too small
-- if sv_idle is always high, you are over-provisioned
-- Little's Law sanity check:
-- default_pool_size >= peak_tps * avg_tx_seconds * 1.5Aturan praktis saya: transaction mode dengan max_prepared_statements dibiarkan pada default-nya, default_pool_size diturunkan dari Hukum Little dan dibatasi jumlah core, dan reserve_pool_size untuk lonjakan sesekali. Gunakan session mode hanya ketika Anda benar-benar butuh state per sesi, dan perlakukan statement mode sebagai alat spesialis yang mungkin tidak akan pernah Anda konfigurasi. Betulkan mode-nya dulu; ukuran pool hanyalah aritmetika setelahnya.

