eduardweb.
PostgreSQLAvansat#devops#prisma#postgresql#databases#pgbouncer

PgBouncer în producție: Transaction mode, pool size-uri reale și integrarea cu Prisma

De Mihai Popescu, 10 aug. 2026 · 3 vizualizări · 3 like-uri

Postat acum 6 zile
prisma
// schema.prisma
datasource db {
  provider  = "postgresql"
  // Trafic de aplicație -> trece prin PgBouncer (Transaction mode)
  url       = env("DATABASE_URL") 
  // Migrări de schemă -> conexiune directă în Postgres pe portul 5432
  directUrl = env("DIRECT_DATABASE_URL")
}

// .env
// DATABASE_URL="postgresql://user:pass@pgbouncer:6432/mydb?pgbouncer=true&connection_limit=10"
// DIRECT_DATABASE_URL="postgresql://user:pass@postgres:5432/mydb"

Am doborât un Postgres în producție acum doi ani doar pentru că am lăsat Prisma să deschidă conexiuni direct în DB la fiecare pod de Kubernetes. Când traficul a crescut la peste 12k utilizatori simultani, Postgres gâfâia în context switching cu peste 500 de conexiuni active, iar latența pe query-uri simple sărise de la 4ms la 2.5 secunde.

Atunci am băgat PgBouncer în schemă. Diferența a fost masivă, dar dacă nu ești atent la câteva detalii de configurare, îți rupi gâtul rapid.

Session vs Transaction Mode: Unde se rupe filmul

PgBouncer știe trei moduri, dar în 95% din cazuri alegi între session și transaction.

În Session mode, PgBouncer se poartă ca un simplu proxy. Când aplicația ta cere o conexiune, i se alocă o conexiune fizică de Postgres și rămâne blocată acolo până când aplicația se deconectează explicit. E util dacă folosești LISTEN/NOTIFY, cursori WITH HOLD sau variabile de sesiune (SET LOCAL). Dar economiile de resurse sunt minime.

În Transaction mode, magic se întâmplă altfel: conexiunea de Postgres este dată clientului doar pe durata unei singure tranzacții (BEGIN ... COMMIT). Imediat ce s-a terminat tranzacția, conexiunea Postgres revine în pool.

Trade-off-ul e dur: pierzi prepared statements clasice, nu mai poți folosi SET la nivel de sesiune și advisory locks devin periculoase. Am economisit 80% din RAM-ul bazei de date (de la 8GB la 1.2GB consumat de procese Postgres), dar a trebuit să regândim modul în care vorbeam cu DB-ul.

Capcana cu Prisma și Prepared Statements

Prisma folosește sub capotă un engine în Rust care se bazează masiv pe prepared statements pentru performanță. Dacă pui PgBouncer în transaction mode în fața lui fără ajustări, vei vedea logurile pline de erori de tip prepared statement "s0" already exists sau prepared statement does not exist.

De ce? Pentru că query-ul e pregătit pe conexiunea A, dar când execută query-ul, PgBouncer îl trimite pe conexiunea B, unde statement-ul nu a fost inițializat.

Soluția este să-i spui explicit lui Prisma că vorbește cu un proxy prin parametrul pgbouncer=true în connection string și să dezactivezi prepared statements. De asemenea, ai nevoie de două URL-uri separate: unul pentru traficul de aplicație (prin PgBouncer) și unul direct (pentru migrări de schemă, fiindcă prisma migrate are nevoie de conexiune directă).

Cât de mare pui pool-ul? Math, nu ghicit

Greșeala clasică pe care o văd la mulți developperi e că pun default_pool_size = 100 în PgBouncer și connection_limit = 50 în aplicație. Rezultatul? Același bottleneck în Postgres.

Formula recomandată de PostgreSQL pentru conexiuni fizice este:

max_connections = (număr_vCPU * 2) + număr_discuri

Pentru un server de Postgres cu 8 vCPU și stocare NVMe, un default_pool_size = 20-25 în PgBouncer este absolut suficient. PgBouncer poate să țină la ușă 3.000 de conexiuni de la pods-urile de Node.js/Go, dar le va rula prin cele 20 de conexiuni reale către Postgres. DB-ul va procesa query-urile instant în loc să piardă timp schimbând contextul între 500 de thread-uri.

Voi ce setup folosiți pentru gestionarea conexiunilor în producție: PgBouncer separat, Supavisor, sau conexiuni directe gestionate la nivel de pods?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.