// schema.prisma - Configurarea corectă pentru PgBouncer în Transaction Mode
datasource db {
provider = "postgresql"
// Conexiunea prin PgBouncer (Port 6432 / Transaction Mode)
url = env("DATABASE_URL")
// Conexiunea directă la Postgres pentru Prisma CLI / Migrations (Port 5432)
directUrl = env("DIRECT_URL")
}
// .env example:
// DATABASE_URL="postgres://user:pass@pgbouncer.internal:6432/mydb?pgbouncer=true&connection_limit=10"
// DIRECT_URL="postgres://user:pass@postgres.internal:5432/mydb"Dacă folosești Prisma cu PostgreSQL în producție pe trafic serios, ești la o singură campanie de marketing distanță de faimosul FATAL: remaining connection slots are reserved for non-replication superuser connections. Am pățit-o acum un an la un proiect unde aveam vreo 8k useri concurenți. Postgres-ul începuse să gâfâie grav și să consume 90% CPU doar pe context switching între procese, deși query-urile în sine rulau rapid. Trecerea la PgBouncer a fost salvarea noastrǎ, dar integrarea cu Prisma are câteva capcane urâte dacă nu ești atent.
Session vs Transaction Mode: Unde e șmecheria?
Implicit, lumea tinde să lase PgBouncer pe Session mode. În modul ăsta, proxy-ul funcționează ca un router pasiv: un client NodeJS se conectează, PgBouncer îi alocă o conexiune fizică de Postgres și i-o lasă dedicată până când procesul de Node se închide. E util dacă folosești LISTEN/NOTIFY sau temporary tables, dar e cvasi-inutil când ai sute de containere serverless sau pods de Kubernetes care deschid conexiuni nesăbuite.
Transaction mode e unde se întâmplă magia. PgBouncer ține conexiunea fizică legată de client DOAR pe durata unei tranzacții SQL. Imediat ce s-a dat COMMIT sau ROLLBACK, conexiunea se întoarce în pool și e dată altui request în câteva microsecunde. Am reușit astfel să deservim 12.000 req/min din Node.js menținând doar 20 de conexiuni reale deschise către Postgres.
Capcana Prisma: Prepared Statements
Trade-off-ul dur dureros în Transaction Mode este că pierzi caracteristicile de sesiune. Aici crapă Prisma în mod tradițional. Implicit, clientul de Prisma încearcă să folosească Prepared Statements (interogări pre-compilate) pentru performanță.
Fluctuația e simplă și toxică: Prisma trimite PREPARE stmt1 pe conexiunea A. La următorul query, PgBouncer îți dă conexiunea B (pentru că A a fost dată altui request). Prisma încearcă EXECUTE stmt1 pe conexiunea B, iar Postgres aruncă eroare: prepared statement "stmt1" does not exist. Rezultatul? Eroare 500 random la utilizatori.
Soluția e să-i spui explicit lui Prisma că vorbește printr-un pooler adăugând ?pgbouncer=true în connection string, plus configurarea unui directUrl pentru migrări.
Dimensionarea corecta a Pool-ului (Less is More)
Cea mai mare greșeală pe care o văd în code-review-uri este setarea unui pool imens. Oamenii pun default_pool_size = 200 în PgBouncer credând că așa scalează. În realitate, fiecare conexiune activă din Postgres e un proces separat (forked) în Linux. Peste un anumit prag, procesorul își pierde timpul mutând memoria din stânga în dreapta (context switching).
Formula empirică pe care o folosesc și care nu m-a dezamăgit e simplă:
max_connections = (CPU cores * 2) + effective_spindle_count
Pe o instanță de Postgres cu 8 vCPU-uri și stocare NVMe, un default_pool_size = 25 în PgBouncer oferă un throughput de 3-4 ori mai mare decât un pool de 200 de conexiuni. Latența medie la noi a scăzut de la 120ms la 18ms pur și simplu prin limitarea conexiunilor active.
Concluzii și bune practici
Transaction mode cu PgBouncer câștigă detașat pe arhitecturi moderne de microservicii sau serverless, dar te obligă să separi conexiunea de migrare (care are nevoie de Session mode) de conexiunea de runtime. Dacă folosești Prisma, asigură-te că ai definit directUrl și folosești versiuni recente unde engine-ul gestionează corect dezactivarea prepared statements.
Voi la ce număr de conexiuni reale ați plafonat baza de date înainte să aveți degradări de performanță?