Mode Pooling PgBouncer: Session vs Transaction vs Statement

Foto oleh KeepActive Australia from Melbourne, VIC, Australia via Wikimedia Commons (CC BY-SA 4.0)
Session pooling menugaskan koneksi backend ke klien untuk seluruh sesi dan hanya melepasnya saat putus koneksi, mempertahankan semua fitur Postgres tapi membatasi konkurensi pada ukuran pool. Transaction pooling melepas backend setelah tiap transaksi, sehingga banyak klien idle bisa berbagi pool kecil. Transaction mode adalah pilihan standar untuk beban web dan API tanpa state.
Bisa, sejak PgBouncer 1.21, selama max_prepared_statements diset ke nilai bukan nol. Sejak 1.24.1 default-nya 200 dan aktif langsung. Dukungan ini hanya mencakup prepare level protokol dari driver, bukan perintah SQL PREPARE mentah yang Anda ketik sendiri, yang tidak bisa dilihat PgBouncer.
Pakai Hukum Little: kalikan transaksi per detik puncak dengan durasi transaksi rata-rata dalam detik untuk mendapat lantai, lalu tambahkan headroom 30 sampai 50 persen. Batasi hasilnya di sekitar (core CPU kali 2) ditambah 1 per database untuk beban CPU-bound, dan pastikan jumlah semua pool tetap di bawah setting max_connections Postgres.
Ini terjadi di transaction atau statement pooling mode ketika backend yang menyiapkan statement bukan backend yang menjalankan execute. Perbaiki dengan memperbarui ke PgBouncer 1.21 atau lebih baru dan menyetel max_prepared_statements ke nilai bukan nol agar PgBouncer menyiapkan ulang statement secara langsung. Versi lama mengharuskan mematikan prepared statement di driver.
Statement pooling melepas backend setelah setiap statement tunggal dan melarang transaksi multi-statement sepenuhnya, secara efektif memaksa autocommit. Ini hanya cocok untuk setup khusus seperti router query di depan Postgres yang di-shard. Aplikasi biasa sebaiknya memakai transaction mode, karena statement mode merusak semantik transaksi normal.

Foto oleh KeepActive Australia from Melbourne, VIC, Australia via Wikimedia Commons (CC BY-SA 4.0)
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.
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.
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.
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 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 |
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.
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.
# 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.