# pgbouncer.ini
[databases]
# Endpoint-ul de la DB real
my_app_db = host=db.example.com port=5432 dbname=prod_db
[pgbouncer]
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 5000
default_pool_size = 20
# Connection string in Prisma
# DATABASE_URL="postgresql://user:pass@pgbouncer-host:6432/my_app_db?pgbouncer=true"Dacă ai trecut de faza de MVP și ai început să ai trafic serios, probabil te-ai lovit de eroarea aia clasică: „too many clients already”. Postgres e o bestie când vine vorba de query-uri complexe, dar are un călcâi al lui Ahile: fiecare conexiune nouă mănâncă între 2 și 10 MB de RAM. La 500 de conexiuni, pierzi lejer câțiva giga de memorie doar ca să ții ușa deschisă, fără să fi rulat un singur SELECT.
Am avut un proiect acum doi ani, un e-commerce cu vreo 12k useri activi simultan, unde am reușit să scad latența medie de la 800ms la 150ms doar punând un PgBouncer între Node.js și baza de date. Faza e că nu poți să-l arunci acolo pur și simplu și să te culci pe o ureche. Trebuie să înțelegi cum jonglează cu firele de execuție.
Transaction vs Session: Unde greșește lumea
Modul session e aproape inutil dacă vrei scalabilitate reală. Practic, PgBouncer se comportă ca un proxy chior: îți dă o conexiune și o ține ocupată până când clientul tău o închide. Dacă ai un pool în Node de 20 de conexiuni, PgBouncer va ține 20 de conexiuni deschise spre Postgres non-stop. E doar un hop în plus fără beneficii.
Modul transaction e unde se întâmplă magia. PgBouncer ia o conexiune din pool, execută query-ul tău, și imediat ce s-a terminat tranzacția, dă conexiunea altcuiva. Aplicația ta crede că are conexiunea ei privată, dar în spate, Postgres vede doar un flux constant de date pe câteva fire de execuție. Am economisit cam 40% din resursele CPU pe un DB mediu trecând pe transaction mode, pentru că am eliminat overhead-ul de fork() pe care îl face Postgres la fiecare conexiune nouă.
Trade-off-ul e că pierzi LISTEN/NOTIFY, tabelele temporare și, cel mai dureros, Prepared Statements (dacă nu ești atent).
Prisma și blestemul conexiunilor
Prisma e un ORM excelent pentru productivitate, dar e un „bulimic” de conexiuni. Fiecare instanță de microserviciu își deschide propriul pool intern. Dacă ai 10 pod-uri în Kubernetes, fiecare setat cu un pool de 10, ai deja 100 de conexiuni blocate.
Ca să meargă cu PgBouncer în mod transaction, trebuie neapărat să adaugi ?pgbouncer=true în connection string. Fără parametrul ăsta, Prisma încearcă să folosească prepared statements care sunt legate de sesiune. O să te trezești cu erori de tipul „prepared statement already exists” pentru că PgBouncer refolosește conexiunea pentru alt user care a trimis alt query. E un haos total dacă uiți flag-ul ăsta.
Cât de mare să fie pool-ul?
O greșeală de începător e să pui default_pool_size = 100 doar pentru că ai auzit că e bine să ai rezerve. Dacă Postgres-ul tău are max_connections = 100, ai umplut deja tot și nu mai poți intra nici cu un psql de mentenanță.
Regula mea e simplă: default_pool_size ar trebui să fie cam 80% din max_connections de pe Postgres, împărțit la numărul de baze de date pe care le proxy-iești. Pe un server cu 4 nuclee, am observat că un pool de 20-30 de conexiuni reale către Postgres e mai mult decât suficient pentru a duce mii de query-uri pe secundă. Mai mult nu înseamnă mai rapid; înseamnă doar mai mult context switching pentru procesorul bazei de date.
În concluzie, PgBouncer e obligatoriu dacă folosești un ORM modern în producție. Voi ce limite de conexiuni folosiți pe instanțele de producție?