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

PgBouncer în producție: Transaction vs Session mode și capcanele cu Prisma

De Bogdan Răducanu, 26 iul. 2026 · 7 vizualizări · 2 like-uri

Postat 26 iul. 2026
prisma
// schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL") // Connection via PgBouncer (e.g. port 6432)
  directUrl = env("DIRECT_URL")   // Direct connection to PostgreSQL (e.g. port 5432)
}

generator client {
  provider = "prisma-client-js"
}

De ce ne blochează Postgres când crește traficul?

Acum vreun an, la un proiect unde aveam în jur de 12k utilizatori activi simultan, ne-am trezit că aplicația crapă la peak. Pod-urile de Node.js scalau frumos în Kubernetes, dar PostgreSQL plângea în hohote din cauza celor 300 de conexiuni deschise direct. Rezolvarea n-a fost să mărim resursele pe DB, ci să punem PgBouncer în față și să configurăm corect pool-urile împreună cu Prisma.

Clasicul thrashing de context switching apare când fiecare proces de Node își deschide propriile 10 conexiuni. Postgres creează câte un proces OS separat pentru fiecare client, iar la 300+ procese active, CPU-ul bazei de date petrece mai mult timp mutând contextul între procese decât executând query-uri SQL.

Session vs Transaction Mode: Ce alegem?

PgBouncer funcționează ca un proxy inteligent, dar alegerea modului de pooling schimba radical ecuația:

  1. Session mode: PgBouncer îi dă clientului o conexiune dedicată din Postgres cât timp clientul rămâne conectat. Dacă ai 500 de pod-uri de Node, ai nevoie tot de 500 de conexiuni în DB. Practic, nu rezolvi mare lucru pentru aplicații web stateless.
  2. Transaction mode: Conexiunea e alocată DOAR pe durata unei tranzacții SQL. Imediat ce s-a dat COMMIT sau ROLLBACK, conexiunea din Postgres se întoarce în pool. Aici e magia: 500 de pod-uri HTTP pot fi deserviți lejer de doar 20-30 de conexiuni reale de baza de date.

Trade-off-ul direct: În transaction mode pierzi suportul pentru Advisory Locks, comenzi SET persistente pe sesiune, Temporary Tables și, istoric vorbind, Prepared Statements (deși PgBouncer 1.21+ a introdus suport experimental pentru ele).

Capcana Prisma: directUrl vs DATABASE_URL

Dacă folosești Prisma ORM, integrarea cu PgBouncer în transaction mode te poate lovi rău la deploy sau în runtime dacă nu ești atent.

Prisma are nevoie de două conexiuni separate configurate în schema.prisma. Una trece prin PgBouncer (portul 6432) pentru traficul normal de query-uri, iar alta merge direct la Postgres (portul 5432) folosind directUrl.

De ce e obligatoriu așa? Pentru că migrațiile (prisma migrate deploy) folosesc advisory locks pentru a preveni rularea concurentă a migrațiilor din mai multe pod-uri. Dacă încerci să rulezi migrații prin PgBouncer în transaction mode, procesul va eșua sau va lăsa locks blocate.

Atenție mare și la prisma.$transaction(): dacă folosești varianta interactive transactions (callback async), Prisma ține o conexiune blocată pe toată durata execuției codului. Dacă faci un await fetch() către un API extern în interiorul acelui callback, vei bloca un slot din pool-ul PgBouncer și îți vei gâtui singur aplicația.

Cum calculezi corect pool size-ul?

Formula clasică recomandată de echipa PostgreSQL este adesea ignorată, dar funcționează impecabil: max_connections = ((număr_core_uri * 2) + număr_disk_uri)

Pe un server de DB cu 8 vCPU și stocare NVMe, un pool real de 20-30 de conexiuni oferă un throughput mai mare decât 200 de conexiuni concurente.

Noi am setat în PgBouncer default_pool_size = 25 și reserve_pool_size = 5, iar în Postgres am coborât max_connections de la 300 la 60. Rezultatul? Latența medie pe p95 a scăzut cu 35%, iar consumul de CPU pe RDS a căzut de la 98% la sub 40% în orele de vârf.

Voi cum gestionați migrațiile Prisma în CI/CD când aveți PgBouncer în față? Folosiți conexiuni directe separate pe runner-ul de GitHub Actions sau aveți alt workflow?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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