// schema.prisma - Configurare corectă pentru PgBouncer
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // Port 6432 (PgBouncer) + ?pgbouncer=true
directUrl = env("DIRECT_DATABASE_URL") // Port 5432 (Direct la Postgres pentru migrări)
}
// .env
// DATABASE_URL="postgresql://user:pass@localhost:6432/mydb?pgbouncer=true&connection_limit=10"
// DIRECT_DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"Dacă ai încercat vreodată să scalezi o aplicație Node.js cu Prisma peste Postgres, probabil te-ai lovit de clasica eroare sorry, too many clients already. Am pățit-o acum doi ani la un proiect cu peste 12.000 de utilizatori activi concurenți, când baza de date a îngenuncheat pur și simplu din cauza sutelor de conexiuni deschise aiurea de instanțele noastre din cloud.
Soluția standard este un pooler de conexiuni, iar PgBouncer este regele nedeclarat aici. Însă, dacă îl arunci în producție cu setările default, riști să strici totul.
De ce transaction mode e singura opțiune reală (și ce strică)
În PgBouncer ai trei moduri de operare: session, transaction și statement. Pe ultimul îl excludem din start pentru că blochează funcționalități de bază din SQL.
Session mode este destul de inutil dacă vrei scalare adevărată. Practic, ține o conexiune fizică blocată pentru un client din aplicație până când acesta se deconectează complet. Dacă ai 50 de instanțe serverless care pornesc și se opresc, vei epuiza conexiunile imediat.
Transaction mode este salvarea. PgBouncer oferă o conexiune fizică doar pe durata unei tranzacții. Imediat ce tranzacția s-a terminat, conexiunea se întoarce în pool pentru a fi folosită de alt client.
Dar există un trade-off major aici: pierzi suportul nativ pentru prepared statements (server-side). Postgres se așteaptă ca ID-ul statementului pregătit să existe pe conexiunea fizică pe care rulează query-ul. Însă, în transaction mode, următorul tău query poate ajunge pe o altă conexiune fizică din pool. Rezultatul? Erori urâte de tipul prepared statement does not exist.
Integrarea cu Prisma
Prisma folosește un query engine scris în Rust care își face propriul management de conexiuni. Pentru a folosi PgBouncer în transaction mode, trebuie să facem două configurări obligatorii.
În primul rând, trebuie să îi spunem explicit lui Prisma că vorbește cu un proxy adăugând parametrul pgbouncer=true în URL-ul de conexiune. Acest parametru forțează Prisma să folosească prepared statements de tip client-side (adică le compilează local în engine înainte de a le trimite ca SQL brut).
În al doilea rând, avem nevoie de două conexiuni diferite în schema.prisma: una pentru aplicația de zi cu zi (care trece prin PgBouncer pe portul 6432) și una directă către Postgres (pe portul 5432) folosită exclusiv pentru migrări. Dacă încerci să rulezi prisma migrate prin PgBouncer în transaction mode, migrările vor eșua garantat din cauza tranzacțiilor lungi și a tabelelor temporare lock-uite.
Dimensionarea pool-ului fără să distrugi serverul
Am văzut des greșeala de a seta pool_size în PgBouncer la valori gigantice, gen 500, crezând că „așa e sigur”. Este o rețetă sigură pentru dezastru.
Pe un server de producție cu 8 vCPUs și 32GB RAM, am limitat conexiunile directe în Postgres (max_connections) la 100. În PgBouncer, am setat default_pool_size = 75 și max_client_conn = 5000.
De ce? Pentru că Postgres consumă resurse serioase pentru fiecare conexiune deschisă (cam 10MB de RAM per conexiune în idle, plus context switching la nivel de procese CPU). Lăsând pool-ul din PgBouncer la 75, ne asigurăm că baza de date lucrează mereu la capacitate optimă, fără să se sufoce. Restul de 25 de conexiuni până la 100 le-am lăsat libere pentru migrări, scripturi de mentenanță și monitorizare.
După această optimizare, utilizarea CPU-ului pe baza de date a scăzut de la 85% la un stabil 25%, iar memoria ocupată de conexiuni a scăzut cu aproape 3GB.
Cum gestionați voi pooling-ul în producție? Preferați să rulați PgBouncer ca sidecar în Kubernetes sau mergeți pe soluții managed gen AWS RDS Proxy ori poolerele native de la Supabase/Neon?