[databases]
; Conectare la DB-ul real
main_db = host=127.0.0.1 port=5432 dbname=myapp_prod
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
; Transaction mode e cheia pentru scalabilitate
pool_mode = transaction
; Nu exagera cu cifrele astea
default_pool_size = 20
max_client_conn = 1000
reserve_pool_size = 5
; Prisma connection string example:
; DATABASE_URL="postgresql://user:pass@localhost:6432/main_db?pgbouncer=true&connection_limit=1"Am lucrat recent la un proiect unde aveam vreo 12 microservicii care atacau o singură instanță de Postgres. Fără un pooler, RAM-ul pe serverul de bază de date stătea constant la 90% doar din cauza proceselor de backend (fork-urile de Postgres sunt scumpe). Am văzut mulți devi care aruncă PgBouncer în stack doar pentru că „așa se face”, fără să înțeleagă că o configurare greșită poate să-ți facă mai mult rău decât bine, mai ales dacă folosești un ORM modern ca Prisma.
Postgres creează un proces nou pentru fiecare conexiune. Asta înseamnă cam 10MB de RAM consumați instant, plus overhead-ul de context switching. La 100 de conexiuni, deja ai pierdut 1GB de RAM doar ca să „stai la masă”. Aici intervine PgBouncer, dar alegerea modului de funcționare e critică.
Session vs Transaction Mode
Majoritatea încep cu Session Mode pentru că e „safe”. În modul ăsta, când clientul ia o conexiune, o ține până când dă disconnect. E practic un proxy chior. Dacă ai o aplicație care ține conexiunile deschise (cum fac majoritatea pool-urilor de conexiuni din Node.js), PgBouncer nu te ajută cu nimic aici. Tot vei lovi limita de max_connections din Postgres.
Magia reală e în Transaction Mode. Aici, PgBouncer eliberează conexiunea către Postgres imediat ce tranzacția s-a terminat, chiar dacă aplicația ta crede că încă e conectată. Am avut un caz unde am redus numărul de conexiuni reale în Postgres de la 800 la sub 50, păstrând același throughput.
Trade-off-ul? Transaction mode sparge Prepared Statements (deși versiunile noi de PgBouncer au un workaround) și nu poți folosi variabile de sesiune (SET NAMES, etc.) sau tabele temporare care să supraviețuiască între tranzacții. Dacă aplicația ta se bazează pe LISTEN/NOTIFY, ai încurcat-o, nu merge în transaction mode.
Calibrarea Pool Size-ului
O greșeală comună e să pui default_pool_size = 100 doar pentru că ai serverul mare. Regula mea de deget, testată în producție, e mult mai conservatoare. Postgres scalează mai bine cu puține conexiuni care rulează la viteză maximă decât cu multe conexiuni care se bat pe resurse.
De obicei, plec de la formula: (2 * număr de nuclee CPU) + număr de discuri. La un server cu 4 vCPUs, un pool de 10-15 conexiuni e de multe ori mai rapid decât unul de 100. Restul conexiunilor de la clienți stau la coadă în PgBouncer (wait queue), ceea ce e mult mai ieftin decât să stresezi kernel-ul cu mii de procese Postgres.
Prisma și coșmarul conexiunilor
Prisma e probabil cel mai „gurmand” ORM când vine vorba de conexiuni. Fiecare instanță de PrismaClient vrea propriul pool. Dacă ai 10 containere în K8s, fiecare cu un connection_limit=10, ai deja 100 de conexiuni.
Ca să faci Prisma să meargă cu PgBouncer în Transaction Mode, trebuie să adaugi ?pgbouncer=true în connection string. Asta îi spune Prismei să nu folosească prepared statements într-un mod care ar crăpa conexiunea. Dar atenție: trebuie să folosești portul de PgBouncer (de obicei 6432), nu cel de Postgres (5432).
Am pățit o fază unde cineva a pus pgbouncer=true dar s-a conectat direct la baza de date. Performanța a scăzut cu 30% pentru că Prisma făcea un round-trip în plus pentru fiecare query, încercând să curețe starea sesiunii pe care oricum nu o gestiona un pooler.
Ce experiențe ați avut cu limitele de conexiuni în Postgres când ați trecut de 1k useri concurenți?