# pgbouncer.ini config
[databases]
# Prisma are nevoie de portul 6432 (default pgbouncer)
# Adaugă ?pgbouncer=true în schema.prisma
mydb = host=127.0.0.1 port=5432 dbname=production_db
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 20
reserve_pool_size = 5
# Important pentru stabilitate
server_idle_timeout = 600
query_timeout = 0Dacă ai ajuns în punctul în care PostgreSQL îți mănâncă 80% CPU deși n-ai cine știe ce trafic, cel mai probabil te lupți cu overhead-ul de conexiuni. Postgres creează un proces nou pentru fiecare conexiune, iar asta devine extrem de scump când ai sute de clienți care stau degeaba (idle) sau fac query-uri scurte și dese.
Am pățit asta la un proiect cu vreo 12 microservicii unde fiecare avea propriul pool de 20 de conexiuni. Ne-am trezit cu peste 240 de conexiuni deschise pe o mașină care abia ducea 100 în mod eficient. Soluția a fost, evident, PgBouncer, dar dracul e în detalii când vine vorba de configurare.
Session vs Transaction Mode: Unde se rupe filmul
Cea mai mare greșeală pe care o văd e folosirea modului session. În modul ăsta, PgBouncer se comportă ca un proxy chior: când clientul ia o conexiune, o ține până când se deconectează. Dacă ai un app server care ține conexiunile deschise (cum face Prisma sau majoritatea ORM-urilor), n-ai rezolvat nimic. Tot vei atinge limita de max_connections din Postgres.
Magia se întâmplă în pool_mode = transaction. Aici, PgBouncer îi dă clientului o conexiune doar cât timp rulează un query sau o tranzacție. Imediat ce s-a terminat COMMIT-ul, conexiunea fizică se întoarce în pool-ul bouncer-ului și poate fi refolosită de alt client. Am reușit să deservim 1500 de conexiuni de client cu doar 40 de conexiuni reale către baza de date, reducând latența medie cu 15%.
Trade-off-ul? În modul tranzacție pierzi Prepared Statements la nivel de protocol, pentru că starea lor e legată de conexiunea fizică. Dacă un client face PREPARE și următorul query ajunge pe altă conexiune fizică, primești eroare.
Prisma și pgbouncer=true
Dacă folosești Prisma, știi probabil că motorul lor de query (Query Engine) iubește să deschidă conexiuni. Ca să funcționeze cu PgBouncer în mod tranzacție, trebuie neapărat să adaugi ?pgbouncer=true în URL-ul de conexiune.
Fără flag-ul ăsta, Prisma încearcă să folosească prepared statements și totul crapă cu erori criptice de tipul "prepared statement does not exist". Când pui flag-ul, Prisma face un workaround: nu mai folosește protocolul binar pentru prepared statements, ci face inline la parametri sau folosește tehnici compatibile cu proxy-urile. E un mic cost de performanță aici, dar e infim față de beneficiul de a nu bloca baza de date.
Cum calculezi Pool Size-ul?
Nu te arunca la cifre mari. O regulă empirică pe care am testat-o: default_pool_size în PgBouncer ar trebui să fie cam de 1.5x - 2x numărul de nuclee CPU ale bazei de date. Dacă ai un DB cu 4 vCPU, un pool de 10-15 conexiuni e de obicei suficient pentru throughput maxim.
Am făcut un test de load pe un DB cu 8k useri concurenți. Cu un pool de 200 (prea mare), context switching-ul în Linux omora performanța. Am scăzut pool-ul la 30 și throughput-ul a crescut cu 40%. Mai puțin e, paradoxal, mai mult în Postgres.
Nu uita să setezi max_client_conn la o valoare mare (ex. 2000), pentru că asta e limita de conexiuni de intrare pe care le acceptă PgBouncer, în timp ce default_pool_size controlează câte pleacă spre Postgres.
Voi cum gestionați conexiunile când scalați? Mergeți pe PgBouncer external sau folosiți soluții managed gen AWS RDS Proxy?